目录
网络流学习笔记
网络流资料
最大流dinic
#include<iostream> #include<cstdio> #include<cstring> #include<queue> using namespace std; const int INF = 1e9; const int N = 10005; const int M = 200005; int to[M], ne[M]; int w[M], h[N], tot = -1; inline void add(int x,int y,int z) { ne[++tot] = h[x], h[x] = tot; to[tot] = y, w[tot] = z; } template <typename T> void read(T &x) { x = 0; int f = 0; char c = getchar(); for (;!isdigit(c);c=getchar()) if (c=='-') f=1; for (;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48); if (f) x = -x; } int n, m, s, t; int maxflow; int depth[N], cur[N]; queue<int> q; bool bfs(void) { memset(depth, 0x7f, sizeof(depth)); while (q.size()) q.pop(); for (int i = 1;i <= n; i++) cur[i] = h[i]; depth[s] = 0; q.push(s); while (q.size()) { int x = q.front(); q.pop(); for (int i = h[x]; ~i; i = ne[i]) { int y = to[i]; if (depth[y] > INF && w[i]) { depth[y] = depth[x] + 1; q.push(y); } } } return depth[t] <= INF; } int dfs(int now, int limit) { if (!limit || now == t) return limit; int flow = 0, f; for (int i = cur[now]; ~i; i = ne[i]) { int y = to[i]; cur[now] = i; if (depth[y] != depth[now] + 1) continue; f = dfs(y, min(limit, w[i])); if (!f) continue; flow += f; limit -= f; w[i] -= f, w[i ^ 1] += f; if (!limit) break; } return flow; } int main() { read(n), read(m), read(s), read(t); memset(h, -1, sizeof(h)); for (int i = 1;i <= m; i++) { int x, y, z; read(x), read(y), read(z); add(x, y, z); add(y, x, 0); } while (bfs()) maxflow += dfs(s, INF); printf ("%d\n", maxflow); return 0; }
费用流dinic
注意:
- 反向边费用为负
- dfs时用v标记每个点有没有被标记
#include<iostream> #include<cstdio> #include<cstring> #include<queue> using namespace std; const int N = 50050; const int M = 500500; const int INF = 0x3f3f3f3f; int read(void) { int x = 0; bool f = 0; char c = getchar(); for (;!isdigit(c);c=getchar()) if (c=='-') f=1; for (;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48); return f ? -x : x; } int h[N], to[M], ne[M]; int cost[M], w[M], tot = 1; inline void add(int x,int y,int z,int k) { ne[++tot] = h[x], h[x] = tot; to[tot] = y, w[tot] = z, cost[tot] = k; } int n, m, s, t; int dis[N], cur[N]; int v[N]; queue<int> q; bool spfa(void) { memset(dis, 0x3f, sizeof(dis)); memset(v, 0, sizeof(v)); q.push(s); dis[s] = 0; while (q.size()) { int x = q.front(); q.pop(); v[x] = 0; for (int i = h[x]; i; i = ne[i]) { int y = to[i]; if (!w[i]) continue; if (cost[i] + dis[x] < dis[y]) { dis[y] = cost[i] + dis[x]; if (!v[y]) { v[y] = 1; q.push(y); } } } } if (dis[t] >= INF) return 0; for (int i = 1;i <= n; i++) cur[i] = h[i]; return 1; } int dfs(int x,int lim) { if (lim <= 0 || x == t) return lim; int res = 0; v[x] = 1; for (int i = cur[x]; i; i = ne[i]) { int y = to[i]; cur[x] = i; if (v[y] || dis[y] != dis[x] + cost[i]) continue; int f = dfs(y, min(lim, w[i])); w[i] -= f, w[i^1] += f; res += f, lim -= f; if (lim <= 0) return res; } return res; } long long ans1, ans2; int main() { freopen("hs.in","r",stdin); n = read(), m = read(), s = read(), t = read(); for (int i = 1;i <= m; i++) { int x = read(), y = read(), z = read(), k = read(); add(x, y, z, k); add(y, x, 0, -k); } while (spfa()) { int tmp = dfs(s, INF); ans1 += tmp, ans2 += tmp * dis[t]; } cout << ans1 << ' ' << ans2 << endl; return 0; }
无源汇有上下界可行流
先让每条边流量设为最小值, 发现可能流入流出量并不平衡
但是没问题, 反手一个转化, 要将网络流图加一个附加网络流图, 每条边的流量为最大流量-最小流量
对于每一个点, 都有一个A[i]表示其流入流量与流出流量的差
若A[i] > 0, 说明流多了, 要往外流, 附加流的流入量要小于流出量, 否则就是附加流的流入量要大与流出量
那么我们新建一个超级源点s和超级汇点t
如果A[i]>0, 连一条从s到i的流量为A[i]的边, 否则连一条从i到t的流量为-A[i]的边
跑一遍最大流, 如果最大流等于s的出边流量之和, 那么满足题意, 每条边的流量加上原来的最小流量即为所求
代码
#include<iostream> #include<cstring> #include<cstdio> #include<queue> using namespace std; const int N = 400005; int h[N], ne[N], to[N]; int w[N], tot = 1; inline void add(int x,int y,int z) { ne[++tot] = h[x], h[x] = tot; to[tot] = y, w[tot] = z; } int read(void) { int x = 0; bool f = 0; char c = getchar(); for (;!isdigit(c);c=getchar()) if (c=='-') f=1; for (;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48); if (f) return -x; return x; } int n, m; int a[N], ans[N]; int num[N]; int sum, s, t; const int INF = 0x7fffffff; int cur[405], dep[405]; queue<int> q; bool bfs(void) { memset(dep, 0, sizeof(dep)); q.push(s); dep[s] = 1; while (q.size()) { int x = q.front(); q.pop(); for (int i = h[x]; i; i = ne[i]) { int y = to[i]; if (!w[i] || dep[y]) continue; dep[y] = dep[x] + 1; q.push(y); } } if (!dep[t]) return false; for (int i = 1;i <= n+4; i++) cur[i] = h[i]; return true; } int flow = 0; int dfs(int x,int lim) { if (x == t) return lim; int res = 0; for (int i = cur[x]; i; i = ne[i]) { int y = to[i]; cur[x] = i; if (!w[i] || dep[x] + 1 != dep[y]) continue; int f = dfs(y, min(lim, w[i])); w[i] -= f, w[i^1] += f; res += f, lim -= f; if (!lim) return res; } return res; } int main() { n = read(), m = read(); for (int i = 1;i <= m; i++) { int x = read(), y = read(), l = read(), r = read(); ans[i] = l; a[y] += l, a[x] -= l; add(x, y, r - l); add(y, x, 0); num[i] = tot; } s = n + 1, t = n + 2; for (int i = 1;i <= n; i++) { if (a[i] > 0) { sum += a[i]; add(s, i, a[i]); add(i, s, 0); } else { add(i, t, -a[i]); add(t, i, 0); } } while (bfs()) flow += dfs(s, INF); if (sum != flow) { cout << "NO\n"; return 0; } cout << "YES\n"; for (int i = 1;i <= m; i++) printf ("%d\n", ans[i] + w[num[i]]); return 0; }
有源汇有上下界可行流 (最大流/最小流)
有汇源有上下界的可行流
设S, T为超级源点和超级汇点, s, t 为源点和汇点
埋伏他一手, 首先把有汇源问题转化为无汇源问题, 闷声发大财
因为源点和汇点入流和出流不平衡, 从t向s连一条无穷大的边使它平衡, 然后在跑上一问题
设tmp = t 到 s的边的流量, res1 = 拆掉无穷大的边从s到t所跑的最大流, res2 = 拆掉无穷大的边从t到s所跑的最大流
最大流, ans = tmp + res1
思路: 先找到一个可行流, 在残余网络中在跑一遍最大流, 使不能产生新流
最小流, ans = tmp - res2
思路: 先找到一个可行流, 从t到s取消流量, 即回退可行流中可以退的流.
最大流
#include<iostream> #include<cstring> #include<cstdio> #include<queue> using namespace std; const int N = 400005; int h[N], ne[N], to[N]; int w[N], tot = 1; inline void add(int x,int y,int z) { ne[++tot] = h[x], h[x] = tot; to[tot] = y, w[tot] = z; } int read(void) { int x = 0; bool f = 0; char c = getchar(); for (;!isdigit(c);c=getchar()) if (c=='-') f=1; for (;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48); if (f) return -x; return x; } int n, m; int a[N]; int num[N]; int sum, ss, tt; const int INF = 0x7fffffff; int cur[405], dep[405]; queue<int> q; bool bfs(int s,int t) { memset(dep, 0, sizeof(dep)); q.push(s); dep[s] = 1; while (q.size()) { int x = q.front(); q.pop(); for (int i = h[x]; i; i = ne[i]) { int y = to[i]; if (!w[i] || dep[y]) continue; dep[y] = dep[x] + 1; q.push(y); } } if (!dep[t]) return false; for (int i = 1;i <= n+4; i++) cur[i] = h[i]; return true; } int flow = 0; int dfs(int x,int lim,int t) { if (x == t) return lim; int res = 0; for (int i = cur[x]; i; i = ne[i]) { int y = to[i]; cur[x] = i; if (!w[i] || dep[x] + 1 != dep[y]) continue; int f = dfs(y, min(lim, w[i]), t); w[i] -= f, w[i^1] += f; res += f, lim -= f; if (!lim) return res; } return res; } int ans, s, t; int main() { n = read(), m = read(), ss = read(), tt = read(); for (int i = 1;i <= m; i++) { int x = read(), y = read(), l = read(), r = read(); a[y] += l, a[x] -= l; add(x, y, r - l); add(y, x, 0); num[i] = tot; } s = n + 1, t = n + 2; for (int i = 1;i <= n; i++) { if (a[i] > 0) { sum += a[i]; add(s, i, a[i]); add(i, s, 0); } else { add(i, t, -a[i]); add(t, i, 0); } } add(tt, ss, INF); while (bfs(s, t)) flow += dfs(s, INF, t); if (sum != flow) { cout << "please go home to sleep\n"; return 0; } flow = w[tot^1]; w[tot] = w[tot^1] = 0; while (bfs(ss, tt)) { dep[n+1] = dep[n+2] = INF; flow += dfs(ss, INF, tt); } cout << flow << endl; return 0; }
最小流
#include<iostream> #include<cstring> #include<cstdio> #include<queue> #define ll long long using namespace std; const int N = 800005; int h[N], ne[N], to[N]; ll w[N], tot = 1; inline void add(int x,int y,ll z) { ne[++tot] = h[x], h[x] = tot; to[tot] = y, w[tot] = z; } ll read(void) { ll x = 0; bool f = 0; char c = getchar(); for (;!isdigit(c);c=getchar()) if (c=='-') f=1; for (;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48); if (f) return -x; return x; } int n, m; ll a[N]; ll num[N]; ll sum, ss, tt; const ll INF = 0x7fffffffffff; ll cur[N], dep[N]; queue<int> q; bool bfs(int s,int t) { memset(dep, 0, sizeof(dep)); q.push(s); dep[s] = 1; while (q.size()) { int x = q.front(); q.pop(); for (int i = h[x]; i; i = ne[i]) { int y = to[i]; if (!w[i] || dep[y]) continue; dep[y] = dep[x] + 1; q.push(y); } } if (!dep[t]) return false; for (int i = 1;i <= n+4; i++) cur[i] = h[i]; return true; } ll flow = 0; int dfs(int x,ll lim,int t) { if (x == t) return lim; ll res = 0; for (int i = cur[x]; i; i = ne[i]) { int y = to[i]; cur[x] = i; if (!w[i] || dep[x] + 1 != dep[y]) continue; int f = dfs(y, min(lim, w[i]), t); w[i] -= f, w[i^1] += f; res += f, lim -= f; if (!lim) return res; } return res; } ll ans, s, t; int main() { n = read(), m = read(), ss = read(), tt = read(); for (int i = 1;i <= m; i++) { ll x = read(), y = read(), l = read(), r = read(); a[y] += l, a[x] -= l; add(x, y, r - l); add(y, x, 0); num[i] = tot; } s = n + 1, t = n + 2; for (int i = 1;i <= n; i++) { if (a[i] > 0) { sum += a[i]; add(s, i, a[i]); add(i, s, 0); } else { add(i, t, -a[i]); add(t, i, 0); } } add(tt, ss, INF); while (bfs(s, t)) flow += dfs(s, INF, t); if (sum != flow) { cout << "please go home to sleep\n"; return 0; } flow = w[tot^1]; w[tot] = w[tot^1] = 0; while (bfs(tt, ss)) { dep[n+1] = dep[n+2] = INF; flow -= dfs(tt, INF, ss); } cout << flow << endl; return 0; }
最大权闭合子图
一个有向图的闭合图是该有向图的一个点集, 且点集所有的出边所指向的点还在该点集
给每个点分配一个权值, 一个闭合图中点权和最大的叫做最大权闭合子图
由定义可知, 闭合图中可能包含不只一个连通块

