一.网络流:流&网络&割
1.网络流问题(NetWork Flow Problem):
给定指定的一个有向图,其中有两个特殊的点源S(Sources)和汇T(Sinks),每条边有指定的容量(Capacity),求满足条件的从S到T的最大流(MaxFlow).
下面给出一个通俗点的解释
(下文基本避开形式化的证明 基本都用此类描述叙述)
好比你家是汇 自来水厂(有需要的同学可以把自来水厂当成银行之类 以下类似)是源
然后自来水厂和你家之间修了很多条水管子接在一起 水管子规格不一 有的容量大 有的容量小
然后问自来水厂开闸放水 你家收到水的最大流量是多少
如果自来水厂停水了 你家那的流量就是0 当然不是最大的流量
但是你给自来水厂交了100w美金 自来水厂拼命水管里通水 但是你家的流量也就那么多不变了 这时就达到了最大流
-------------------------------------------------------------------------------------------------------------
2.三个基本的性质:
如果 C代表每条边的容量 F代表每条边的流量
一个显然的实事是F小于等于C 不然水管子就爆了,这就是网络流的第一条性质 容量限制(Capacity Constraints):F<x,y> ≤ C<x,y>
再考虑节点任意一个节点 流入量总是等于流出的量 否则就会蓄水(爆炸危险...)或者平白无故多出水(有地下水涌出?)
这是第二条性质 流量守恒(Flow Conservation):Σ F<v,x> = Σ F<x,u>
当然源和汇不用满足流量守恒 我们不用去关心自来水厂的水是河里的 还是江里的
最后一个不是很显然的性质 是斜对称性(Skew Symmetry): F<x,y> = - F<y,x>
这其实是完善的网络流理论不可缺少的 就好比中学物理里用正负数来定义一维的位移一样
百米起点到百米终点的位移是100m的话 那么终点到起点的位移就是-100m
同样的 x向y流了F的流 y就向x流了-F的流
对于任意一个时刻,设f(u,v)实际流量,则整个图G的流网络满足3个性质:
1. 容量限制:对任意u,v∈V,f(u,v)≤c(u,v)。
2. 反对称性:对任意u,v∈V,f(u,v) = -f(v,u)。从u到v的流量一定是从v到u的流量的相反值。
3. 流守恒性:对任意u,若u不为S或T,一定有∑f(u,v)=0,(u,v)∈E。即u到相邻节点的流量之和为0,因为流入u的流量和u点流出的流量相等,u点本身不会"制造"和"消耗"流量。
-------------------------------------------------------------------------------------------------------------
3.容量网络&流量网络&残留网络:
网络就是有源汇的有向图 关于什么就是指边权的含义是什么
容量网络就是关于容量的网络 基本是不改变的(极少数问题需要变动)
流量网络就是关于流量的网络 在求解问题的过程中
通常在不断的改变 但是总是满足上述三个性质
调整到最后就是最大流网络 同时也可以得到最大流值
残留网络往往概括了容量网络和流量网络 是最为常用的
残留网络=容量网络-流量网络
这个等式是始终成立的 残留值当流量值为负时甚至会大于容量值
流量值为什么会为负?有正必有负,记住斜对称性!
4.割&割集:
无向图的割集(Cut Set):C[A,B]是将图G分为A和B两个点集 A和B之间的边的全集
网络的割集:C[S,T]是将网络G分为s和t两部分点集 S属于s且T属于t 从S到T的边的全集
带权图的割(Cut)就是割集中边或者有向边的权和
通俗的理解一下:
割集好比是一个恐怖分子 把你家和自来水厂之间的水管网络砍断了一些
然后自来水厂无论怎么放水 水都只能从水管断口哗哗流走了 你家就停水了
割的大小应该是恐怖分子应该关心的事 毕竟细管子好割一些
而最小割花的力气最小
==================================================================================
二.计算最大流的基本算法
那么怎么求出一个网络的最大流呢?
这里介绍一个最简单的算法:Edmonds-Karp算法 即最短路径增广算法 简称EK算法
EK算法基于一个基本的方法:Ford-Fulkerson方法 即增广路方法 简称FF方法
增广路方法是很多网络流算法的基础 一般都在残留网络中实现
其思路是每次找出一条从源到汇的能够增加流的路径 调整流值和残留网络 不断调整直到没有增广路为止
FF方法的基础是增广路定理(Augmenting Path Theorem):网络达到最大流当且仅当残留网络中没有增广路
证明略 这个定理应该能够接受的吧
EK算法就是不断的找最短路 找的方法就是每次找一条边数最少的增广 也就是最短路径增广
这样就产生了三个问题:
-------------------------------------------------------------------------------------------------------------
1.最多要增广多少次?
可以证明 最多O(VE)次增广 可以达到最大流 证明略
2.如何找到一条增广路?
先明确什么是增广路 增广路是这样一条从s到t的路径 路径上每条边残留容量都为正
把残留容量为正的边设为可行的边 那么我们就可以用简单的BFS得到边数最少的增广路
3.如何增广?
BFS得到增广路之后 这条增广路能够增广的流值 是路径上最小残留容量边决定的
把这个最小残留容量MinCap值加到最大流值Flow上 同时路径上每条边的残留容量值减去MinCap
最后 路径上每条边的反向边残留容量值要加上MinCap 为什么? 下面会具体解释
-------------------------------------------------------------------------------------------------------------
这样每次增广的复杂度为O(E) EK算法的总复杂度就是O(VE^2),事实上 大多数网络的增广次数很少 EK算法能处理绝大多数问题,平均意义下增广路算法都是很快的
增广路算法好比是自来水公司不断的往水管网里一条一条的通水
上面还遗留了一个反向边的问题: 为什么增广路径上每条边的反向边残留容量值要加上MinCap?
*********************************************************************************************
因为斜对称性! 由于残留网络=容量网络-流量网络
容量网络不改变的情况下
由于增广好比给增广路上通了一条流 路径说所有边流量加MinCap
流量网络中路径上边的流量加MinCap 反向边流量减去MinCap
相对应的残留网络就发生相反的改变
*********************************************************************************************
这样我们就完成了EK算法 具体实现可以用邻接表存图 也可以用邻接矩阵存图
邻接表存图 由于流量同时存在于边与反向边 为了方便求取反向边 建图把一对互为反向边的边建在一起
代码很简单 最好自己实现一下
看一个具体的增广路算法的例子吧
=====================================================================
三.最大流最小割定理
下面介绍网络流理论中一个最为重要的定理
最大流最小割定理(Maximum Flow, Minimum Cut Theorem):网络的最大流等于最小割
具体的证明分三部分
1.任意一个流都小于等于任意一个割
这个很好理解 自来水公司随便给你家通点水 构成一个流
恐怖分子随便砍几刀 砍出一个割
由于容量限制 每一根的被砍的水管子流出的水流量都小于管子的容量
每一根被砍的水管的水本来都要到你家的 现在流到外面 加起来得到的流量还是等于原来的流
管子的容量加起来就是割 所以流小于等于割
由于上面的流和割都是任意构造的 所以任意一个流小于任意一个割
2.构造出一个流等于一个割
当达到最大流时 根据增广路定理
残留网络中s到t已经没有通路了 否则还能继续增广
我们把s能到的的点集设为S 不能到的点集为T
构造出一个割集C[S,T] S到T的边必然满流 否则就能继续增广
这些满流边的流量和就是当前的流即最大流
把这些满流边作为割 就构造出了一个和最大流相等的割
3.最大流等于最小割
设相等的流和割分别为Fm和Cm
则因为任意一个流小于等于任意一个割
任意F≤Fm=Cm≤任意C
定理说明完成,证明如下:
对于一个网络流图G=(V,E),其中有源点s和汇点t,那么下面三个条件是等价的:
1. 流f是图G的最大流
2. 残留网络Gf不存在增广路
3. 对于G的某一个割(S,T),此时f = C(S,T)
首先证明1 => 2:
我们利用反证法,假设流f是图G的最大流,但是残留网络中还存在有增广路p,其流量为fp。则我们有流f'=f+fp>f。这与f是最大流产生矛盾。
接着证明2 => 3:
假设残留网络Gf不存在增广路,所以在残留网络Gf中不存在路径从s到达t。我们定义S集合为:当前残留网络中s能够到达的点。同时定义T=V-S。
此时(S,T)构成一个割(S,T)。且对于任意的u∈S,v∈T,有f(u,v)=c(u,v)。若f(u,v)<c(u,v),则有Gf(u,v)>0,s可以到达v,与v属于T矛盾。
因此有f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T)。
最后证明3 => 1:
由于f的上界为最小割,当f到达割的容量时,显然就已经到达最大值,因此f为最大流。
这样就说明了为什么找不到增广路时,所求得的一定是最大流。
=====================================================================
网络流入门
基本概念(从书上摘抄,可以直接跳过不看)
容量网络和网络最大流
容量网络: 设 G(V, E)
是一个有向网络, 在 V 中指定了一个顶点, 称为源点(记为 Vs ), 以及另一个顶点, 称为汇点(记为 Vt); 对于每一条弧 <u, v>∈E
, 对应有一个权值 c(u, v)>0, 称为弧的容量
, 通常把这样的有向网络 G 称为容量网络。
也就是指: 一个拥有源点、汇点并且可以容纳流量的图.
弧的流量: 通过容量网络 G 中每条弧 <u, v>
上的实际流量(简称流量), 记为 f(u, v)
。
网络流: 所有弧上流量的集合 f = { f(u, v) }
,称为该容量网络 G 的一个网络流。
可行流: 在容量网络 G(V, E)
中, 满足以下条件的网络流 f, 称为可行流:
- 弧流量限制条件: 0≤f(u,v)≤c(u,v)
- 平衡条件: 除了 Vs, Vt 外, 其余的点流入的流量总和等于流出的流量总和, 其中
Vs 流出的流量总和 - 流出的流量总和 = f
,Vt 流入的流量总和 - 流出的流量总和 = f
, 并且称f
为可性流的流量.
也就是指: 在图中有一条从 Vs 到 Vt 的路径, 这条路径上起点 fo−fi=f, 终点 fi−fo=f, 其他的点 fi==fo, 并且所有的边的当前流量小于等于最大流量.(其中 fi 代表流入流量, fo 代表流出流量)
伪流: 如果一个网络流只满足弧流量限制条件, 不满足平衡条件, 则这种网络流称为伪流, 或称为容量可行流。
最大流: 在容量网络 G(V, E)
中, 满足弧流量限制条件和平衡条件、且具有最大流量的可行流, 称为网络最大流, 简称最大流。
链与增广路
在容量网络 G(V, E)
中, 设有一可行流 f = { f(u, v) }
, 根据每条弧上流量的多少、以及流量和容量的关系,可将弧分四种类型:
- 饱和弧, 即 f(u,v)=c(u,v);
- 非饱和弧,即 f(u,v)<c(u,v);
- 零流弧, 即 f(u,v)=0;
- 非零流弧, 即 f(u,v)>0。
链: 在容量网络中,称顶点序列(u,u1,u2,…,un,v)为一条链,要求相邻两个顶点之间有一条弧, 如 <u, u1>
或 <u1, u>
为容量网络中一条弧。沿着 Vs 到 Vt 的一条链, 各弧可分为两类:
- 前向弧: 方向与链的正方向一致的弧, 其集合记为
P+
; - 后向弧: 方向与链的正方向相反的弧, 其集合记为
P-
;
增广路: 设 f 是一个容量网络 G 中的一个可行流, P 是从 Vs 到 Vt 的一条链, 若 P 满足下列条件:
- 在 P 的所有前向弧
<u, v>
上, 0≤f(u,v)<c(u,v), 即P+
中每一条弧都是非饱和弧; - 在 P 的所有后向弧
<u, v>
上, 0<f(u,v)≤c(u,v), 即P–
中每一条弧是非零流弧。
则称 P 为关于可行流 f 的一条增广路, 简称为 增广路(或称为增广链、可改进路)
。沿着增广路改进可行流的操作称为增广
残留容量与残留网络
残留容量: 给定容量网络 G(V, E)
及可行流 f, 弧 <u, v>
上的残留容量记为 c′(u,v)=c(u,v)–f(u,v)。每条弧的残留容量表示该弧上可以增加的流量。因为从顶点 u 到顶点 v 流量的减少, 等效于顶点 v 到顶点 u 流量增加, 所以每条弧 <u, v>
上还有一个反方向的残留容量 c′(v,u)=–f(u,v)。
一个容量网络中还可以压入的流量称为残留容量
残留网络: 设有容量网络 G(V, E)
及其上的网络流 f,G 关于 f 的残留网络(简称残留网络)记为 G'(V', E')
, 其中 G’的顶点集 V’和 G 的顶点集 V 相同,即 V’=V, 对于 G 中的任何一条弧 <u, v>
, 如果 f(u,v)<c(u,v), 那么在 G’中有一条弧 <u, v>∈E'
, 其容量为 c′(u,v)=c(u,v)–f(u,v), 如果 f(u,v)>0,则在 G’中有一条弧 <v, u>∈E'
, 其容量为 c′(v,u)=f(u,v), 残留网络也称为剩余网络
.
由残留的容量以及源点汇点构成的网络。
割与最小割
割: 在容量网络 G(V, E)
中, 设 E'⊆E
, 如果在 G 的基图中删去 E’ 后不再连通, 则称 E’ 是 G 的割。割将 G 的顶点集 V 划分成两个子集 S 和 T = V - S。将割记为(S, T)。
s-t 割: 更进一步, 如果割所划分的两个顶点子集满足源点 Vs ∈ S
,汇点 Vt ∈ T
, 则称该割为 s-t 割
。 s-t 割(S, T)中的弧 <u, v>(u∈S, v∈T)
称为割的前向弧, 弧 <u, v>( u∈T, v∈S)
称为割的反向弧。
割的容量: 设 (S, T)
为容量网络 G(V, E)
的一个割, 其容量定义为所有前向弧的容量总和, 用 c(S, T)
表示。
最小割: 容量网络 G(V, E)
的最小割是指容量最小的割。
相关定理
残留网络与原网络的关系
设 f 是容量网络 G(V, E) 的可行流, f’ 是残留网络 G’ 的可行流, 则 f + f’ 仍是容量网络 G 的一个可行流。(f + f’ 表示对应弧上的流量相加)
网络流流量与割的净流量之间的关系
在一个容量网络 G(V, E) 中, 设其任意一个流为 f, 关于 f 的任意一个割为(S, T), 则有 f(S,T)=|f|,即网络流的流量等于任何割的净流量。
网络流流量与割的容量之间的关系
在一个容量网络 G(V, E) 中, 设其任意一个流为 f, 任意一个割为(S, T), 则必有 f(S,T)≤c(S,T),即网络流的流量小于或等于任何割的容量。
最大流最小割定理
对容量网络 G(V, E), 其最大流的流量等于最小割的容量。
增广路定理
设容量网络 G(V, E) 的一个可行流为 f, f 为最大流的充要条件是在容量网络中不存在增广路。
几个等价命题
设容量网络 G(V, E)的一个可行流为 f 则:
- 1) f 是容量网络 G 的最大流;
- 2) | f |等于容量网络最小割的容量;
- 3) 容量网络中不存在增广路;
- 4) 残留网络 G’中不存在从源点到汇点的路径。
最大流
最大流相关算法有两种解决思想, 一种是增广路算法
思想, 另一种是预流推进
算法思想。 下面将分别介绍这两种算法思想。
增广路算法(Ford-Fulkerson)
基本思想
根据增广路定理, 为了得到最大流, 可以从任何一个可行流开始, 沿着增广路对网络流进行增广, 直到网络中不存在增广路为止,这样的算法称为增广路算法。问题的关键在于如何有效地找到增广路, 并保证算法在有限次增广后一定终止。
增广路算法的基本流程是 :
- (1) 取一个可行流 f 作为初始流(如果没有给定初始流,则取零流 f= { 0 }作为初始流);
- (2) 寻找关于 f 的增广路 P,如果找到,则沿着这条增广路 P 将 f 改进成一个更大的流, 并建立相应的反向弧;
- (3) 重复第(2)步直到 f 不存在增广路为止。
增广路算法的关键是 寻找增广路
和 改进网络流
。
问题: 为什么要创建反向弧呢?
原因: 为程序提供一次反悔的机会 什么意思, 如下图所示:
在图中如果程序找到了一条增广路 1 -> 2 -> 4 -> 6, 此时得到一个流量为 2 的流并且无法继续进行增广,
但是如果在更新可行流的同时建立反向弧的话, 就可以找到 1 -> 3 -> 4 -> 2 -> 5 -> 6 的可行流, 流量为1, 这样就可以得到最大流为 3.
一般增广路算法(EdmondsKarp)
算法流程
在一般的增广路算法中, 程序的实现过程与增广路求最大流的过程基本一致. 即每一次更新都进行一次找增广路然后更新路径上的流量的过程。但是我们可以从上图中发现一个问题, 就是每次找到的增广路曲曲折折非常长, 此时我们往往走了冤枉路(即:明明我们可以从源点离汇点越走越进的,可是中间的几条边却向离汇点远的方向走了), 此时更新增广路的复杂度就会增加。EK 算法为了规避这个问题使用了 bfs 来寻找增广路, 然后在寻找增广路的时候总是向离汇点越来越近的方向去寻找下一个结点。
算法实现
邻接矩阵
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int MAXN = 300;
const int MAX_INT = ((1 << 31) - 1);
int n; // 图中点的数目
int pre[MAXN]; // 从 s - t 中的一个可行流中, 节点 i 的前序节点为 Pre[i];
bool vis[MAXN]; // 标记一个点是否被访问过
int mp[MAXN][MAXN]; // 记录图信息
bool bfs(int s, int t){
queue <int> que;
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
pre[s] = s;
vis[s] = true;
que.push(s);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i = 1; i <= n; i++){
if(mp[u][i] && !vis[i]){
pre[i] = u;
vis[i] = true;
if(i == t) return true;
que.push(i);
}
}
}
return false;
}
int EK(int s, int t){
int ans = 0;
while(bfs(s, t)){
int mi = MAX_INT;
for(int i = t; i != s; i = pre[i]){
mi = min(mi, mp[pre[i]][i]);
}
for(int i = t; i != s; i = pre[i]){
mp[pre[i]][i] -= mi;
mp[i][pre[i]] += mi;
}
ans += mi;
}
return ans;
}
邻接表
const int MAXN = 430;
const int MAX_INT = (1 << 30);
struct Edge{
int v, nxt, w;
};
struct Node{
int v, id;
};
int n, m, ecnt;
bool vis[MAXN];
int head[MAXN];
Node pre[MAXN];
Edge edge[MAXN];
void init(){
ecnt = 0;
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
bool bfs(int s, int t){
queue <int> que;
memset(vis, 0, sizeof(vis));
memset(pre, -1, sizeof(pre));
pre[s].v = s;
vis[s] = true;
que.push(s);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i = head[u]; i + 1; i = edge[i].nxt){
int v = edge[i].v;
if(!vis[v] && edge[i].w){
pre[v].v = u;
pre[v].id = i;
vis[v] = true;
if(v == t) return true;
que.push(v);
}
}
}
return false;
}
int EK(int s, int t){
int ans = 0;
while(bfs(s, t)){
int mi = MAX_INT;
for(int i = t; i != s; i = pre[i].v){
mi = min(mi, edge[pre[i].id].w);
}
for(int i = t; i != s; i = pre[i].v){
edge[pre[i].id].w -= mi;
edge[pre[i].id ^ 1].w += mi;
}
ans += mi;
}
return ans;
}
// 加边
addEdge(u, v, w);
addEdge(v, u, 0);
// 调用
int ans = EK(s, t);
算法复杂度
每进行一次增广需要的时间复杂度为 bfs 的复杂度 + 更新残余网络的复杂度, 大约为 O(m)(m为图中的边的数目), 需要进行多少次增广呢, 假设每次增广只增加1, 则需要增广 nW 次(n为图中顶点的数目, W为图中边上的最大容量), .
Dinic 算法
算法思想
DINIC 在找增广路的时候也是找的最短增广路, 与 EK 算法不同的是 DINIC 算法并不是每次 bfs 只找一个增广路, 他会首先通过一次 bfs 为所有点添加一个标号, 构成一个层次图, 然后在层次图中寻找增广路进行更新。
算法流程
- 利用 BFS 对原来的图进行分层,即对每个结点进行标号, 这个标号的含义是当前结点距离源点的最短距离(假设每条边的距离都为1),注意:构建层次图的时候所走的边的残余流量必须大于0
- 用 DFS 寻找一条从源点到汇点的增广路, 注意: 此处寻找增广路的时候要按照层次图的顺序, 即如果将边(u, v)纳入这条增广路的话必须满足dis[u]=dis[v]−1, 其中 dis[i]为结点 ii的编号。找到一条路后要根据这条增广路径上的所有边的残余流量的最小值ll更新所有边的残余流量(即正向弧 - l, 反向弧 + l).
- 重复步骤 2, 当找不到一条增广路的时候, 重复步骤 1, 重新建立层次图, 直到从源点不能到达汇点为止。
算法实现
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 510;
const int MAXN_INT = (1 << 29);
int n, m;
int dis[MAXN];
int mp[MAXN][MAXN];
int bfs(int s){
memset(dis, 0xff, sizeof(dis));
dis[s] = 0;
queue <int> que;
que.push(s);
while(!que.empty()){
int top = que.front();
que.pop();
for(int i = 1; i <= n; i++){
if(dis[i] < 0 && mp[top][i] > 0){
dis[i] = dis[top] + 1;
que.push(i);
}
}
}
if(dis[n] > 0) return true;
return false;
}
int Find(int x, int low){
int a = 0;
if(x == n) return low;
for(int i = 1; i <= n; i++){
if(mp[x][i] > 0
&& dis[i] == dis[x] + 1
&& (a = Find(i, min(low, mp[x][i])))){
mp[x][i] -= a;
mp[i][x] += a;
return a;
}
}
return 0;
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
memset(mp, 0, sizeof(mp));
int u, v, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &w);
mp[u][v] += w;
}
int ans = 0, tmp;
while(bfs(1)){
while(tmp = Find(1, MAXN_INT))
ans += tmp;
}
printf("%d\n", ans);
}
return 0;
}
当前弧优化和多路增广:
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 101000;
const int MAXN_INT = (1 << 29);
struct Edge{
int v, w, nxt;
};
int s, t;
int n, m, ecnt;
Edge edge[MAXN * 2];
int head[MAXN], dis[MAXN], curEdge[MAXN];
void init(){
ecnt = 0;
memset(dis, -1, sizeof(dis));
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
int bfs(){
memset(dis,-1,sizeof(dis));
dis[t] = 0;
queue <int> que;
que.push(t);
while(!que.empty()){
int u = que.front();
que.pop();
for(int i = head[u]; i + 1; i = edge[i].nxt){
if(dis[edge[i].v] == -1 && edge[i ^ 1].w > 0){
dis[edge[i].v] = dis[u] + 1;
que.push(edge[i].v);
}
}
}
return dis[s] != -1;
}
int dfs(int u, int v, int flow){
if(u == t) return flow;
int delta = flow;
for(int &i = curEdge[u]; i + 1; i = edge[i].nxt){
if(dis[u] == dis[edge[i].v] + 1 && edge[i].w){
int d = dfs(edge[i].v, v, min(delta, edge[i].w));
edge[i].w -= d, edge[i ^ 1].w += d;
delta -= d;
if(delta == 0) break;
}
}
return flow - delta;
}
int dinic(){
int ans = 0;
while(bfs()){
for(int i = 0; i < n; i++)
curEdge[i] = head[i];
ans += dfs(s, t, MAXN_INT);
}
return ans;
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
init();
int u, v, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
addEdge(v, u, 0);
}
printf("%d\n", dinic());
}
return 0;
}
时间复杂度
$O(V^2E)
最短增广路算法(SAP)
算法思想
最短增广路算法是一种运用距离标号使寻找增广路的时间复杂度下降的算法。所谓的距离标号就是某个点到汇点的最少的弧的数量(即当边权为1时某个点的最短路径长度). 设点i的标号为d[i], 那么如果将满足d[i] = d[j] + 1, 且增广时只走允许弧, 那么就可以达到”怎么走都是最短路”的效果. 每个点的初始标号可以在一开始用一次从汇点沿所有反向的BFS求出.
算法流程
1) 定义节点的标号为到汇点的最短距离;
2) 每次沿可行边进行增广, 可行边即: 假设有两个点 i, j 若 d[i] = 3, d[j] = 4, 则d[j] = d[i] + 1, 也就是从 j 到 i 有一条边.
3) 找到增广路后,将路径上所有边的流量更新.
4) 遍历完当前结点的可行边后更新当前结点的标号为 d[now]=min(d[next]|Flow(now,next)>0)+1,使下次再搜的时候有路可走。
5) 图中不存在增广路后即退出程序,此时得到的流量值就是最大流。
需要注意的是, 标号的更新过程首先我们要理解更新标号的目的。标号如果需要更新,说明在当前的标号下已经没有增广路可以继续走,这时更新标号就可以使得我们有继续向下走的可能,并且每次找的都是能走到的点中标号最小的那个点,这样也使得每次搜索长度最小.
下面的图演示了标号的更新过程:
- 首先我们假设有个图如下,为了简化没有标箭头也没有写流量:
- 为图标号, 每个点的标号为其到汇点的最短距离(这里把每条边看作1)
- 第一遍遍历时,找到了1->2->9这样一条增广路以后,更新边上流量值, 得到下图
棕色字体为边上的流量值。这时按照标号再搜一遍,发现从1出发已经找不到增广路了,因为flow(1,2)等于0不可以走,h[1]=2,h[3]=2≠h[1]+1,h[5]=4≠h[1]+1, 所以这时更新1的标号, - 按照 min(h[next]|Flow(now,next)>0)+1,修改后 h[1]=h[3]+1=3.
- 第二遍遍历以后找到了这样一条增广路:1->3->4->9,做完这条路以后又发现无法找到可行边了,这时再更新标号使图中有路可走,如上文所说的那样做,再次修改后h[1]=h[5]+1=5h[1]=h[5]+1=5,就这样搜索并更新直到变成下图
- 这时再更新h[1]发现没有点可以用来更新h[1]了,于是此时h[1]=∞,使程序退出。
GAP 优化: 由于可行边定义为:(now,next)|h[now]=h[next]+1,所以若标号出现“断层”即有的标号对应的顶点个数为0,则说明剩余图中不存在增广路,此时便可以直接退出,降低了无效搜索。举个栗子:若结点标号为3的结点个数为0,而标号为4的结点和标号为2的结点都大于 0,那么在搜索至任意一个标号为4的结点时,便无法再继续往下搜索,说明图中就不存在增广路。此时我们可以以将h[1]=n 形式来变相地直接结束搜索
算法实现
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 5010;
const int MAXN_INT = (1 << 29);
struct Edge{
int v, w, nxt;
};
bool isFind;
int head[MAXN];
Edge edge[MAXN];
int dis[MAXN], gap[MAXN];
int n, m, ecnt, aug, maxFlow;
void init(){
ecnt = maxFlow = 0;
memset(gap, 0, sizeof(gap));
memset(dis, 0, sizeof(dis));
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
gap[0] = n;
}
void addEdge(int u, int v, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
void Find(int s){
int dx, augc, minDis;
if(s == n){
isFind = true;
maxFlow += aug;
return;
}
augc = aug;
minDis = n - 1;
for(int i = head[i]; i + 1; i = edge[i].nxt){
if(edge[i].w > 0){
if(dis[s] == dis[edge[i].v] + 1){
aug = min(aug, edge[i].w);
Find(edge[i].v);
if(dis[1] >= n) return;
if(isFind){
dx = i;
break;
}
aug = augc;
}
minDis = min(minDis, dis[edge[i].v]);
}
}
if(!isFind){
gap[dis[s]]--;
if(gap[dis[s]] == 0) dis[1] = n;
dis[s] = minDis + 1;
gap[dis[s]]++;
}else{
edge[dx].w -= aug;
edge[dx ^ 1].w += aug;
}
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
init();
int u, v, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
addEdge(v, u, 0);
}
while(dis[1] < n){
isFind = 0;
aug = MAXN_INT;
Find(1);
}
cout << maxFlow << endl;
}
return 0;
}
时间复杂度
O(V2E)
预流推进算法
预流推进算法是从一个预流出发对活跃顶点沿着允许弧进行流量增广,每次增广称为一次推进。在推进过程中,流一定满足流量限制条件,但一般不满足流量平衡条件, 因此只是一个伪流。此外, 如果一个伪流中, 从每个顶点(除源点 V s 、汇点 V t 外)流出的流量之和总是小于等于流入该顶点的流量之和, 称这样的伪流为预流。因此这类算法被称为预流推进算法。
算法流程
- 首先用一边 BFS 为图中每个顶点一个标号dis[v], 表示该点到v的最短路.
- 将与 S 相连的边设为满流, 并将这时产生的活动结点加入队列Q。
- 选出 Q 的一个活动结点 u 并依次判断残量网咯 G’ 中每条边(u, v), 若 dis[u]=min(dis[v]+1) 则顺着这些边推流, 直到 Q 变成非活动结点(不存在多余流量).
- 如果 u 还是活动结点,则需要对 u 进行重新标号: dis[u]=min(dis[v]+1), 其中边 (u, v) 存在于 G’ 中,然后再将 u 加入队列。
- 重复3, 4两个步骤直到队列 Q 为空。
算法实现
const int size = 501;
const int MAX = 1 << 15;
int graph[size][size];
int label[size]; //标号
bool visited[size];
bool bfs(int st, int ed)
{
memset(label, -1, sizeof(label));
memset(visited, false, sizeof(visited));
label[st] = 0;
visited[st] = true;
vector < int >plist;
plist.push_back(st);
while (plist.size()) {
int p = plist[0];
plist.erase(plist.begin());
for (int i = 0; i < size; i++) {
if (graph[i][p] > 0 && !visited[i]) {
plist.push_back(i);
visited[i] = true;
label[i] = label[p] + 1;
}
}
}
if (label[ed] == -1) {
return false;
}
return true;
}
int inflow[size]; //流入量
int maxFlow()
{
memset(inflow, 0, sizeof(inflow));
//hights
bfs(size - 1, 0); //end point: size - 1, start point: 0
memset(visited, false, sizeof(visited));
//prepare()
vector < int >plist;
for (int i = 0; i < size; i++) {
if (graph[start][i] > 0) {
inflow[i] = graph[start][i];
graph[start][i] -= inflow[i];
graph[i][start] += inflow[i];
if (!visited[i]) {
plist.push_back(i);
visited[i] = true;
}
}
}
while (plist.size()) {
int p = plist[0];
plist.erase(plist.begin());
visited[p] = false;
int minLabel = -1;
for (int i = 0; i < size; i++) {
if (graph[p][i] > 0) {
if (label[p] == label[i] + 1) {
int flow = min(inflow[p], graph[p][i]);
inflow[p] -= flow;
inflow[i] += flow;
graph[p][i] -= flow;
graph[i][p] += flow;
if (!visited[i] && inflow[i] > 0) {
plist.push_back(i);
visited[i] = true;
}
}
}
}
if (inflow[p] > 0 && p != end) {
for (int i = 0; i < size; i++) {
if (graph[p][i] > 0) {
if (minLabel == -1 || minLabel > label[i] + 1) {
minLabel = label[i] + 1;
}
}
}
if (!visited[p] && minLabel != -1 && minLabel < size) //minLabel < size, 这个条件需要加上, 因为经过测试发现有死循环的可能
{
for (int i = 0; i < size; i++) {
if (label[i] + 1 == minLabel && graph[p][i] > 0) {
visited[p] = true;
label[p] = minLabel;
plist.push_back(p);
break;
}
}
}
}
}
return inflow[end];
}
复杂度分析
如果该算法的Q是标准的FIFO队列,则时间复杂度为(n2m),最高标号不会超过n(超过时必无到汇的路径),所以n个点每个最多重新标号n次,两次标号之间m条边每条最多推流一次。如果是优先队列,并且标号最高的点优先的话,我们就得到了最高标号预流推进算法,其时间复杂度仅为n2m−−√.
最小费用最大流
简介
最小费用最大流是解决这么一种问题: 对于图中的每一条边来说, 除了有一个最大容量的属性以外,还有一个费用属性, 即流过这条边的单位流量的花费。求解的问题为在保证从源点到汇点的流量最大的前提下使得花费最少。
求解思想
我们来考虑这么一个问题: 在最短路的一些变形的题目中往往有这种题,每条路不仅仅有一个长度还有一个建设的费用, 最终求从起点到终点在保证路最短的前提下,使得花费的钱最少。当时我们是怎么求解的呢?
首先我们知道,最短路的长度是一定的,但是组成一条最短路的边是不一定的,所以我们在搜索这条最短路的时候只要通过调整待选边的优先级来控制搜索的方向就可以满足上述问题的要求。
这个问题跟我们现在求解的最小费用最大流问题神似啊,只要我们在寻找增广路的时候调整待选边的优先级来控制寻找方向,这个问题就可以解决了啊。我们直到对于一条增广路来说, 花费满足: cost=minFlow∗∑wi(i∈增广路上的边), 实际上这里的优先级就是每条边的长度认为是其单位流量的花费的最短路。
求解算法
基于最大流的三种算法,求解最小费用最大流也具有三种算法,我们来对比一下这三对算法:
最大流 EK 算法: 每次用广搜寻找一条最短的增广路(即包含最少的边),然后沿其增广。
费用流 E’K’ 算法: 每次用spfa计算图的距离标号,然后沿着可行边进行增广。最大流 DINIC 算法: 用广搜获得每个点到源点的距离标号,增广时沿距离标号严格减1的路径增广,直到网络中不再存在这么一条路径,那么重新广搜计算距离标号,如果广搜发现整个源点到汇点已经不连通那么退出算法。
费用流 原始对偶 算法: 用 SPFA 获得每个点到源点的最短路,增广时沿着最短路前进的方向增广, 直到网络中不存在一条路径时重新 SPFA 求最短路, 直到没有一条最短路可以到达汇点为止。最大流 SAP 算法: 与 dinic 一样基于距离标号,不过这里保存的是到汇点的距离标号。并且考虑每次增广对网络的影响,发现增广只会使点的距离标号变大,并且并不会破坏距离标号 dis[u]<=dis[v]+w[u,v] 的性质,只会使得等号不再成立。找不到可行边就是因为没有一个结点v使得dis[u]==dis[v]+w[u,v] 。那么重新使等号成立的方法也很简单,并不需要重新计算整个图的距离标号,只需要调整距离标号:如果从u点开始寻找增广路没有成功,即没有一个v使得dis[u]==dis[v]+w[u,v]那么在所有(v∈V)中找到距离标号最小的一个v,使dis[u]=dis[v]+w[u,v]
即可。
费用流 ZKW 算法: 每次增广,同样不会破坏距离标号 dis[u]<=dis[v]+w[u,v] ,只会使得等号不再成立。并且被破坏的点并没有很多(只有在增广路上的点有可能被破坏)。因此并不需要SPFA来重新计算全部的距离标号。如果某一次寻找可行边组成增广路的尝试进行到点u失败,那么在所有的边$ (v∈V中找到距离标号最小的一个v,使中找到距离标号最小的一个v,使dis[v] == dis[v] + w[u, v]&成立即可。
费用流 E’K’ 算法
思想上面说过了, 就是把最大流 EK 算法里面的 bfs 替换为 SPFA, 改变遍历的优先级来实现:
算法步骤
与 EK 算法相同, 只不过将 bfs 换成 spfa求最短路, 边权为该边的单位流量花费.
如下图所示
算法实现
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1010;
const int MAXM = 1000100;
const int MAXN_INT = (1 << 29);
struct Edge{
int v, w, c, nxt;
};
struct Node{
int id, v;
};
bool vis[MAXN];
Node pre[MAXN];
Edge edge[MAXN];
int n, m, ecnt, sumFlow;
int head[MAXN], dis[MAXN];
void init(){
ecnt = 0;
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int c, int w){
edge[ecnt].v = v;
edge[ecnt].w = w;
edge[ecnt].c = c;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
}
bool SPFA(int s, int t, int n){
queue <int> que;
memset(vis, 0, sizeof(vis));
fill(dis, dis + MAXN, MAXN_INT);
vis[s] = true;
dis[s] = 0;
que.push(s);
while(!que.empty()){
int u =que.front();
que.pop();
vis[u] = false;
for(int i = head[u]; i + 1; i = edge[i].nxt){
int v = edge[i].v;
if(edge[i].c && dis[v] > dis[u] + edge[i].c){
dis[v] = dis[u] + edge[i].c;
pre[v].v = u;
pre[v].id = i;
if(!vis[v]){
que.push(v);
vis[v] = true;
}
}
}
}
if(dis[t] == MAXN_INT) return false;
return true;
}
int MCMF(int s, int t, int n){
int flow = 0;
int minCost = 0;
while(SPFA(s, t, n)){
int minFlow = MAXN_INT + 1;
for(int i = t; i != s; i = pre[i].v){
minFlow = min(minFlow, edge[pre[i].id].w);
}
for(int i = t; i != s; i = pre[i].v){
edge[pre[i].id].w -= minFlow;
edge[pre[i].id ^ 1].w += minFlow;
}
minCost += dis[t] * minFlow;
}
sumFlow = flow;
return minCost;
}
int main(){
while(scanf("%d%d", &n, &m) != EOF){
int u, v, c, w;
for(int i = 0; i < m; i++){
scanf("%d%d%d%d", &u, &v, &c, &w);
addEdge(u, v, c, w);
addEdge(v, u, -c, 0);
}
int ans = MCMF(1, n, n);
printf("%d\n", ans);
}
return 0;
}
第二部分转自:(https://blog.andrewei.info/2016/04/11/network-flows/)