网络最大流
Posted natsuka
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络最大流相关的知识,希望对你有一定的参考价值。
(quad) 网络流(Network-Flows)指的是一张联通无向图 (G),每条边具有两个属性:容量、流量。每条边的流量常用 (f_{i,j}) 表示,容量常用 (c_{i,j}) 表示。网络流有容量限制(Capacity Constraints),一条边的流量不可以超过它的容量;网络流流守恒(Flow Conservation),对于每个除了源点、汇点之外的点,都有:(f_{in} = f_{out}),即流入的流量始终等于流出的流量。
(quad) (ullet) 源点(Source Point)
(qquad) 即所有流量的源头。性质:只出不进。
(quad) (ullet) 汇点(Sink Point)
(qquad) 即所有流量的终点。性质:只进不出。
(quad) (ullet) 容量(Capacity)
(qquad) 每条有向边固有的属性。容量指这条边可以容纳的最大流量。
(quad) (ullet) 残量(Residual Capacity)
(qquad) 容量与流量的差。
(quad) (ullet) 割(Cut)
(qquad) 割又叫割集(Cutset)。在网络流中,割是一些边的集合,满足当整张图去除割集中所有边后不再联通。割所允许通过的最大流量就是割的容量。
(quad) (ullet) 最大流最小割定理(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 o 2 o 3 o 4),流量为 (1)。增广后不存在其他增广路,得出最大流为 (1)。然而,显然 (1 o 2 o 4,, 1 o 3 o 4) 才是真正的最大流,流量为 (2)。
(quad) 问题出在了哪儿?我们没有给予程序一个“反悔”的机会。只需要在选择 (1 o 2 o 3 o 4) 的同时添加相应的反向边即可。接下来,程序得以找到第二条大小为 (1) 的增广路 (1 o 3 o 2 o 4),得到正确的最大流 (2)。选择 (3 o 2) 这条反向边增广相当于反悔之前选择 (2 o 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
", 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
", 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 算法将在下一篇博客介绍。
以上是关于网络最大流的主要内容,如果未能解决你的问题,请参考以下文章