Introduction
最大流问题就是求出一个可行流量使得从单源点到单汇点的流网络中流量最大。
且满足如下约束:
- 流过边\(e\)的流量不超过\(e\)的容量\(capacity\)
- 对于每一个顶点\(node_{\neq source,sink} \quad incoming flow=outgoing flow\)
预备知识
增广路 \(Augmenting \ Path\)
一条能够增加源点到汇点的流量的路径残余网络 \(Residual \ Network\)
仍然有容量的边构成的网络就是残余网络。最初始时,\(残余网络=原网络\)
残余网络中我们增加了一种特殊的反向边。反向边的作用在于当我们找的增广路不是最优的时候,能够进行"反悔",因为正向反向都传递相同流量\(\Leftrightarrow\)取消该流量关键边 一条增广路上流量\(=\)容量的边,即恰好能被流量填满的边。
最大流\(=\)最小割\(\ maximum \ flow = minimum \ cut\)
最小割问题:把边权看作割去某条边代价,找到一种割边的方案使得源点\(s\)和汇点\(t\)不连通所付出的代价最小。
如下图
可行的最大流为23
增广路有:
\(A \rightarrow B \rightarrow C \rightarrow E\)
\(A \rightarrow C \rightarrow E \rightarrow F\)
\(A \rightarrow C \rightarrow B \rightarrow D \rightarrow F\)
\(A \rightarrow C \rightarrow E \rightarrow D \rightarrow F\)
若用\(最大流 = 最小割\)的角度来看,最小割为\((E,D) + (E,F) + (B,D)\),加起来恰好就是23.
Ford-Fulkerson 算法
基本思路:
- 初始化流量为\(flow=0\)
- 若当前存在从源点\(source\)到汇点\(sink\)的增广路\(path\),则将流量\(path\)-\(flow\)增加到总流量\(flow\)中,同时更新残余网络。重复此步直至没有增广路。
- 返回流量\(flow\)
Edmonds-Karp算法实现Ford-Fulkerson算法
初始化流量\(maxFlow\)为0,初始化残余网络\(rG\)为原网络。
- BFS遍历\(rG\)找到一条增广路\(path\),若找到则2,否则算法结束
- 遍历增广路\(path\),找出路径能够容纳的最大容量限制,即路径上的最小边权\(pathFlow\)。将\(pathFlow\)添加到\(maxFlow\),同时更新残余网络\(rG\):由于增加了\(pathFlow\),所以增广路径上原边的容量减小\(pathFlow\),反向边的容量增加\(pathFlow\).
- 重复1直至没有增广路。
时间复杂度
每一趟增广:
- BFS查找一条增广路\(O(E)\)
- 增广后更新残余网络\(O(E)\)
关键边的总数为\(O(VE)\),每一趟赠广必然会将一条关键边填满,所以最多\(O(VE)\)趟,总的时间复杂度为\(O(VE^2)\)
具体证明请参阅《算法导论》,鄙人能力有限~
Pseudo Code for Edmonds-Karp
#define V 1e3;
#define E 1e4;
vector<vector<int> > res; //残余网络
vector<vector<int> > g; //邻接表存储【双向图】,因为需要利用到反向边
/*
* 查找是否还存在s->t的增广路
* @param s 源点
* @param t 汇点
* @param fa[] 存储前驱顶点,整体为一条增广路
* @return 若有增广路,返回增广的流量,否则返回0
*/
int bfs(int res[][V], int s, int t, int fa[]) {
fill(fa.begin(), fa.end(), -1);
fa[s] = -2;
// pair的第二参数用来在查找增广路的同时顺便找到最大流量
queue<pair<int,int> > q;
q.push({s, INF});
while (q.size()) {
int u = q.front().first;
int f = q.front().second; q.pop();
for (const v& next: adj[u]) {
if (fa[v]==-1 && res[u][v]) { // 能够增广
fa[v] = u;
int newF = min(f, res[u][v]);
if (v == t) return newF;
else q.push({v, newF});
}
}
}return 0;
}
int fordFulerson(int s, int t) {
int maxFlow = 0;
vector<int> fa(n); // 前驱数组,用于记录路径
while (int pathF = bfs(res, s, t, fa)) {
maxFlow += pathF;
int u = t;
while (u != s) {// 增加流量之后,沿着找到的增广路更新残余网络
res[ fa[u] ][u] -= pathF;
res[u][ fa[u] ] += pathF; // 反向边增加容量
u = fa[u];
}
}return maxFlow;
}
Dinic 算法
预备知识\(_{补充}\)
- 层级网络 \(layered \ network\)
原网络指定一个源点\(s\)一个汇点\(t\),每一个顶点u赋予一个\(level[u]=u与s的最短距离(路径上每条边都有剩余容量,边权看作1)\),层级网络则是包含所有满足\(level[v]=level[u]+1\)的边的网络。
算法思想
类似于Edmond-Karp算法,Dinic算法同样使用了以下的观点:
- 循环使用BFS来查找增广路。
- 若在残余网络中没有\(s\)到\(t\)的增广路,流量\(flow\)已经最大
相较于Edmond-Karp算法,Dinic算法的优化之处在于BFS查找增广路时得到的不是仅仅一条增广路,而是一张增广网络,同时进行多路增广,有些像Edmond-Karp算法的并发版本。这张增广网络的基础就是一张层级网络,BFS的过程就是给每一个点赋予一个层级建立层级网络,之后沿着多条层级逐一递增(0,1,2,3...)的路径传送流量。当层级网络无法构建时,说明当前流量已经最大。
算法步骤
- 初始化\(maxFlow\)为0,初始化残余网络\(rG\)为原网络
- 在\(rG\)上进行BFS建立层次网络(或者赋予每个顶点一个层次值\(level\)),若能够建立层次网络,即存在增广路能够增加流量:
- 利用层次网络在\(rG\)中多条增广路DFS输送流量。利用层次网络意思是每一个流量经过的路径上的顶点层次必须是从\(s\)到\(t\)逐一递增的,0,1,2,3...
- 返回\(maxFlow\).
当前弧优化
传送流量的过程中,在每一次传送完之后都会出现已经充满流量的边,这些边已经不能够再为增加流量做贡献,为了避免DFS传送流量时重复遍历这些无用边,利用一个指针来指向当前能够利用的第一条边。
时间复杂度
每一趟增广:
- BFS构建层级网络\(O(E)\)
- 层次网络中最多\(O(V)\)个顶点,每个顶点最多往\(O(E)\)个不同路径传送流量,故DFS多路传送流量直至找到最大流\(O(VE)\)。\(note \ that\) 找到最大流\(\Leftrightarrow\)某条关键边被填满。一旦发现找到了最大流就可以结束DFS了。
最多\(O(V)\)躺结束,总的时间复杂度\(O(V^2E)\).
Pseudo Code
/*
普通图的情况下:复杂度O(V2 E)
二分图下的复杂度:O(根号(VE))
*/
const int maxv = 10004, maxe = 200004;
// 链式向前星存图,双向图
struct Edge {
int next, to, cap; /*cap 为边的残余容量 */
Edge(int a=0, int b=0, int c=0):to(a), cap(b), next(c) {}
}e[maxe];
int head[maxv], cnte = 1;
int level[maxv], cur[maxv];
int n, m;
inline bool min(const int& a, const int& b) { return a<b? a:b; }
inline void add_edge(int from, int to, int cap) {
cnte += 1;
e[cnte] = Edge(to, cap, head[from]);
head[from] = cnte;
//反向边, 初始的容量为0
cnte += 1;
e[cnte] = Edge(to, cap, head[from]);
head[to] = cnte;
return;
}
//bfs分层找增广路,建立层次图
bool bfs(int s, int t) {
memset(level, 0, sizeof(level));
queue<int> q; while(q.size()) q.pop();
level[s] = 1;
q.push(s);
while (q.size())
{
int now = q.front(); q.pop();
for (int i=head[now]; i; i=e[i].next)
{
//没有被标记且有容量
if (level[e[i].to]==0 && e[i].cap > 0) {
level[e[i].to] = level[now]+1;
q.push(e[i].to);
}
}
}
//终点的level大于0说明存在增广路
return level[t] > 0;
}
// 增广流量
// 自底向上传递
// @param in 为源点输入到这个点上的最大流量
// @param now 当前顶点
// @return 到达now时传进的最大流量
int dfs(int now, int t, int in) {
if (!in || now==t)
return in;
//i为cur[now]的引用-->cur[now]随着i改变
// 保证了cur[now]为第一个有用的边
for (int& i=cur[now]; i; i=e[i].next) {
int nv = e[i].to;
if (level[nv]==level[now]+1 && e[i].cap>0)
{
// out为点now经边e[i]流向nv的流量
int out = dfs(nv, t, min(in, e[i].cap)); //一路上都受到最小流量的限制
// 能够传送说明这条路径上必然填满了一条关键边
// 那么这次的流量传送就可以结束了
if (out > 0) {
e[i].cap -= out;
e[i^1].cap += out; //反向边,用了下异或1的小技巧
return out;
}
}
}
return 0; //没有增广路,返回0
}
// 当前弧优化Dinic
// 注意是两个while,每次bfs找到多个增广路后
// 多次dfs计算网络中所有增广路的流量
int dinic(int s, int t) {
int maxFlow = 0;
while (bfs(s, t))
{
//每次建立完层次图之后记录当前弧
for (int i=1; i<=n; ++i)
cur[i] = head[i];
int flow=0;
while (flow = dfs(s, t, INF))
maxFlow += flow;
}
return maxFlow;
}
References
1.ford-fulkerson-algorithm-for-maximum-flow-problem
2.dinics-algorithm-maximum-flow
3.graph-dinic
4.stanford-network-flow-problems
5.cornell-edmondskarp