定义

网络是有向图的一种,不同于其他有向图,一个网络有两个特殊的点源点(一般记作 \(s\)汇点(一般记作 \(t\),顾名思义,源点只有出边而汇点只有入边。同时,每条边有边权 \(c\),表示容量,同时还映射一个值 \(flow\),叫做流量。一种容易的理解方式是将边想象成不同大小的管道。值得注意的是,流量可以为负,这一般表示反向流动以及在一些算法中对先前流量分配的“撤销”。 残量网络:将容量已满的边删去,剩下的边构成的图就是残量网络。 增广路:从源点到汇点的一条路径,其中每条边都有剩余容量。 ### 最大流问题 对于一个网络,找到最大的流量。 ### 最小割问题 我们将网络上的一些边进行分割,使之分为两部分,一部分包含 \(s\),另一部分包含 \(t\),所有切割的边的容量和叫做网络的最小割。 例:现有网络

image

其最小割为

image

\(3=2+1\)。 ### 最大流最小割定理 对于一个网络,最大流总等于最小割。这个定理看似显然,实则并不好证。证明


接下来以最大流模板题为例讲解一些算法。 ### Edmonds-Karp 算法 流程: - 从 \(s\) 出发进行 BFS 尝试走到 \(t\),即寻找增广路。 - 找到增广路后计算增广路上剩余容量的最小值 \(f\),给增广路上每条边加上 \(f\) 容量,并将它们的反向边退掉 \(f\) 容量。 - 在新图上重复以上操作直到增广路不存在。

时间复杂度 \(O(nm^2)\)。 关于反向边,有个小技巧,就是直接将正反两边的编号设为 \(2n\)\(2n+1\),这样可以通过异或 \(1\) 的操作快速完成正反边的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<cstdio>
#include<queue>
#include<cstring>
#define inf 1e18
using namespace std;
const int N=210,M=5010;
int head[N],tot=1;//因为要异或1所以初始化为1
int n,m,s,t;
int pre[N];//路径的前驱节点
long long flow[N];//各边剩余容量最小值
struct edge{
int to,nxt;
long long c;//剩余容量
}e[M*2];
int flag[N][N];
long long ans;
void add_edge(int u,int v,long long c){
e[++tot].to=v;
e[tot].c=c;
e[tot].nxt=head[u];
head[u]=tot;//正向边
e[++tot].to=u;
e[tot].c=0;
e[tot].nxt=head[v];
head[v]=tot;//反向边
return;
}
bool bfs(){
memset(pre,0,sizeof(pre));
queue<int> q;
q.push(s);
pre[s]=s;
flow[s]=inf;
while(q.size()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].c==0||pre[v])
continue;
flow[v]=min(flow[u],e[i].c);
pre[v]=i;
q.push(v);
if(v==t)
return 1;
}
}
return 0;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;i++){
int u,v;
long long w;
scanf("%d%d%lld",&u,&v,&w);
if(!flag[u][v]){//去重边
add_edge(u,v,w);
flag[u][v]=tot;
}
else
e[flag[u][v]^1].c+=w;
}
while(bfs()){
for(int u=t;u!=s;u=e[pre[u]^1].to){
e[pre[u]].c-=flow[t];
e[pre[u]^1].c+=flow[t];
}
ans+=flow[t];
}
printf("%lld",ans);
return 0;
}
### Dinic 算法 个人认为这个更难理解一些。 流程: - 在残量网络中进行 BFS 构建分层图。 - 使用 DFS,每次找到 \(t\) 后直接回溯更新边权。

