标签:代码 网络流 dinic 反向 等于 网络 返回结果 集合 print
\(\quad\) 网络流(Network-Flows)指的是一张联通无向图 \(G\),每条边具有两个属性:容量、流量。每条边的流量常用 \(f_{i,j}\) 表示,容量常用 \(c_{i,j}\) 表示。网络流有容量限制(Capacity Constraints),一条边的流量不可以超过它的容量;网络流流守恒(Flow Conservation),对于每个除了源点、汇点之外的点,都有:\(f_{in} = f_{out}\),即流入的流量始终等于流出的流量。
\(\quad\) \(\bullet\) 源点(Source Point)
\(\qquad\) 即所有流量的源头。性质:只出不进。
\(\quad\) \(\bullet\) 汇点(Sink Point)
\(\qquad\) 即所有流量的终点。性质:只进不出。
\(\quad\) \(\bullet\) 容量(Capacity)
\(\qquad\) 每条有向边固有的属性。容量指这条边可以容纳的最大流量。
\(\quad\) \(\bullet\) 残量(Residual Capacity)
\(\qquad\) 容量与流量的差。
\(\quad\) \(\bullet\) 割(Cut)
\(\qquad\) 割又叫割集(Cutset)。在网络流中,割是一些边的集合,满足当整张图去除割集中所有边后不再联通。割所允许通过的最大流量就是割的容量。
\(\quad\) \(\bullet\) 最大流最小割定理(Maxium Flow Minium Cut Theorem)
\(\qquad\) 所有割的容量都比所有可行流的流量大;特别的,容量最小的割的容量等于最大流的流量。如果有一个可行流的流量与一个割的容量相等,那么它们分别为最大流和最小割。
\(\quad\) 如果每条边的流量均不大于它们的容量,则称之为可行流(Feasible Flow),其流量为汇点的流量。特别地,当所有边的流量均为 \(0\) 时,称为零流,其流量为 \(0\)。对于一个可行流,如果存在一条从源点 \(s\) 至汇点 \(t\) 的路径,路径上所有边残量的最小值 \(e > 0\),则这条路径称为一条增广路(Augmenting Path),这条增广路的容量为 \(e\)。当一个可行流不存在增广路时,这个可行流称为最大流(Maxium Flow)。
\(\quad\) 那么,欲求最大流,只需求出一个零流中的所有增广路并增广即可。然而,一条不恰当的增广路可能会阻塞其他的增广路,使得最终的总流量变小。
\(\quad\) 如上图,每条边上所标数字代表每条边的残量,初始时为零流,残量等于容量,\(1\) 为源点,\(4\) 为汇点。
\(\quad\) 猴子找到了一条增广路:\(1 \to 2 \to 3 \to 4\),流量为 \(1\)。增广后不存在其他增广路,得出最大流为 \(1\)。然而,显然 \(1 \to 2 \to 4,\, 1 \to 3 \to 4\) 才是真正的最大流,流量为 \(2\)。
\(\quad\) 问题出在了哪儿?我们没有给予程序一个“反悔”的机会。只需要在选择 \(1 \to 2 \to 3 \to 4\) 的同时添加相应的反向边即可。接下来,程序得以找到第二条大小为 \(1\) 的增广路 \(1 \to 3 \to 2 \to 4\),得到正确的最大流 \(2\)。选择 \(3 \to 2\) 这条反向边增广相当于反悔之前选择 \(2 \to 3\) 的操作。
\(\quad\) 这就是 Edmonds-Karp 算法,增广路可以用宽度优先搜索查找。其时间复杂度上限为 \(O(n \cdot m^2)\)。
#include <stdio.h>
#include <string.h>
#include <queue>
const int MAXN = 2e2 + 19;
inline int min(const int& a, const int& b){
return a < b ? a : b;
}
int n, m, s, t, c[MAXN][MAXN], pre[MAXN];
int bfs(){
int stream = 0x3f3f3f3f; memset(pre, 0, sizeof(pre));
std::queue<int>q;
q.push(s);
while(!q.empty()){
int node = q.front();
q.pop();
if(node == t)
break;
for(int i = 1; i <= n; ++i)
if(!pre[i] && c[node][i]){
pre[i] = node;
stream = min(stream, c[node][i]);
q.push(i);
}
}
pre[s] = 0;
if(pre[t])
return stream;
else
return 0;
}
int edmonds_karp(void){
int stream, flow = 0;
while(stream = bfs()){
for(int u = t; pre[u]; u = pre[u])
c[pre[u]][u] -= stream, c[u][pre[u]] += stream;
flow += stream;
}
return flow;
}
int main(){
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1, u, v, w; i <= m; ++i)
scanf("%d%d%d", &u, &v, &w), c[u][v] += w;
printf("%d\n", edmonds_karp());
return 0;
}
\(\quad\) 一般的 EK 算法使用邻接矩阵存图,占用空间太多,效率太低。用链式前向星可以一定程度上优化它。
#include <stdio.h>
#include <string.h>
#include <queue>
const int MAXN = 1e4 + 19, MAXM = 1e5 + 19;
struct Edge{
int to, next, c;
}edge[MAXM];
int cnt, head[MAXN];
inline void add(int from, int to, int c){
edge[cnt].to = to;
edge[cnt].c = c;
edge[cnt].next = head[from];
head[from] = cnt++;
}
inline int min(const int& a, const int& b){
return a < b ? a : b;
}
int n, m, s, t, pre[MAXN];
int bfs(){
int stream = 0x3f3f3f3f; memset(pre, -1, sizeof(pre));
std::queue<int>q;
q.push(s);
pre[s] = 0;
while(!q.empty()){
int node = q.front();
q.pop();
if(node == t)
break;
for(int i = head[node]; i != -1; i = edge[i].next)
if(pre[edge[i].to] == -1 && edge[i].c){
stream = min(stream, edge[i].c);
pre[edge[i].to] = i;
q.push(edge[i].to);
}
}
pre[s] = -1;
if(pre[t] != -1)
return stream;
else
return 0;
}
int edmonds_karp(void){
int stream = 0, flow = 0;
while(stream = bfs()){
for(int u = pre[t]; u != -1; u = pre[edge[u ^ 1].to])
edge[u].c -= stream, edge[u ^ 1].c += stream;
flow += stream;
}
return flow;
}
int main(){
memset(head, -1, sizeof(head));
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1, u, v, w; i <= m; ++i)
scanf("%d%d%d", &u, &v, &w), add(u, v, w), add(v, u, 0);
printf("%d\n", edmonds_karp());
return 0;
}
\(\quad\) 慢是 Edmonds-Karp 算法的硬伤。Dinic 算法减少了搜索的次数,一定程度上解决了这个问题。
\(\quad\) Dicnic 算法要求先将整张图分为若干层:\(G_0,G_1,G_2,G_3,...G_k\)。其中,源点在 \(G_0\) 中,与源点最短距离为 \(k\) 的在 \(G_k\) 中。显然,\(\forall G_i,G_j\),若 \(|i - j| > 1\),则 \(\forall x \in G_i,y \in G_j\) 都不存在一条连接 \(x, y\) 的边。这样,使用深度优先搜索寻找增广路,要求每次都进入更深的一层图,即可大大减少搜索次数。
\(\quad\) 以 P3376 【模板】网络最大流 中的样例为例。
\(\quad\) 为了方便,定义 \(dep_s = 1\)。一次宽度优先搜索即可获得所有点的深度。
int bfs(void){
memset(dep, 0, sizeof(dep)); dep[s] = 1;
std::queue<int>q; q.push(s);
while(!q.empty()){
int node = q.front();
for(int i = head[node]; i != -1; i = edge[i].next)
if(!dep[edge[i].to] && edge[i].capacity)
dep[edge[i].to] = dep[node] + 1, q.push(edge[i].to);
q.pop();
}
return dep[t];
}
\(\quad\) 深搜发现了一条流量为 \(20\) 的增广路,增广并建立反向边,回溯至源点并退出。
\(\quad\) 重新标记各点深度(这里没有变化是个巧合),再次深搜,又发现一条 \(20\) 的增广路,建立反向边,回溯至 \(2\)。
\(\quad\) 搜索下一个深度为 \(3\) 的节点,发现无法到达,回溯至 \(2\)。此时 \(2\) 已无其他出边,回溯至上一个节点,由于是源点,退出。
\(\quad\) 重新标记各点深度。如法炮制,找到一条 \(10\) 的增广路。至此已无其他增广路,一路回溯,返回结果 \(50\)。
\(\quad\) 按照以上的思路,深搜代码为:
int dfs(int node, int flow){
if(node == t || !flow)
return flow;
int stream = 0, f;
for(int i = head[node]; i != -1; i = edge[i].next)
if(dep[edge[i].to] == dep[node] + 1 && (f = dfs(edge[i].to, min(flow, edge[i].capacity)))){
flow -= f, stream += f;
edge[i].capacity -= f, edge[i ^ 1].capacity += f;
if(!flow)
break;
}
return stream;
}
\(\quad\) Dinic 代码为:
int dinic(void){
int flow = 0;
while(bfs())
flow += dfs(s, INF);
return flow;
}
\(\quad\) 这样的代码已经能够通过 P3376 【模板】网络最大流 了。ISAP 算法将在下一篇博客介绍。
标签:代码 网络流 dinic 反向 等于 网络 返回结果 集合 print
原文地址:https://www.cnblogs.com/natsuka/p/networkflow_max.html