网络流
网络流
基本概念
可以类比于下水管道,每个管道都有一定容积,源点是自来水厂,将水注入与源点相邻管道,经过多个中转站后到达家中,即汇点
性质
- 容量限制:每条边的流量不可能大于该边的容量
- 斜对称:正向边的流量 = 反向边的流量
- 流量守恒:正向的所有流量和 = 反向的所有流量和
注:代码中每条边初始均要建出流量为 0 的反向边,叙述中为了方便均省去,要注意!
最大流
算法:\(Ek\) \(O(nm^2)\) 不优,一般网络流中虽不至于稠密图,但边数不少
\(Dinic\):
先用 bfs 只走还有流量的边把图分层,然后 dfs 找增广路,每次只找下一层的,注意每次增广成功后把边权减去流量,反边加上流量
一直进行增广直到找不到增广路为止,然后重新 bfs 分层,bfs 时源点与汇点不连通时结束
优化:
-
当前弧优化:已经增广过的就不增广了,记录一下下次从下一个没增广的开始,注意这个记录重新 bfs 时清空
-
剪枝:从这个点出发无增广路,即流量为 0 时,把这个点在这次 bfs 中的层数清零,表示不用再找了,当流量为 0 或已流满时直接退出
复杂度:加优化后,\(O(n^2m)\) 或 \(O(m\sqrt{\text{最大流流量}})\)
特殊图上的复杂度
单位图上:\(O(m\min\{n^{\frac 2 3},m^{\frac 1 2}\})\)
单位图:边权一样的图,边权相差小或者都是某个数的倍数的也可近似为单位图处理
code:
#include<bits/stdc++.h>
#define reg register
using namespace std;
typedef long long ll;
const ll N = 410, M = 10010, inf = INT_MAX;
ll n, m, s, t, u, v, w, head[N], nxt[M], to[M], e[M], st[N], idx = 1, ans, sum, dep[N];
inline ll read()
{
reg char ch = getchar(); reg ll x = 0;
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x;
}
inline void add(ll u, ll v, ll w)
{
e[++idx] = w, nxt[idx] = head[u];
head[u] = st[u] = idx, to[idx] = v;
}
inline ll bfs()
{
queue<ll> q; memset(dep, 0, sizeof(dep));
q.push(s), dep[s] = 1;
while(!q.empty())
{
ll tt = q.front(); q.pop();
st[tt] = head[tt]; // 清零标记
if(tt == t) return 1; // 结束了
for(reg ll i = head[tt]; i; i = nxt[i])
if(e[i] > 0 && !dep[to[i]]) dep[to[i]] = dep[tt] + 1, q.push(to[i]);
}
return 2; // 源点汇点不连通
}
inline int dinic(int x, int flow)
{
if(x == t || flow <= 0) return flow; // 到了汇点或没流,返回流量
int res = flow, k = 0;
for(reg int i = st[x]; i; i = nxt[i])
{
st[x] = i; // 当前弧优化,记录标记,下次直接从这开始找
if(e[i] > 0 && dep[to[i]] == dep[x] + 1)
{
k = dinic(to[i], min(e[i], res));
if(k > 0) e[i] -= k, e[i ^ 1] += k, res -= k; // 流过去
else dep[to[i]] = 0; // 剪枝优化
if(res <= 0) return flow; // 加上优化!不然 T飞
}
}
return flow - res;
}
int main()
{
n = read(), m = read(), s = read(), t = read();
for(reg ll i = 1; i <= m; ++i)
{
u = read(), v = read(), w = read();
if(w) add(u, v, w), add(v, u, 0);
}
while(bfs() == 1)
while((sum = dinic(s, inf)) > 0) ans += sum;
printf("%lld", ans);
return 0;
}
最小割
定理:最大流 = 最小割
跑最大流即可
最小割输出方案
从最大流构造最小割
- 从源点出发只走不满流的边
- 如果一条边左右两端的访问情况不同,则属于割集
应用:二分图中求最大权独立集
独立集,指选出的集合中两两间没有边
此时把二分图左部连向 \(S\),右部连向 \(T\),边权均为点权,把二分图中原来有的边连上,边权为 \(+\infty\)
最小割就是不得不舍弃的权值,用总权值-最大流即为答案
费用流
全称:最小费用最大流
此时自来水厂输水收费,每条管道每单位流量有一个费用,此时输水总量(流量)不变的情况下让总费用最小
注意:先满足最大流,再满足最小费用
算法
1.Dinic + SPFA (可能中途有负权,用 SPFA)
把上面 bfs 分层改为以每条边单位费用为边权 SPFA 跑最短路,每次只转移到最短路上下一个可能走到的点
注意 Dinic 中优化为栈优化,搜到一个点标记,回溯时取消标记,有标记的就不搜了,不能剩余流量为 0 即退出,否则回溯时标记无法清零
但是,它无法分层,仍然可能被卡
code:
inline ll spfa()
{
memset(dis, 0x3f, sizeof(dis)), memset(book, 0, sizeof(book));
memcpy(st, head, sizeof(st));
queue<ll> q; q.push(s), dis[s] = 0, book[s] = 1;
while(!q.empty())
{
ll tt = q.front(); q.pop(), book[tt] = 0;
for(reg ll i = head[tt]; i; i = nxt[i])
if(e[i] > 0 && dis[to[i]] > dis[tt] + cost[i])
{
dis[to[i]] = dis[tt] + cost[i];
if(!book[to[i]]) q.push(to[i]), book[to[i]] = 1;
}
}
if(dis[t] < 3e13) return 1;
return 2;
}
inline ll dinic(ll x, ll flow)
{
if(x == t) return flow;
ll res = flow, flw = 0, k = 0;
vis[x] = 1;
for(reg ll i = st[x]; i; i = nxt[i])
{
if(e[i] > 0 && !vis[to[i]] && dis[to[i]] == dis[x] + cost[i])
{
flw = min(res, e[i]), k = dinic(to[i], flw);
if(k) e[i] -= k, e[i ^ 1] += k, res -= k, sum += cost[i] * k;
}
}
vis[x] = 0;
return flow - res;
}
int main()
{
n = read(), m = read(), s = read(), t = read();
for(reg ll i = 1; i <= m; ++i)
{
u = read(), v = read(), d = read(), w = read();
add(u, v, d, w), add(v, u, 0, -w);
}
while(spfa() == 1)
while((lsh = dinic(s, inf)) > 0) ans += lsh;
printf("%lld %lld", ans, sum);
return 0;
}
2.EK + SPFA
此时,用 EK 这个看起来不优的算法才不被卡
EK 简介一下吧:
虽然它一次只能找一条增广路,但是它用 bfs 找,沿仍有流量的边走,找不到汇点即退出
bfs 时记录从源点流向每个点的流量(即源点到它的经过边权最小值),和它的前驱边(从哪条边流到它),最后流量就是流向汇点的
但是缺点是 bfs 时无法确定到底流哪条边,于是结束后利用前驱边手动流,把反向边和正向边处理了
费用流,就把 bfs 换成 SPFA,同理跑费用的最短路
代码:
inline int spfa() // 找最小费用到 T 路径
{
memset(dis, 0x3f, sizeof(dis)), book.reset();
queue<int> q;
q.push(S), dis[S] = 0, incf[S] = inf;
while(!q.empty())
{
int t = q.front(); q.pop();
book[t] = 0;
for(reg int i = head[t]; i; i = nxt[i])
if(e[i] > 0 && dis[to[i]] > dis[t] + cost[i])
{
dis[to[i]] = dis[t] + cost[i], pre[to[i]] = i;
incf[to[i]] = min(incf[t], e[i]);
if(!book[to[i]]) q.push(to[i]), book[to[i]] = 1;
}
}
if(dis[T] < inf) return 1;
return 2;
}
int main()
{
n = read(), m = read(), S = read(), T = read();
for(reg int i = 1, u = 0, v = 0, d = 0, w = 0; i <= m; ++i)
{
u = read(), v = read(), d = read(), w = read();
add(u, v, d, w), add(v, u, 0, -w);
}
while(spfa() == 1)
{
int nw = T, i = pre[nw];
ans += incf[T], sum += dis[T] * incf[T];
while(nw != S) // 手动模拟流量减少,EK 中不像 dinic 在 dfs时即减少流量
{
e[i] -= incf[T], e[i ^ 1] += incf[T];
nw = to[i ^ 1], i = pre[nw];
}
}
printf("%d %d", ans, sum);
return 0;
}
无源汇上下界可行流
对于上下界网络流的处理,通过新建附加源汇来保证流量平衡
可以想到转化:刚开始每条边的流量定为下界减去上界,跑最大流
但会出现问题:每个点初始有流量下界,先流满下界,每个点应该流入的不等于此时应该流出的,导致流量不平衡
所以怎么办?
流满下界后,计算每个点的流入与流出之差 \(d_i\),由于源汇点可不满足流量平衡,所以可以添加一些辅助边使流量平衡
-
流入大于流出,即 \(d_i>0\),这个点理应流入也只等于流出,源点连向它补齐流量平衡造成的流入损失,流量为 \(d_i\)
-
流入小于流出,即 \(d_i<0\),同理,它连向汇点,流量为 \(-d_i\)
从附加源跑最大流到附加汇
-
如果流量不等于源点应该流出(或汇点应该流入)的流量,即流量还是不平衡,无可行流
-
反之,有可行流,此时的流量就是一组解
有源汇上下界流
此时相比无源汇的情况,多了 2 个可以流量不平衡的点
那就让原图中的汇点向源点连一条流量为 \(+\infty\) 的边
由于源点多流出的流量 \(=\) 汇点多流入的流量,这样源点多流出的流量、汇点多流入的流量就可以通过此边平衡
先按无源汇点的跑一边最大流,设此时 \(T\to S\) 的边流量为 \(ans1\)
可行流
判断标准与无源汇时相同
最小流
把原图中汇点流向源点的边删掉(防死循环),再在残量网络上从原图汇点向原图原点跑最大流,流量为 \(ans2\)
\(ans=ans1-ans2\)
感性理解:这是跑出可行解后还能向下浮动的最大流量
最大流
同最小流,只不过此时删边后从原图源点向原图汇点跑最大流,可浮动的流量要向上浮动,\(ans=ans1+ans2\)
code:(P5192 【模板】有源汇上下界最大流)
这题建模:还比较直接,以少女和天数为点,互相据题意连边即可
#include<bits/stdc++.h>
#define reg register
#define clear(a) memset(a,0,sizeof(a))
using namespace std;
const int N = 1000500, M = 2500010, inf = 1e9;
int n, m, g, c, day, id, l, r, e[M], to[M], nxt[M], head[N], st[N], idx, dep[N], d[N], s1, t1, s, t, lsh, ans, sum;
inline void add(int u, int v, int w)
{
e[++idx] = w, nxt[idx] = head[u];
to[idx] = v, head[u] = idx;
}
inline int bfs()
{
memset(dep, 0, sizeof(dep));
queue<int> q; q.push(s), dep[s] = 1;
while(!q.empty())
{
int tt = q.front(); q.pop();
st[tt] = head[tt];
if(tt == t) return 1;
for(reg int i = head[tt]; i; i = nxt[i])
if(e[i] > 0 && !dep[to[i]]) q.push(to[i]), dep[to[i]] = dep[tt] + 1;
}
return 2;
}
inline int dinic(int x, int flow)
{
if(x == t || flow <= 0) return flow;
int res = flow, k = 0;
for(reg int i = st[x]; i; i = nxt[i])
{
st[x] = i;
if(e[i] > 0 && dep[to[i]] == dep[x] + 1)
{
k = dinic(to[i], min(e[i], res));
if(k > 0) e[i] -= k, e[i ^ 1] += k, res -= k;
else dep[to[i]] = 0;
if(res <= 0) return flow;
}
}
return flow - res;
}
int main()
{
while(~scanf("%d%d", &n, &m))
{
clear(head), clear(d), ans = sum = 0;
s1 = n + m + 1, t1 = n + m + 2, s = n + m + 3, t = n + m + 4, idx = 1;
for(reg int i = 1; i <= m; ++i)
{
scanf("%d", &g), d[i + n] -= g, d[t1] += g;
add(i + n, t1, inf - g), add(t1, i + n, 0);
}
for(reg int i = 1; i <= n; ++i)
{
scanf("%d%d", &c, &day);
add(s1, i, day), add(i, s1, 0);
for(reg int j = 1; j <= c; ++j)
{
scanf("%d%d%d", &id, &l, &r), ++id;
add(i, id + n, r - l), add(id + n, i, 0), d[id + n] += l, d[i] -= l;
}
}
for(reg int i = 1; i <= n + m + 2; ++i)
if(d[i] > 0) add(s, i, d[i]), add(i, s, 0), sum += d[i];
else if(d[i] < 0) add(i, t, -d[i]), add(t, i, 0);
add(t1, s1, inf), add(s1, t1, 0);
while(bfs() == 1)
while((lsh = dinic(s, inf)) > 0) ans += lsh; // 错的!
if(ans != sum)
{
puts("-1\n");
continue;
}
e[idx] = e[idx ^ 1] = 0, s = s1, t = t1;
while(bfs() == 1)
while((lsh = dinic(s, inf)) > 0) ans += lsh;
printf("%d\n\n", ans);
}
return 0;
}
update on 2024.5.6:
之前的是错的!\(ans\) 一定要取 \(T\to S\) 边的流量,由于此时流量平衡这个流量就是一组可行解
而第一次 dinic
求出的和只是调整的流量!
但为什么这个错的能过 LG 模板啊……
费用流
注:这里的最小费用流指的是满足上下界要求中的流的最小费用,无需满足最大流
与普通有源汇上下界流相同,不过只用从辅助源汇点出发跑一遍最小费用最大流,答案注意加上预先流满的下界产生的费用
code:P4043 [AHOI2014/JSOI2014]支线剧情
#include<bits/stdc++.h>
#define reg register
using namespace std;
const int N = 410, M = 200010, inf = 5e8;
int n, p[N], v, w, s, t, e[M], to[M], nxt[M], head[N], cost[M], book[N], idx = 1, dep[N], st[N], d[N], ans, lsh, sum, vis[N];
inline int read()
{
reg char ch = getchar(); reg int x = 0;
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x;
}
inline void add(int u, int v, int w, int val)
{
e[++idx] = w, cost[idx] = val;
to[idx] = v, nxt[idx] = head[u], head[u] = idx;
}
inline int spfa()
{
memset(dep, 0x3f, sizeof(dep)), memset(vis, 0, sizeof(vis));
queue<int> q; q.push(s), dep[s] = 1, vis[s] = 1;
while(!q.empty())
{
int tt = q.front(); q.pop();
st[tt] = head[tt], vis[tt] = 0;
for(int i = head[tt]; i; i = nxt[i])
if(e[i] > 0 && dep[to[i]] > dep[tt] + cost[i])
{
dep[to[i]] = dep[tt] + cost[i];
if(!vis[to[i]]) vis[to[i]] = 1, q.push(to[i]);
}
}
if(dep[t] < inf) return 1;
return 2;
}
inline int dinic(int x, int flow)
{
if(x == t || flow <= 0) return flow;
int res = flow, k = 0;
book[x] = 1; // 注意栈优化
for(reg int i = st[x]; i; i = nxt[i])
{
st[x] = i;
if(e[i] > 0 && !book[to[i]] && dep[to[i]] == dep[x] + cost[i])
{
k = dinic(to[i], min(res, e[i]));
if(k > 0) e[i] -= k, e[i ^ 1] += k, res -= k, sum += k * cost[i];
}
}
book[x] = 0;
return flow - res;
}
int main()
{
n = read(), s = n + 2, t = n + 3; // 辅助源汇点,真实源点:1,真实汇点:n+1
for(reg int i = 1; i <= n; ++i)
{
p[i] = read();
for(reg int j = 1; j <= p[i]; ++j)
{
v = read(), w = read(), ++d[v], --d[i], sum += w;
add(i, v, inf - 1, w), add(v, i, 0, -w);
}
} // 注意这里可以随时退出游戏,所有点都可以连向真实汇点
for(reg int i = 1; i <= n; ++i) add(i, n + 1, inf, 0), add(n + 1, i, 0, 0);
for(reg int i = 1; i <= n; ++i)
if(d[i] > 0) add(s, i, d[i], 0), add(i, s, 0, 0);
else if(d[i] < 0) add(i, t, -d[i], 0), add(t, i, 0, 0);
add(n + 1, 1, inf, 0), add(1, n + 1, 0, 0);
while(spfa() == 1)
while((lsh = dinic(s, inf)) > 0) ans += lsh;
printf("%d", sum);
return 0;
}
例题与常见套路
1.P4313 文理分科
看到这种二选一的题,想到网络流
把每个人看作一个点,连 \(S\) 的边为 \(sci_i\),连 \(T\) 的边为 \(art_i\)
那相邻相同产生的贡献?新建一个点,连向 \(S/T\),边权为贡献
把这个点和对应的人和它周围的人连边权为 \(+\infty\) 的边(保证不被割掉)
这样跑最小割,发现文、理中肯定要去掉一个,如果相邻几个人割选文科的边,即选理科,那它们连接的都选文科的贡献点就必须割掉,无法产生贡献,而都选理科的点不必割掉,反之同理
如果相邻的人选理科、文科的都有,那两边的相同的贡献点都要被割掉
发现这样是符合题意的
code:
#include<bits/stdc++.h>
#define reg register
#define pb push_back
using namespace std;
const int N = 182010, M = 1220010, inf = 2e9;
int n, m, s, t, art, sci, sa, sc, num, sum, id[210][210], ans = 0, idx = 1, st[N], dep[N], lsh;
int nex[6] = {0, 0, 0, 1, -1}, ney[6] = {0, 1, -1, 0, 0}, e[M];
vector<int> edge[N], eid[N];
inline int read()
{
reg char ch = getchar(); reg int x = 0;
while(ch < '0' || ch > '9') ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x;
}
inline void add(int u, int v, int w)
{
edge[u].pb(v), eid[u].pb(++idx), e[idx] = w;
}
inline int bfs()
{
memset(dep, 0, sizeof(dep));
queue<int> q; q.push(s), dep[s] = 1;
while(!q.empty())
{
int tt = q.front(); q.pop();
st[tt] = 0;
if(tt == t) return 1;
for(reg int i = 0; i < (int)edge[tt].size(); ++i)
if(e[eid[tt][i]] > 0 && !dep[edge[tt][i]]) dep[edge[tt][i]] = dep[tt] + 1, q.push(edge[tt][i]);
}
return 2;
}
inline int dinic(int x, int flow)
{
if(x == t || flow == 0) return flow;
int res = flow, k = 0;
for(reg int i = st[x]; i < (int)edge[x].size(); ++i)
{
st[x] = i;
if(e[eid[x][i]] > 0 && dep[edge[x][i]] == dep[x] + 1)
{
k = dinic(edge[x][i], min(res, e[eid[x][i]]));
if(k > 0) e[eid[x][i]] -= k, e[eid[x][i] ^ 1] += k, res -= k;
else dep[edge[x][i]] = 0;
}
}
return flow - res;
}
int main()
{
n = read(), m = read(), s = n * m + 1, t = num = n * m + 2;
for(reg int i = 1; i <= n; ++i)
for(reg int j = 1; j <= m; ++j)
{
art = read(), id[i][j] = (i - 1) * m + j, sum += art;
add(id[i][j], t, art), add(t, id[i][j], 0);
}
for(reg int i = 1; i <= n; ++i)
for(reg int j = 1; j <= m; ++j)
{
sci = read(), sum += sci;
add(id[i][j], s, 0), add(s, id[i][j], sci);
}
for(reg int i = 1; i <= n; ++i)
for(reg int j = 1; j <= m; ++j)
{
sa = read(), sum += sa, ++num, add(num, t, sa), add(t, num, 0);
for(reg int k = 0; k <= 4; ++k)
{
int nx = i + nex[k], ny = j + ney[k];
if(nx < 1 || ny < 1 || nx > n || ny > m) continue;
add(id[nx][ny], num, inf), add(num, id[nx][ny], 0);
}
}
for(reg int i = 1; i <= n; ++i)
for(reg int j = 1; j <= m; ++j)
{
sa = read(), sum += sa, ++num, add(num, s, 0), add(s, num, sa);
for(reg int k = 0; k <= 4; ++k)
{
int nx = i + nex[k], ny = j + ney[k];
if(nx < 1 || ny < 1 || nx > n || ny > m) continue;
add(id[nx][ny], num, 0), add(num, id[nx][ny], inf);
}
}
while(bfs() == 1)
while((lsh = dinic(s, inf)) > 0) ans += lsh;
printf("%d", sum - ans);
return 0;
}
2. P3227 [HNOI2013]切糕
最小割建模应该不用解释什么,\(P\times Q\) 行,每行 \(R+1\) 个点
这里 \((P,Q,R)\) 代表平面内 \((P,Q)\) 对应行的第 \(R\) 个点
红色边为关键,处理 \(D\) 的限制,\((P,Q,R)\) 向 \((P,Q-1,R+D)\) 和 \((P-1,Q,R+D)\) 连边,反着同理
这里一行只会被割一条边,红边不能被割
注意:如果是 \(\ge\),则有些限制越界了,代表不能选,此时如果不连限制的边会有问题
方法1:将越界的直接连向应连的那排的最后一个点,最后点连向 \(T\) 流量为 \(\infty\),无法被选,那个点也选不了
方法2:连反向边,\((u,j)\to(u,j+1)\),连 \((u,j+1)\to(u,j)\) 流量为 \(\infty\) 的边,也是为了补全限制,这样只能选一行的一个前缀与 \(S\) 相连
这个模型用来处理形如 \(x_i-x_j\le/\ge k\) 的限制
建图的 code
inline int getid(int a, int b, int h) {return ((a - 1) * q + b - 1) * (r + 1) + h;}
s = p * q * (r + 1) + 1, t = p * q * (r + 1) + 2;
for(reg int i = 1; i <= p; ++i)
for(reg int j = 1; j <= q; ++j)
{ // 先建好黑色边
add(s, getid(i, j, 1), inf), add(getid(i, j, 1), s, 0);
add(getid(i, j, r + 1), t, inf), add(t, getid(i, j, r + 1), 0);
for(reg int k = 1; k <= r; ++k)
add(getid(i, j, k), getid(i, j, k + 1), v[i][j][k]), add(getid(i, j, k + 1), getid(i, j, k), 0);
for(reg int k = 1; k <= r - d; ++k)
{ // 建红色边
if(i - 1)
{
add(getid(i - 1, j, k + d), getid(i, j, k), inf), add(getid(i, j, k), getid(i - 1, j, k + d), 0);
add(getid(i, j, k + d), getid(i - 1, j, k), inf), add(getid(i - 1, j, k), getid(i, j, k + d), 0);
}
if(j - 1)
{
add(getid(i, j - 1, k + d), getid(i, j, k), inf), add(getid(i, j, k), getid(i, j - 1, k + d), 0);
add(getid(i, j, k + d), getid(i, j - 1, k), inf), add(getid(i, j - 1, k), getid(i, j, k + d), 0);
}
}
}