使用分层图的原因是因为分层图是 DAG,进行 DFS 时不会走回头路或环路,保证了算法的正确性和高效性。 时间复杂度 \(O(n^2m)\),优于 Edmonds-Karp 算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<cstdio>
#include<queue>
#include<cstring>
#define inf 1e18
using namespace std;
const int N=210,M=5010;
int n,m,s,t,tot=1,head[N];
long long ans;
struct edge{
int to,nxt;
long long c;
}e[M*2];
void add_edge(int u,int v,long long c){
e[++tot].to=v;
e[tot].c=c;
e[tot].nxt=head[u];
head[u]=tot;
e[++tot].to=u;
e[tot].c=0;
e[tot].nxt=head[v];
head[v]=tot;
return;
}
int level[N];//层级
bool bfs(){
memset(level,-1,sizeof(level));
queue<int> q;
q.push(s);
level[s]=0;
while(q.size()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].c==0||level[v]!=-1)
continue;
level[v]=level[u]+1;
q.push(v);
}
}
return level[t]!=-1;
}
long long dfs(int u,long long flow){
if(u==t)
return flow;
long long res=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].c==0||level[v]!=level[u]+1)//按层dfs
continue;
long long temp=dfs(v,min(flow-res,e[i].c));
e[i].c-=temp;
e[i^1].c+=temp;
res+=temp;
if(res==flow)
break;
}
if(res==0)
level[u]=-1;
return res;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
}
while(bfs())
ans+=dfs(s,inf);
printf("%lld",ans);
return 0;
}
优化:当前弧优化。减少 DFS 对边的重复检查。若某边剩余容量已为 \(0\),则不再处理这条边。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//...
int cur[N];//当前弧优化
long long dfs(int u,long long flow){
if(u==t)
return flow;
long long res=0;
for(int i=cur[u];i;i=e[i].nxt){
cur[u]=i;//当前弧优化
int v=e[i].to;
if(e[i].c==0||level[v]!=level[u]+1)//按层dfs
continue;
long long temp=dfs(v,min(flow-res,e[i].c));
e[i].c-=temp;
e[i^1].c+=temp;
res+=temp;
if(res==flow)
break;
}
if(res==0)
level[u]=-1;
return res;
}
//...
while(bfs()){
memcpy(cur,head,sizeof(head));//重置当前弧
ans+=dfs(s,inf);
}
//...
### 最小费用最大流 费用流定义:对于一个网络的每条边,我们再加一个边权称为一个单位流量的费用,记作 \(w(u,v)\),则这条边的费用为 \(f(u,v)\times w(u,v)\)。与上文中提到的反向边回退流量相同,费用也可以这样操作。 与普通最大流问题不同,最小费用最大流的求解过程不用 BFS 而用最短路算法寻找增广路,这样就保证了最小费用。 这里给出 Edmonds-Karp + SPFA 的做法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=5e3+10,M=5e4+10;
struct edge{
int to,nxt,c,w;
}e[M*2];
int tot=1,head[N],n,m,s,t;
void add_edge(int u,int v,int c,int w){
e[++tot].to=v;
e[tot].nxt=head[u];
e[tot].c=c;
e[tot].w=w;
head[u]=tot;
e[++tot].to=u;
e[tot].nxt=head[v];
e[tot].c=0;
e[tot].w=-w;
head[v]=tot;
return;
}
int min_flow[N],dis[N],pre[N],flow,cost;
queue<int> q;
bool vis[N];
bool spfa(){
memset(dis,0x3f,sizeof(dis));
memset(min_flow,0,sizeof(min_flow));
memset(vis,0,sizeof(vis));
q.push(s);
vis[s]=1;
dis[s]=0;
min_flow[s]=2e9;
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,c=e[i].c,w=e[i].w;
if(c&&dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
pre[v]=i;
min_flow[v]=min(min_flow[u],c);
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
return min_flow[t]>0;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,c,w;
cin>>u>>v>>c>>w;
add_edge(u,v,c,w);
}
while(spfa()){
int minf=min_flow[t];
for(int i=t;i!=s;i=e[pre[i]^1].to){
e[pre[i]].c-=minf;
e[pre[i]^1].c+=minf;
}
flow+=minf;
cost+=minf*dis[t];
}
cout<<flow<<' '<<cost;
return 0;
}
### 网络流建模 例题

