概念定义
-
网络:带权有向图 G=(V,E)。
-
容量:边 (u,v)∈E 的权值,记作 c(u,v)。
-
源点、汇点: s∈v,t∈v,(s=t)。
-
流量:设 f(u,v) 满足
-
容量限制:对每条边流量不超过容量,即f(u,v)≤c(u,v)
-
斜对称性:每条边容量与其相反边容量为0,即f(u,v)=−f(v,u)
-
流守恒性:除源点和汇点,流入每个点的流量与流出每个点的容量相同,即
∀x∈V−{s,t},(u,v)∈E∑f(u,x)=(u,v)∈E∑f(x,v)
则 f(u,v) 为这条边的容量。整个网络的容量 f(s,t) 为 ∑(s,v)∈Ef(s,v),即源点流出的流量总和。并有 ∑(s,v)∈Ef(s,v)=∑(v,t)∈Ef(v,t)。
最大流
给定一张网络,求出源点流向汇点的最大流量。
Ford-Fulkerson 算法
我们先考虑如下一个朴素算法,在一个网络中,寻找源点到汇点的路径,且路径上每一点都满足 f(u,v)<c(u,v),对此路径进行增流,我们定义这样的路径为增广路。重复此步骤,直到找不到路径为止。
我们考虑如下图左的一个网络,假设朴素算法第一次寻找到的路径为下图右红色箭头所示。可以发现,这时候已经找不到路径了,朴素算法得出结果为1,但真实最大流为2。
为了解决此问题,Ford-Fulkerson 算法引入了反向边,通过反向边,可以起到一个反悔作用,保证找到的一定就是最大流。
如下图,图中红色箭头为找到的增广路,黄色箭头为反向边。
我们规定反向边的容量为0,这样在对一条边进行增流时,只需同时对他的反向边进行减流即可。在实际进行操作时,我们会预先创建好所有反向边,方便进行处理。
为什么这样添加反向边就能反悔呢?我们看下图:
假设目前算法得出答案的状态是第一个,而最大流的状态是第二个。我们可以发现,第二个和第三个是完全等价的。故添加反向边就能达到一个随时反悔的作用。
这样的算法的复杂度是 O(mk) 的。其中 m 为边数,k 为最大流流量。因为每次找增广路的复杂度为 O(m),每次流量至少增加1。
Edmonds-Karp 算法
为了解决这个问题,Edmonds-Karp 算法在Ford-Fulkerson 算法上的基础上做了一个小改进:在寻找增广路时,通过BFS,找到最短路(忽略边权)。
在这篇论文中,严谨证明了若寻找增广路时是最短路,则最多进行增广 O(nm) 轮,总复杂度 O(nm2)。
在实现过程中,我们一般不会采用邻接矩阵存图(因为无法处理重边,这点到处理费用流时会很明显),我们采用一个 vector
存储所有的边(edges
),并用 n 个 vector
存储第 i
个点所有边在 edges
中的下标。
如此,我们只要依次存正向边和反向边,可以直接对这条边的下标异或1就可以得到它的反向边的下标(0^1=1,1^1=0;2^1=3,3^1=2;...
)。数据结构如下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct Edge{ int u,v; ll f,c; }; vector<Edge> edges; vector<int> G[N]; void add_edge(int u,int v,int w){ edges.push_back({u,v,0,w}); edges.push_back({v,u,0,0});
auto cnt = edges.size(); G[u].push_back(cnt-2); G[v].push_back(cnt-1); }
|
对于BFS过程,我们对每个节点记录 vis
,fa
,flow
三个数据,分别代表是否访问过,最短路的父节点以及从源点到该节点最多还能流的流量。
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
| int s,t; bool vis[N]; int fa[N]; ll flow[N]; bool bfs(){ memset(vis,0,sizeof(vis)); queue<int> q; q.push(s); vis[s]=1; flow[s]=INF; while(!q.empty() && !vis[t]){ auto u = q.front();q.pop(); for(auto e:G[u]){ auto edge = edges[e]; if(edge.c - edge.f >0 && !vis[edge.v]){ vis[edge.v]=1; fa[edge.v]=e; flow[edge.v] = min(flow[u],edge.c - edge.f); q.push(edge.v); } } } return vis[t]; }
|
EK算法流程如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ll EK(){ ll ans = 0; while(bfs()){ int now = t; ll availableFlow = flow[t]; while(now!=s){ edges[fa[now]].f += availableFlow; edges[fa[now]^1].f -= availableFlow; now = edges[fa[now]].u; } ans += availableFlow; } return ans; }
|
Dinic 算法
在实际应用中,我们更多采用的是速度更快的 Dinic 算法。
Dinic 算法要求在每次增广前用 BFS 将图分层,保证我们找到的增广路是最短的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| bool bfs(){ memset(vis,0,sizeof(vis)); queue<int> q; q.push(s); vis[s]=1; d[s]=0; while(!q.empty()){ int u = q.front();q.pop(); for(int i=0;i<G[u].size();i++){ auto& e = edges[G[u][i]]; if(!vis[e.v] && e.f<e.c){ d[e.v]=d[u]+1; vis[e.v]=1; q.push(e.v); } } } return vis[t]; }
|
我们保证从 u
出发找增广路时,只找 d[v]==d[u]+1
的节点,保证增广路最短,然后进行 DFS 找增广路。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int cur[N]; ll dfs(int u,ll a){ if(u==t || a==0) return a; ll flow = 0; for(int &i=cur[u];i<G[u].size();i++){ auto& e = edges[G[u][i]]; if(d[u]+1 == d[e.v]){ auto f = dfs(e.v,min(a,e.c-e.f)); e.f += f; edges[G[u][i]^1].f -= f; a -= f; flow += f; if(a==0) break; } } if(flow==0)d[u]=0; return flow; }
|
使用 DFS 可以一次找出多条增广路。其中 dfs(u,a)
的意思是,以 u
为起点,在最多流入 a
流量的情况下,最多能流入汇点的流量。
注意第1,5行,我们定义了一个 cur
数组,代表该节点已经增广到第几条边了(因为一条边被增广过就不可能在被增广)。
Dinic 算法主流程如下:
1 2 3 4 5 6 7 8
| ll dinic(){ ll flow = 0; while(bfs()){ memset(cur,0,sizeof(cur)); flow += dfs(s,INF); } return flow; }
|
这样,单轮增广复杂度 O(nm),总共最多进行 n 轮。 Dinic 算法的复杂度为 O(n2m),在稠密图上复杂度远超 EK 算法。
例题
洛谷P3376 【模板】网络最大流