网络流是指给定一个有向图,其中有两个特殊的点:源点 s s s(Source)和汇点 t t t(Sink);每条边都有一个指定的流量上限,下文均称之为容量(Capacity),即经过这条边的流量不能超过容量,这样的图被称为网络流图。同时,除了源点和汇点外,所有点的入流和出流都相等,源点只有流出的流,汇点只有流入的流,网络流就是从 s s s 到 t t t 的一个可行流。
定义 c ( u , v ) c(u,v) c(u,v) 表示边 ( u , v ) (u,v) (u,v) 的容量, f ( u , v ) f(u,v) f(u,v) 表示边 ( u , v ) (u,v) (u,v) 的流量。如果满足 0 ≤ f ( u , v ) ≤ c ( u , v ) 0≤f(u,v)≤c(u,v) 0≤f(u,v)≤c(u,v),则称 f ( u , v ) f(u,v) f(u,v) 为边 ( u , v ) (u,v) (u,v) 上的流量。
如果有一组流量满足:源点 s s s 的流出量等于整个网络的流量,汇点 t t t 的流入量等于整个网络的流量,除了任意一个不是 s s s 或 t t t 的点的总流入量等于总流出量。那么整个网络中的流量被称为一个可行流。
在所有可行流中,最大流指其中流量最大的一个流的流量。
1.容量限制
对于任何一条边,都有 0 ≤ f ( u , v ) ≤ c ( u , v ) 0≤f(u,v)≤c(u,v) 0≤f(u,v)≤c(u,v)。
2.斜对称性
对于任何一条边,都有 f ( u , v ) = − f ( v , u ) f(u,v)=−f(v,u) f(u,v)=−f(v,u)。即从 u u u 到 v v v 的流量一定等于从 v v v 到 u u u 的流量的相反数。
3.流守恒性
对于任何一个点 u u u,如果满足 u ≠ s u≠s u=s 并且 u ≠ t u≠t u=t,那么一定有 ∑ f ( u , v ) = 0 ∑f(u,v)=0 ∑f(u,v)=0,即 u u u 到相邻节点的流量之和为 0 0 0。因为 u u u 本身不会制造和消耗流量。
增广路思想
不防依照这个思想先模拟一遍:
如果我们把每条边的的信息用残量 / 容量表示出来,可以得到下图:
假设我们第一次找到的増广路为 1 − 2 − 3 − 4 1−2−3−4 1−2−3−4,那么我们把这条路径上的边的 w ( u , v ) w(u,v) w(u,v) 减去 m i n f ( u , v ) min{f(u,v)} minf(u,v) 即 1 1 1,得到下图:
然后我们发现已经没有増广路了,此时算出来的“最大流”为 1。但是我们可以手动计算一下,这张图的最大流其实是 2。这个最大流的路径为 1 − 2 − 4 1−2−4 1−2−4(流量为 1 1 1)和 1 − 3 − 4 1−3−4 1−3−4(流量为 1 1 1)。
因此,我们可以发现这样的过程是错误的。原因就是増广路在一定意义上是有顺序的,说白了就是没有给它反悔的机会。所以接下来我们要引入反向边的概念。
反向边思想
通过上文的分析我们已经知道,当我们在寻找増广路的时候,找到的并不一定是最优解。如果我们对正向边的 w ( u , v ) w(u,v) w(u,v) 减去 f l o w flow flow 的同时,将对应的反向边的 w ( v , u ) w(v,u) w(v,u) 加上 f l o w flow flow,我们就相当于可以反悔从这条边流过。
那么我们可以建立反向边,初始时每条边的 w ( u , v ) = c ( u , v ) w(u,v)=c(u,v) w(u,v)=c(u,v),它的反向边的 w ( v , u ) = 0 w(v,u)=0 w(v,u)=0(显然反向边不能有流量,因此残量为 0 0 0)。
接下来再看一下上面那个例子,我们只用 w ( u , v ) w(u,v) w(u,v) 来表示每条边(包括反向边)的信息:
接下来开始寻找増广路,假如还是 1 − 2 − 3 − 4 1−2−3−4 1−2−3−4 这条路径。
我们需要把 w ( 1 , 2 ) w(1,2) w(1,2), w ( 2 , 3 ) w(2,3) w(2,3), w ( 3 , 4 ) w(3,4) w(3,4) 减少 1 1 1,同时把反向边的 w ( 2 , 1 ) w(2,1) w(2,1), w ( 3 , 2 ) w(3,2) w(3,2), w ( 4 , 3 ) w(4,3) w(4,3) 增加 1。那么可以得到下图:
继续从 s s s 开始寻找増广路(不需要考虑边的类型),显然可以发现路径 1 − 3 − 2 − 4 1−3−2−4 1−3−2−4,其中 f l o w = 1 flow=1 flow=1。更新边的信息,得到下图:
此时我们发现没有増广路了,为了直观观察这个网络,我们去掉反向边,显然我们求出的最大流为 2
正确性
当我们第二次増广边 ( 2 , 3 ) (2,3) (2,3) 走这条反向边 ( 3 , 2 ) (3,2) (3,2) 时,把 ( 2 , 3 ) (2,3) (2,3) 和 ( 3 , 2 ) (3,2) (3,2) 的流量抵消了,相当于把 ( 2 , 3 ) (2,3) (2,3) 这条正向边的流量给退了回去使得可以不走 ( 2 , 3 ) (2,3) (2,3) 这条边。
如果反向边 ( v , u ) (v,u) (v,u) 的流量不能完全抵消正向边 ( u , v ) (u,v) (u,v),那么意味着从 u u u 开始还可以流一部分流量到 v v v,这样也是允许的。
最大流算法主要有两类,增广路算法和预留推进算法,下面介绍的两种算法都属于增广路算法。
增广路算法都是基于增广路定理(Augmenting Path Theorem):网络达到最大流当且仅当残留网络中没有増广路。
#include
using namespace std;
const int maxn=1e5+7,inf=0x3f3f3f3f;
int n,m,s,t;
bool used[maxn]; //DFS中用到的访问标记
struct node{ //用于表示边的结构体(终点、容量、反向边)
int to,cap,rev;
};
vector<node> v[maxn]; //图的邻接表表示
void add(int from,int to,int cap){ //向图中增加一条从from到to容量为cap的边
v[from].push_back((node){to,cap,v[to].size()});
v[to].push_back((node){from,0,v[from].size()-1});
}
int dfs(int x,int t,int f){ //通过DFS寻找增广路
if(x==t) return f;
used[x]=true;
for(int i=0;i<v[x].size();i++){
node &e=v[x][i];
if(!used[e.to]&&e.cap>0){
int d=dfs(e.to,t,min(f,e.cap));
if(d>0){
e.cap-=d;
v[e.to][e.rev].cap+=d;
return d;
}
}
}
return 0;
}
int Ford_Fulkerson(int s,int t){ //求解从s到t的最大流
int flow=0;
while (1)
{
memset(used,false,sizeof(used));
int f=dfs(s,t,inf);
if(!f) return flow;
flow+=f;
}
}
int main(){
cin>>n>>m>>s>>t;
while(m--){
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
cout<<Ford_Fulkerson(s,t);
return 0;
}
时间复杂度:记最大流的流量为 F F F,那么 F o r k − f u l k e r s o n Fork-fulkerson Fork−fulkerson算法最多进行 F F F次深度优先搜索,所以其复杂度为 O ( F ∣ E ∣ ) O(F|E|) O(F∣E∣)。不过,这是一个很松的上界,达到这种最坏复杂度的情况几乎不存在。所以在多数情况下,即便通过估算得到的复杂度偏高,实际运用当中也还是比较快的。
#include
using namespace std;
const int maxn=1e5+7,inf=0x3f3f3f3f;
int n,m,s,t,level[maxn],iter[maxn]; //level:顶点到源点的距离标号 iter:当前弧,在其之前的边已经没有用了
struct node{
int to,cap,rev;
};
vector<node> v[maxn];
void add(int from,int to,int cap){
v[from].push_back((node){to,cap,v[to].size()});
v[to].push_back((node){from,0,v[from].size()-1});
}
int bfs(int s){ //通过BFS计算从源点出发的距离标号
memset(level,0,sizeof(level));
queue<int> q;
level[s]=1;
q.push(s);
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=0;i<v[x].size();i++){
node e=v[x][i];
if(e.cap&&!level[e.to]){
level[e.to]=level[x]+1;
q.push(e.to);
}
}
}
return level[t];
}
int dfs(int x,int t,int f){ //通过DFS寻找增广路
if(x==t) return f;
for(int &i=iter[x];i<v[x].size();i++){
node &e=v[x][i];
if(e.cap&&level[x]<level[e.to]){
int d=dfs(e.to,t,min(f,e.cap));
if(d){
e.cap-=d;
v[e.to][e.rev].cap+=d;
return d;
}
}
}
return 0;
}
int Dinic(int s,int t){ //求解从s到t的最大流
int flow=0;
while(bfs(s)){
memset(iter,0,sizeof(iter));
int f;
while((f=dfs(s,t,inf))){
flow+=f;
}
}
return flow;
}
int main(){
cin>>n>>m>>s>>t;
while(m--){
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
cout<<Dinic(s,t);
return 0;
}
时间复杂度:每一步构造分层图的复杂度为 O ( ∣ E ∣ ) O(|E|) O(∣E∣),加入当前弧优化后,可以保证对每次分层图进行深度优先搜索复杂度为 O ( ∣ E ∣ ∣ V ∣ ) O(|E||V|) O(∣E∣∣V∣),而每一步完成之后最短增广路的长度都会至少增加1,由于增广路的长度不会超过 ∣ V ∣ − 1 |V|-1 ∣V∣−1,因此最多重复O(|V|)步就可以了,这样总的复杂度就是 O ( ∣ E ∣ ∣ V ∣ O(|E||V| O(∣E∣∣V∣2)。不过,该算法在实际应用中速度非常快,很多时候即便图的规模比较大也没有问题。
不好意思爆粗口了,不过,还真的有这种毒瘤出题人,
【模板】最大流 加强版 / 预流推进
解决这道题就必须要用我上面说的第二种最大流算法:预流推进法。
但是我太懒(蒻)了,现在并不想去写那种算法,就交给你们了QWQ
洛谷P1345奶牛的电信
做这道题之前,先说一个很重要的定理:
最大流最小割定理:最大流等于最小割。
那么,最小割又是什么呢?最小割是要求为了使原点(记为S)和汇点(记为T)不连通,最少要割几条边。
好了,你是不是觉得你可以去做这道题了(其实说的就是我 ),裸的割边,打个Dinic就行了,但有时候我们还是太naive了。
重新读题,会发现这题割的不是边,是点。所以说,我们需要一个割边转割点的小技巧。
我们可以考虑“拆点”,即把一个点拆成两个点,中间连一条边权为1的边。
前一个点作为“入点”,别的点连边连入这里。
后一个点作为“出点”,出去的边从这里出去。
这样,只要我们切断中间那条边,就可以等效于除去这个点,如图:
红色的边边权为1,黑色的边边权为 i n f inf inf。
原点和汇点的内部边权为 i n f inf inf,因为显然这两个点不能删除。
题面给的边删除没意义(因为我们要删点),所以也设为inf(事实上设为1也没问题,因为删除这条边的权值可以理解为删除了一个点)
至此,我们就可以把这道题AC了
c++代码:
#include
using namespace std;
const int maxn=1e5+7,inf=0x3f3f3f3f;
int n,m,s,t,level[maxn],iter[maxn];
struct node{
int to,cap,rev;
};
vector<node> v[maxn];
void add(int from,int to,int cap){
v[from].push_back((node){to,cap,v[to].size()});
v[to].push_back((node){from,0,v[from].size()-1});
}
int bfs(int s){
memset(level,0,sizeof(level));
queue<int> q;
level[s]=1;
q.push(s);
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=0;i<v[x].size();i++){
node e=v[x][i];
if(e.cap&&!level[e.to]){
level[e.to]=level[x]+1;
q.push(e.to);
}
}
}
return level[t];
}
int dfs(int x,int t,int f){
if(x==t) return f;
for(int &i=iter[x];i<v[x].size();i++){
node &e=v[x][i];
if(e.cap&&level[x]<level[e.to]){
int d=dfs(e.to,t,min(f,e.cap));
if(d){
e.cap-=d;
v[e.to][e.rev].cap+=d;
return d;
}
}
}
return 0;
}
int Dinic(int s,int t){
int flow=0;
while(bfs(s)){
memset(iter,0,sizeof(iter));
int f;
while((f=dfs(s,t,inf))){
flow+=f;
}
}
return flow;
}
int main(){
cin>>n>>m>>s>>t;
for(int i=1;i<=n;i++){
if(i==s||i==t){
add(i,i+n,inf);
}
else add(i,i+n,1);
}
while(m--){
int x,y;
cin>>x>>y;
add(x+n,y,inf);
add(y+n,x,inf);
}
cout<<Dinic(s,t);
return 0;
}