考虑拆点。将点 \((x,y)\) 拆为 \((x,y)_1\)\((x,y)_2\)。 对于 \(0\)(平坦无障碍),建 \((x,y)_1\to(x,y)_2\),容量 \(inf\),费用 \(0\),表示可以无限通过但无收益。同时,建 \((x,y)_2\to(x+1,y)_1\)\((x,y)_2\to(x,y+1)_1\),容量与费用同上,表示向南和向东走。 对于 \(1\)(障碍),不建立节点。 对于 \(2\)(石块),先建 \(0\)(平坦无障碍)的 \(3\) 条边,然后再建一条 \((x,y)_1\to(x,y)_2\),容量 \(1\),费用 \(1\),表示只能走 \(1\) 次且贡献为 \(1\)。 然后跑最大费用最大流就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include<iostream>
#include<queue>
#include<cstring>
#define inf 2e9
using namespace std;
const int N=35*35*2+10,M=1e5+10;
int p,q,nodecnt,node[N][N],tot=1,head[N],n;
struct edge{
int to,nxt,c,w;
}e[M];
void add_edge1(int u,int v,int c,int w){
e[++tot].to=v;
e[tot].nxt=head[u];
e[tot].c=c;
e[tot].w=w;
head[u]=tot;
return;
}
void add_edge(int u,int v,int c,int w){
add_edge1(u,v,c,w);
add_edge1(v,u,0,-w);
return;
}
int pre[N],dis[N],s,t,flow[N];
bool vis[N];
queue<int> que;
bool spfa(){
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(flow,0,sizeof(flow));
dis[s]=0;
vis[s]=1;
flow[s]=inf;
que.push(s);
while(!que.empty()){
int u=que.front();
que.pop();
vis[u]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to,c=e[i].c,w=e[i].w;
if(c&&dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
pre[v]=i;
flow[v]=min(flow[u],c);
if(!vis[v]){
vis[v]=1;
que.push(v);
}
}
}
}
return flow[t]>0;
}
int cnt[N][N];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>p>>q;
for(int i=1;i<=q;i++)
for(int j=1;j<=p;j++)
node[i][j]=++nodecnt;
for(int i=1,c;i<=q;i++){
for(int j=1;j<=p;j++){
cin>>c;
if(c==1)
continue;
add_edge(node[i][j],node[i][j]+nodecnt,inf,0);
if(c==2)
add_edge(node[i][j],node[i][j]+nodecnt,1,-1);
if(i<q)
add_edge(node[i][j]+nodecnt,node[i+1][j],inf,0);
if(j<p)
add_edge(node[i][j]+nodecnt,node[i][j+1],inf,0);
}
}
s=0,t=nodecnt*2+1;
add_edge(s,node[1][1],n,0);
add_edge(node[q][p]+nodecnt,t,n,0);
while(spfa()){
int minf=flow[t];
for(int i=t,cnt=0;i!=s;i=e[pre[i]^1].to,cnt++){
e[pre[i]].c-=minf;
e[pre[i]^1].c+=minf;
}
}
for(int i=1;i<=q;i++)
for(int j=1;j<=p;j++)
for(int k=head[node[i][j]];k;k=e[k].nxt){
int v=e[k].to;
if(v==node[i][j]+nodecnt)
cnt[i][j]+=e[k^1].c;
}
for(int i=1;i<=n;i++){
int x=1,y=1;
while(x!=q||y!=p){
if(cnt[x+1][y]){
cout<<i<<" 0\n";
cnt[x+1][y]--;
x++;
}
else{
cout<<i<<" 1\n";
cnt[x][y+1]--;
y++;
}
}
}
return 0;
}
### 其他 参考资料:

OI Wiki

题解 P3376 【【模板】网络最大流】

【题解】P3356 火星探险问题