可以用网络流来解决, 首先建图
i的点权为w[i]
超级源点s向所有点权为正的点连一条边权为w[i]的边, 超级汇点t向所有点权为负的点连一条边权为-w[i]的边
对于原来有向图上的每条边(u, v), 在网络图上连一条容量为INF的边(u, v), 答案是正点权和减去最小割
最大闭合子图就是与超级源点在同一连通块的点集
证明(proof)
首先, 最小割割断的只能是与源点和汇点相连的边, 因为割断无穷大显然是不优的
性质: 闭合图和简单割互相对应
以下均为绝对值:
S连接的割边分割后在T集边权为正的点的权值和S1
S连接的割边分割后在T集边权为负的点的权值和S2
T连接的割边分割后在S集边权为正的点的权值和T1
T连接的割边分割后在S集边权为负的点的权值和T2
所以最小割的权值为S1 + T2, 最大闭合子图权值为正权减去负权= T1 - T2
相加即为T1 + S1 = 正点权之和
得证
例题
最大密度子图
定义: 一个无向图(V, E) 的密度g = \(\frac{|E|}{|V|}\)
最大密度子图即最大化无向图的密度
考虑分数规划, 二分g的值
设计函数\(H(x) = max \{\sum_{a\in E}1 - \sum_{b \in V}x\}\) 若H(g) > 0 说明密度大于g, else 密度小于等于g
可以证明二分的范围是 \(\frac{|E|}{|V|}\) 到 m, 精度是\(\frac{1}{n^2}\)
反手一步转化, 每条边(u, v)的存在条件是u和v已经存在, 所以考虑最大闭合子图, 8将边化为权值为1的点, 分别向u, v连一条边权为INF的边, 点权为-g, 用上一方法即可
复杂度 \(\Theta(log_nmaxflow(n+m,n+m))\)
此算法还可以继续改进为\(\Theta(log_nmaxflow(n,n+m))\), 详见文头资料
拓展: 带点权和边权也可以哦
二分图的最小点权覆盖集和最大点权独立集
覆盖集: 所有边至少一个端点在覆盖集中
独立集: 一坨点两两之间没有连边
对于边(u, v) 连一条INF边, s向左部点连边权为点权的边, 右部点向t连边权为点权的边
对于一条简单路径s -> u -> v -> t, 至少要割断一条边
u - > v是不可能被割断的, 被割断的一定是s -> u, v -> t, 代表u, v中最多选一个, 删其边代表不选他, 那么最大点权独立集为点权和-最小割, 最小割对应最小点权覆盖集
网络流24题中的思想与解题方案
拆点思想: 一个物品/时间拆成两个点, 两点之间连边表示此物品的使用次数, 费用
分层图思想: 按时间(或其他)分层, 每次新建一个单位时间的图, 在残余网络上跑dinic
转化思想: 将问题转化为模型(最小点权覆盖集, 最大点权独立集, 最大权闭合子图)