网络流
网络流
最大流
基本概念
-
流网络
不考虑反向边 -
可行流
不考虑反向边
(1)两个条件:容量限制、流量守恒
(2)可行流的流量指从源点流出的流量 - 流入源点的流量
(3)最大流是指最大可行流 -
残留网络
考虑反向边,残留网络的可行流f' + 原图的可行流f = 原题的另一个可行流
(1) \(|f' + f| = |f'| + |f|\)
(2) \(|f'|\) 可能是负数 -
增广路径
-
割
(1)割的定义
(2)割的容量,不考虑反向边,“最小割”是指容量最小的割。
(3)割的流量,考虑反向边,\(f(S, T) <= c(S, T)\)
(4)对于任意可行流\(f\),任意割\([S, T],|f| = f(S, T)\)
(5)对于任意可行流\(f\),任意割\([S, T],|f| <= c(S, T)\) -
最大流最小割定理
(1)可行流f是最大流
(2)可行流f的残留网络中不存在增广路 -
二分图的最大独立集
二分图的最大独立集 \(=\) 点数 \(-\) 匹配数
算法
1、Edmonds-Karp 动能算法 \(O(VE^2)\)
每次对一张网络流图,每次在残量网络中找到一条流量不为 0 的路径,对其进行增广。
由于其效率过于低下,在 OI 中几乎用不到。
EK算法
const int N = 1010, M = 20010, INF = 1e8;
int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int d[N], q[N], pre[M];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
bool bfs()
{
int hh = 0, tt = 0;
memset(st, 0 ,sizeof st);
q[0] = S, st[S] = 1, d[S] = INF;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (!st[ver] && f[i])
{
st[ver] = 1;
d[ver] = min(d[t], f[i]);
pre[ver] = i;
if (ver == T) return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
int EK()
{
int r = 0;
while (bfs())
{
r += d[T];
for (int i = T; i != S; i = e[pre[i] ^ 1])
f[pre[i]] -= d[T], f[pre[i] ^ 1] += d[T];
}
return r;
}
·
2、Dinic \(O(V^2E)\)
在 EK 算法基础上进行了改进,优化了寻找路径的方式,同时每次对多条路径增广。
具体来说:Dinic 算法每次增广前会先跑一遍 bfs,对残量网络按到 S 距离分层,只
寻找满足 \(d_v = d_u + 1\) 的路径。
同时我们每次不再只增广一条路,而是对所有满足条件的路径同时增广。
当前弧优化:在每次增广时对每个点维护一个指针,它指向最靠前的满足增广条件
的边 \((d_v = d_u + 1\) 且 \(f_{u,v} > 0)\)。
由于 Dinic 好写,并且速度较快,于是它是 OI 中最常使用的最大流算法。
Dinic
const int N = 10010, M = 2e5 + 10, INF = 1e8;
int n, m, S, T;
int h[N], e[M], f[M], ne[M], idx;
int q[N], d[N], cur[N];
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
bool bfs()
{
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if (ver == T) return true;
q[ ++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit)
{
if (u == T) return limit;
int flow = 0;
for (int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i;
int ver = e[i];
if (d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if (!t) d[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic()
{
int r = 0, flow;
while (bfs())
while (flow = find(S, INF)) r += flow;
return r;
}
·
3、*最高标号预流推进算法 HLPP \(O(n^2\sqrt{m})\)
Highest Label Preflow Push
const int N = 1300, M = 241000, INF = 0x3f3f3f3f;
int n, m, S, T;
int h[N], e[M], ne[M], f[M], idx;
int ht[N], ex[N], gap[N], q[N];
stack<int> B[N];
int level;
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++ ;
}
bool bfs()
{
memset(ht, 0x3f, sizeof ht);
int hh = 0, tt = 0;
q[0] = T, ht[T] = 0;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if (f[i ^ 1] && ht[ver] > ht[t] + 1) ht[ver] = ht[t] + 1, q[ ++ tt] = ver;
}
}
return ht[S] != INF;
}
int push(int u)
{
bool init = u == S;
for (int i = h[u]; ~i; i = ne[i])
{
int ver = e[i], w = f[i];
if (!w || !init && ht[u] != ht[ver] + 1) continue;
int k = init ? w : min(w, ex[u]);
if (ver != S && ver != T && !ex[ver])B[ht[ver]].push(ver), level = max(level, ht[ver]);
ex[u] -= k, ex[ver] += k, f[i] -= k, f[i ^ 1] += k;
if (!ex[u]) return 0;
}
return 1;
}
void relabel(int u)
{
ht[u] = INF;
for (int i = h[u]; ~i; i = ne[i])
if (f[i]) ht[u] = min(ht[u], ht[e[i]]);
if ( ++ ht[u] < n);
B[ht[u]].push(u);
level = max(level, ht[u]);
gap[ht[u]] ++ ;
}
int select()
{
while (B[level].size() == 0 && level > -1) level -- ;
return level == -1 ? 0 : B[level]. top();
}
int HLPP()
{
if (!bfs()) return 0;
memset(gap, 0, sizeof gap);
for (int i = 1; i <= n; i ++ )
if (ht[i] != INF) gap[ht[i]] ++ ;
ht[S] = n;
push(S);
int u;
while (u = select())
{
B[level].pop();
if (push(u))
{
if (!--gap[ht[u]])
for (int i = 1; i <= n; i ++ )
if (i != S && i != T && ht[i] > ht[u] && ht[i] < n + 1)
ht[i] = n + 1;
relabel(u);
}
}
return ex[T];
}
应用
1、二分图
(1)二分图匹配
飞行员配对方案问题
[TJOI2011] 卡片
2、上下界网络流
(1)无源汇上下界可行流(上下界循环流)
上下界循环流:在网络流基础上,每条边多给一个下界,表示至少流这么多。
先强制每条边流下界这么多。建立超级源汇点 \(S, t\),令 \(d_u\) 为 \(u\) 的流出量-流入量,
如果 \(d_u < 0\),那么连 \((S, u, d_u)\),否则 \(d_u < 0\) 连 \((u, T, −d_u)\)。
最后跑一边最大流,检查一下是否满流。
正确性:这种做法保证了流量平衡,并且如果有解一定可以按如上方式构造。
code
const int N = 210, M = (10200 + N) * 2, INF = 1e8;
int n, m, S, T;
int h[N], e[M], ne[M], f[M], l[M], idx;
int q[N], d[N], cur[N], A[N];
void add(int a, int b, int c, int d)
{
e[idx] = b, ne[idx] = h[a], f[idx] = d - c, l[idx] = c, h[a] = idx ++;
e[idx] = a, ne[idx] = h[b], f[idx] = 0, h[b] = idx ++;
}
bool bfs()
{
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
int hh = 0, tt = 0;
while(hh <= tt)
{
int t = q[hh ++];
for(int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if(d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if(ver == T) return true;
q[++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit)
{
if(u == T) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i;
int ver = e[i];
if(d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if(!t) d[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic()
{
int res = 0, flow;
while(bfs())
while(flow = find(S, INF))
res += flow;
return res;
}
int main()
{
scanf("%d%d", &n, &m);
S = 0, T = n + 1;
memset(h, -1, sizeof h);
for(int i = 1, a, b, c, d; i <= m; i ++)
{
scanf("%d%d%d%d", &a, &b, &c, &d);
add(a, b, c, d);
A[a] -=c, A[b] += c;
}
int tot = 0;
for(int i = 1; i <= n; i ++)
if(A[i] > 0) add(S, i, 0, A[i]), tot += A[i];
else if(A[i] < 0) add(i, T, 0, -A[i]);
if(dinic() != tot)puts("NO");
else
{
puts("YES");
for(int i = 0; i < m * 2; i += 2)
printf("%d\n", f[i ^ 1] + l[i]);
}
return 0;
}
·
(2)有源汇上下界最大流
连接 \((t, s, ∞)\),先跑一遍上下界循环流。然后再把 \((t, s)\) 边断掉,再跑一遍 \(s\) 到 \(t\) 的
最大流。
大致证明:首先我们求出了一组合法的流,然后在这组流的基础上增广。容易发现一定可以增广出最大流。
code
const int N = 210, M = (N + 10000) * 2, INF = 1e8;
int n, m, s, t, S, T;
int h[N], e[M], ne[M], f[M], idx;
int q[N], d[N], cur[N], A[N];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], f[idx] = c ,h[a] = idx ++;
e[idx] = a, ne[idx] = h[b], f[idx] = 0, h[b] = idx ++;
}
bool bfs()
{
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
int hh = 0, tt = 0;
while(hh <= tt)
{
int t = q[hh ++];
for(int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if(d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if(ver == T) return true;
q[++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit)
{
if(u == T) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = ne[i])
{
int ver = e[i];
if(d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if(!t) d[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic()
{
int res = 0, flow;
while(bfs())
while(flow = find(S, INF))
res += flow;
return res;
}
int main()
{
scanf("%d%d%d%d", &n, &m, &s, &t);
S = 0, T = n + 1;
memset(h, -1, sizeof h);
while(m --)
{
int a, b, c, d;
scanf("%d%d%d%d", &a, &b, &c, &d);
add(a, b, d - c);
A[a] -=c, A[b] += c;
}
int tot = 0;
for(int i = 1; i <= n; i ++)
if(A[i] > 0) add(S, i, A[i]), tot += A[i];
else if(A[i] < 0) add(i, T, -A[i]);
add(t, s, INF);
if(dinic() < tot)puts("No Solution");
else
{
int res = f[idx - 1];
S = s, T = t;
f[idx - 1] = f[idx - 2] = 0;
printf("%d\n", res + dinic());
}
return 0;
}
·
(3)有源汇上下界最小流
连接 \((t, s, ∞)\),先跑一遍上下界循环流。然后再把 \((t, s)\) 边断掉,再跑一遍 \(t\) 到 \(s\) 的
最大流。
大致证明:首先我们求出了一组合法的流,然后在这组流的基础上退流。容易发现一定可以退出最小流。
code
const int N = 500010, M = (N + 125010) * 2, INF = 2147483647;
int n, m, s, t, S, T;
int h[N], e[M], ne[M], f[M], idx;
int q[N], d[N], cur[N], A[N];
void add(int a, int b, int c)
{
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++;
}
bool bfs()
{
memset(d, -1, sizeof d);
q[0] = S, d[S] = 0, cur[S] = h[S];
int hh = 0, tt = 0;
while(hh <= tt)
{
int t = q[hh ++];
for(int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if(d[ver] == -1 && f[i])
{
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if(ver == T) return true;
q[++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit)
{
if(u == T) return limit;
int flow = 0;
for(int i = cur[u]; ~i && flow < limit; i = ne[i])
{
cur[u] = i;
int ver = e[i];
if(d[ver] == d[u] + 1 && f[i])
{
int t = find(ver, min(f[i], limit - flow));
if(!t) cur[ver] = -1;
f[i] -= t, f[i ^ 1] += t, flow += t;
}
}
return flow;
}
int dinic()
{
int res = 0, flow;
while(bfs())
while(flow = find(S, INF))
res += flow;
return res;
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d%d%d%d", &n, &m ,&s, &t);
S = 0, T = n + 1;
for(int i = 1, a, b, c, d; i <= m; i ++)
{
scanf("%d%d%d%d", &a, &b, &c, &d);
add(a, b, d - c);
A[a] -= c, A[b] += c;
}
int tot = 0;
for(int i = 1; i <= n; i ++)
if(A[i] > 0) add(S, i, A[i]), tot += A[i];
else if(A[i] < 0) add(i, T, -A[i]);
add(t, s, INF);
if(dinic() < tot) puts("No Solution");
else
{
int res = f[idx - 1];
S = t, T = s;
f[idx - 1] = f[idx - 2] = 0;
printf("%d\n", res - dinic());
}
return 0;
}
·
4、最小路径覆盖
给定一张 DAG,要求用最少的路径覆盖整张图。数据范围:n, m ≤ 100。
将每个点拆为两个点:入点和出点。
如果原图存在一条边 \((u, v)\),那么连一条从 \(u\) 的出点向 \(v\) 的入点的流量为 \(1\) 的边,
并且从 \(s\) 向所有出点连流量为 \(1\) 的边,从所有入点向 \(t\) 连流量为 \(1\) 的边。
\(n − flow\) 就是答案。
5、最大权闭合子图
给定一个有向图,点有点权。如果一个点 \(u\) 被选了,所有 \(u\) 的出边指向的点 \(v\) 也必须选。求最大收益。(点权可以为负数)
利用最小割来解决。先假设所有正点权都选。正点权连到 \(S\),表示放弃这个点,负点权连到 \(T\),表示选择这个点。原图中所有 \((u, v)\) 连接一条 \((u, v, ∞)\) 的边。
[CEOI2008] order
把原来的 \(inf\) 边改成租用的代价,来表示可以付出一定代价不选后面的点即可。
费用流
SPFA求最小费用最大流
code
const int N = 5010, M = 100010, INF = 1e8;
int n, m, S, T;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];
void add(int a, int b, int c, int d)
{
e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx ++;
e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx ++;
}
bool spfa()
{
int hh = 0, tt = 1;
memset(d, 0x3f, sizeof d);
memset(incf, 0, sizeof incf);
q[0] = S, d[S] = 0, incf[S] = INF;
while(hh != tt)
{
int t = q[hh ++];
if(hh == N) hh = 0;
st[t] = false;
for(int i = h[t]; ~i; i = ne[i])
{
int ver = e[i];
if(f[i] && d[ver] > d[t] + w[i])
{
d[ver] = d[t] + w[i];
pre[ver] = i;
incf[ver] = min(f[i], incf[t]);
if(!st[ver])
{
q[tt ++] = ver;
if(tt == N) tt = 0;
st[ver] = true;
}
}
}
}
return incf[T] > 0;
}
void EK(int &flow, int &cost)
{
flow = cost = 0;
while(spfa())
{
int t = incf[T];
flow += t, cost += t * d[T];
for(int i = T; i != S; i = e[pre[i] ^ 1])
{
f[pre[i]] -= t;
f[pre[i] ^ 1] += t;
}
}
}
·

浙公网安备 33010602011771号