网络流
随缘更新中
网络流基本概念
- 基本概念
1.1 流网络,不考虑反向边
1.2 可行流,不考虑反向边
1.2.1 两个条件:容量限制、流量守恒
1.2.2 可行流的流量指从源点流出的流量 − - − 流入源点的流量
1.2.3 最大流是指最大可行流
1.3 残留网络,考虑反向边,残留网络的可行流 f ′ f' f′ + + + 原图的可行流 f f f = 原题的另一个可行流
(1) ∣ f ′ + f ∣ = ∣ f ′ ∣ + ∣ f ∣ |f' + f| = |f'| + |f| ∣f′+f∣=∣f′∣+∣f∣
(2) ∣ f ′ ∣ |f'| ∣f′∣ 可能是负数
1.4 增广路径
1.5 割
1.5.1 割的定义
1.5.2 割的容量,不考虑反向边,“最小割”是指容量最小的割。
1.5.3 割的流量,考虑反向边, f ( S , T ) < = c ( S , T ) f(S, T) <= c(S, T) f(S,T)<=c(S,T)
1.5.4 对于任意可行流f,任意割 [ S , T ] , ∣ f ∣ = f ( S , T ) [S, T],|f| = f(S, T) [S,T],∣f∣=f(S,T)
1.5.5 对于任意可行流f,任意割 [ S , T ] [S, T] [S,T], ∣ f ∣ < = c ( S , T ) |f| <= c(S, T) ∣f∣<=c(S,T)
1.5.6 最大流最小割定理
(1) 可以流f是最大流
(2) 可行流f的残留网络中不存在增广路
(3) 存在某个割 [ S , T ] [S, T] [S,T], ∣ f ∣ = c ( S , T ) |f| = c(S, T) ∣f∣=c(S,T)
1.6. 算法
1.6.1 E K O ( n m 2 ) EK O(nm^2) EKO(nm2)
1.6.2 D i n i c O ( n 2 m ) Dinic O(n^2m) DinicO(n2m)
1.7 应用
1.7.1 二分图
(1) 二分图匹配
(2) 二分图多重匹配
1.7.2 上下界网络流
(1) 无源汇上下界可行流
(2) 有源汇上下界最大流
(3) 有源汇上下界最小流
1.7.3 多源汇最大流
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/403258/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网络流算法模板
E d m o n d s − K a r p Edmonds-Karp Edmonds−Karp 增广路算法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e4 + 10, 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;
}
int main()
{
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
while(m --)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
printf("%d\n", dinic());
return 0;
}
D i n i c Dinic Dinic 算法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e4 + 10, 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;
}
int main()
{
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
while(m --)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
printf("%d\n", dinic());
return 0;
}
对算法的一点小理解
一 些 解 释 : 一些解释: 一些解释:
- 在 i n t int int t = f i n d ( v e r , m i n ( f [ i ] , l i m i t − f l o w ) ) ; t = find(ver, min(f[i], limit - flow)); t=find(ver,min(f[i],limit−flow)); 中,是在求当前节点继续往后走的时候的限制的 l i m i t limit limit 比如当该节点第一次向后遍历时, f l o w flow flow = = = 0 0 0 时,此时流量的限制应该为上一条边的容量 f [ i ] f[i] f[i] , 如果是第二次向后遍历,根据流量守恒定律,此时流量的限制可能会是总限制减去已经流出的流量,即: l i m i t limit limit − - − f l o w flow flow
二分图解决最大匹配问题
在解决二分图问题时,一般使用匈牙利算法(牛头人)来进行解决,该算法中记录的
m
a
t
c
h
[
a
]
[
b
]
match[a][b]
match[a][b]数组,即记录该点所匹配的点和网络流中所建的残留网络是同理的。
在使用最大流解决二分图问题时:
- 用 源点 向非匹配点连接一条容量为 1 1 1 的边,再从匹配点向 汇点 连接一条容量为1的边。
- 这时候如果匹配点每流出的流量 + 1 +1 +1 时,说明建立的匹配边也 + 1 +1 +1 。
- 所以我们可以得出 最 大 匹 配 数 = = 最 大 流 最大匹配数 == 最大流 最大匹配数==最大流
例
题
:
A
c
W
i
n
g
:
2175.
飞
行
员
配
对
方
案
例题:AcWing:2175.飞行员配对方案
例题:AcWing:2175.飞行员配对方案
第二次世界大战时期,英国皇家空军从沦陷国征募了大量外籍飞行员。
由皇家空军派出的每一架飞机都需要配备在航行技能和语言上能互相配合的 2 名飞行员,其中 1 1 1 名是英国飞行员,另 1 1 1 名是外籍飞行员。
在众多的飞行员中,每一名外籍飞行员都可以与其他若干名英国飞行员很好地配合。
如何选择配对飞行的飞行员才能使一次派出最多的飞机。
对于给定的外籍飞行员与英国飞行员的配合情况,试设计一个算法找出最佳飞行员配对方案,使皇家空军一次能派出最多的飞机。
输入格式
第
1
1
1 行有
2
2
2 个正整数
m
m
m 和
n
n
n。
m
m
m 是外籍飞行员数;
n
n
n 是皇家空军的飞行员总数。
外籍飞行员编号为 1 ∼ m 1∼m 1∼m;英国飞行员编号为 m + 1 ∼ n m+1∼n m+1∼n。
接下来每行有 2 2 2 个正整数 i i i 和 j j j,表示外籍飞行员 i i i 可以和英国飞行员 j j j 配合。
文件最后以 2 2 2 个 − 1 −1 −1 结束。
输出格式
第
1
1
1 行是最佳飞行员配对方案一次能派出的最多的飞机数
M
M
M。
接下来 M 行是最佳飞行员配对方案。
每行有 2 2 2 个正整数 i i i 和 j j j,表示在最佳飞行员配对方案中,外籍飞行员 i i i 和英国飞行员 j 配对。
如果有多种配对方案,则输出任意一种即可,方案内部配对输出顺序随意。
数
据
范
围
数据范围
数据范围
1
<
m
<
n
<
100
1<m<n<100
1<m<n<100
输
入
样
例
:
输入样例:
输入样例:
5 10
1 7
1 8
2 6
2 9
2 10
3 7
3 8
4 7
4 8
5 10
-1 -1
输 出 样 例 : 输出样例: 输出样例:
4
1 7
2 9
3 8
5 10
思 路 : 思路: 思路:
- 根据上述思想求出最大流得出最大匹配.
- 输出方案时,我们只需要输出匹配点和对应的非匹配点,利用建立的残余网络即可
- 如果残余网络的流量不为 0 0 0,则说明已经被匹配
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110, M = 1e5 + 10, INF = 1e8;
int n, m, S, T;
int e[M], ne[M], h[N], f[M], idx;
int q[N], d[N], cur[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()
{
int tt = 0, hh = 0;
memset(d, -1 ,sizeof h);
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])
{
cur[ver] = h[ver];
d[ver] = d[t] + 1;
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];
cur[u] = 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;
}
int main()
{
cin >> m >> n;
S = 0, T = n + 1;
memset(h, -1, sizeof h);
for(int i = 1; i <= m; i ++) add(S, i, 1);
for(int i = m + 1; i <= n; i ++) add(i, T, 1);
int a, b;
while(cin >> a >> b, a != -1) add(a, b, 1);
cout << dinic() << endl;
for(int i = 0; i < idx; i += 2)
if(e[i] > m && e[i] <= n && !f[i])
cout << e[i ^ 1] << " " << e[i] << endl;
return 0;
}
无源汇上下界可行流问题
- 在这种题中,我们使得新图的流容量为 上界 - 下界
- 进行完这一操作后,我们会发现对于一个点,如果入边数量 ≠ \ne = 出边数量 由于下界的影响,会使得 流量不守恒,所以我们需要加入新边使得每个点的流量守恒。
- 流量守恒后我们需要考虑该图的最大匹配数是否能够使得所有新边都流满,宏观来看,我们对源点添加的边和汇点添加的边的边的容量是相同的,显然在求最大流的时候会使得新边都流满,因而通过这种算法可以得到该问题的可行方案。
- 最后我们只需要再将减去的下界加上即可得到可行方案。
代 码 : 代码: 代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, M = 1e5 + 10, INF = 1e8;
int n, m, S, T;
int A[N];
int e[M], ne[M], f[M], l[M], h[N], idx;
int d[N], cur[N], q[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()
{
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;
}
int main()
{
cin >> n >> m;
S = 0, T = n + 1;
memset(h, -1 ,sizeof h);
for(int i = 1; i <= m; i ++)
{
int a, b, c, d;
cin >> 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(tot != dinic()) puts("NO");
else
{
puts("YES");
for(int i = 0; i < m * 2; i += 2)
cout << f[i ^ 1] + l[i] << endl;
}
return 0;
}
有源汇上下界最大流问题
建立新的源点和汇点 S S S, T T T, 增加一条从原来的 t t t 到 s s s 容量为 I N F INF INF 的边, 在新图上求一遍从 S S S 到 T T T 的最大流 r e s res res, 再删除掉新加的边和新建的源点和汇点,在删除后的图上求一遍网络的最大流,最后两个值相加就是要求的最大流 r e s + d i n i c ( ) res + dinic() res+dinic()。
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, M = 3e4 + 10, INF = 1e8;
int n, m, s, t, S, T;
int e[M], ne[M], f[M], h[N], idx;
int d[N], q[N], cur[N];
int 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()
{
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])
{
cur[ver] = h[ver];
d[ver] = d[t] + 1;
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];
cur[u] = 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;
}
int main()
{
cin >> n >> m >> s >> t;
S = 0, T = n + 1;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++)
{
int a, b ,c, d;
cin >> 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;
cout << res + dinic() << endl;
}
return 0;
}
有源汇上下界最小流问题
与最大流操作相同,求出新图的最大流 r e s res res, 然后在残余网络中求出最大流:删除新加的边,然后使 S = t S = t S=t 、 T = s T = s T=s再求一遍最大流,答案就是 r e s − d i n i c ( ) res - dinic() res−dinic()
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 50010, M = (N + 125003) * 2, INF = 2147483647;
int n, m, S, T;
int h[N], e[M], f[M], ne[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()
{
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;
}
int main()
{
int s, t;
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 = t, T = s;
f[idx - 1] = f[idx - 2] = 0;
printf("%d\n", res - dinic());
}
return 0;
}
多元汇最大流问题
该题型中存在多个源点和多个汇点,建立超级源点与超级汇点
S
=
0
;
S = 0;
S=0;
T
=
n
+
1
T = n + 1
T=n+1
为保证流量守恒, 将
S
S
S 与 所有源点建立一条容量为
I
N
F
INF
INF 的的流,所有汇点与
T
T
T建立一条容量为
I
N
F
INF
INF 的流。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e4 + 10, M = (1e5 + N) * 2, INF = 1e8;
int n, m, S, T;
int e[M], ne[M], f[M], h[N], idx;
int cur[N], d[N], q[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])
{
int ver = e[i];
cur[u] = 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;
}
int main()
{
int sc, tc;
scanf("%d%d%d%d", &n, &m, &sc, &tc);
S = 0, T = n + 1;
memset(h, -1, sizeof h);
while (sc -- )
{
int x;
scanf("%d", &x);
add(S, x, INF);
}
while (tc -- )
{
int x;
scanf("%d", &x);
add(x, T, INF);
}
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
printf("%d\n", dinic());
return 0;
}
判定最大流中的关键边
设一条边为 l ( u , v ) l(u,v) l(u,v),此时该边的容量已满,并且存在从点 v v v 到汇点 T T T 容量不为 0 0 0 的流,从源点 S S S 到 u u u 有容量不为 0 0 0 的流, 则可以认定该边为关键边
最大流中的拆点
一般用与当题目中限制 只允许使用一次 的条件时,可以把一个点拆成 入点 和 出点 再在入点和出点之间连一条容量为
1
1
1 的边,就可以限制这个点只在图中被使用一次。
如果需要放开该限制的时候,只需要将入点与出点之间的边的容量设置为正无穷即可。