网络最大流
作者:互联网
最大流是网络中的基本问题,它是基于带权有向图的。
【模板】网络最大流
对于上面的图,可以想象成输水系统。源点为s(出水口),汇点为t(入水口),每条边都有容量,求如何分配水流才能使t点接受到的流量最大。
那么我们很容易得到以下限制
- 除了源点与汇点,其他所有点流入的流量等于流出的流量。
- 一条路径的流量应该等于该路径所有边的最小容量。
残存网络
当我们给网络中的边分配(可以给某条边加流量,也可以减流量)流量后,所有剩余容量大于0的边构成的图,这些边的容量为当前剩余容量。(就是说一条边要是满了(流量等于容量)就把他去掉)
反向边
反向边是极其重要的思想,当我们为边\(u\rightarrow v\)分配流量w后,同时为边\(v\rightarrow u\)增加w容量,这种反向边可以帮助我们“反悔”。
比如上面这张网络,边上的数字代表剩余容量。
我们选择一次路径\(s\to a\to b\to t\),并添加对应的反向边,此时整张图的流量为1。
第二次选择路径(或者说寻找路径)\(s\to b\to a\to t\),并添加对应反向边,整张图的流量变为2。
这时候发现已经找不到新的路径,并且整张图的流量达到最大。
这次寻找路径时,我们选择了\(b\to a\)这条边,这表示什么意思呢?
原先b点的1流量由a提供,选择\(b\to a\)这条边后,\(a\to b\)的容量恢复为1,既a不在向b提供流量,此时由s向b提供1流量,a的流量将流往他处。
这就是“反悔”操作。
容易发现按照这种方法(增加反向边)寻找路径,就算最后我们的路径到达不了t,也不会使当前的总容量减少。
而且在具有反向边的图中进行的操作在原图中也都是合法的。
增广路径
增广路径为在残存网络中从原点到汇点的有向路径。就是我们按照上述方法得到的路径。
那么是不是当一个残存网络中不存在增广路径时,整张网络达到最大容量呢?
是的,下面简单写一下证明过程。
不存在增广路径说明不能从源点(s)走到汇点(t)。
那么我们把所有点分成两个集合,集合S表示源点能走到的点,集合T表示剩下的点,显然t不在S中。
那么整张图可能是下面这个样子。
该图中没有画出反向边,10/10表示流量=容量=10,及流量已满,故集合S不能到达集合T(在残存网络中)。
那么有没有这样一种可能,存在一条从T到S的边,且该边有流量。
如下图。
如果这样一条边被分配了流量,那么在残存网络中会存在一条对应的有剩余容量的反向边,故该点在残存网络中为可达点。
所以若存在从T到S的边,那么改变的流量必为0。
以上的到的结论都是在残存网络中不存在增广路径的前提下,并且从图中我们可以很直观的看出此时的流量为30,而且已经无法在增大,
因为连接这两个集合的“关键边”都已经满了。
这同时有引出了应一个概念,最小割
说通俗一点,最小割就是为了不让水从s流向t,怎么破环水沟代价最小。
最大流最小割定理:源点s和汇点t之间的最小割等于s和t之间的最大流。
那么求解最大流问题变成了寻找增广路径,下面给出几种常用算法。
Ford-Fulkerson
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=210;
const int M=2*(5e3+10);
int cnt=1,head[N];
struct{
int to,next;
ll w;
}e[M];
void add(int u,int v,int w){
e[++cnt].to=v;
e[cnt].next=head[u];
e[cnt].w=w;
head[u]=cnt;
}
bool vis[N];
int n,m,s,t;
ll dfs(int u,ll f){
if(u==t)return f;//到达汇点直接返回
vis[u]=1;
int v;
ll w;
for(int i=head[u];i;i=e[i].next){
v=e[i].to;w=e[i].w;
ll res=0;
if(!vis[v]&&w&&(res=dfs(v,min(f,w)))>0){//找到路径修改相关的边
e[i].w-=res;
e[i^1].w+=res;
return res;//找到一条路径后及时返回
}
}
return 0;
}
ll FF(){
ll ans=0;
ll res=0;
do{
memset(vis,0,sizeof(vis));
res=dfs(s,1e10);
ans+=res;
}while(res!=0);//res==0表示没有增广路径了
return ans;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
int u,v,w;
for(int i=1;i<=m;++i){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);//正向边
add(v,u,0);//反向边
}
printf("%lld",FF());
return 0;
}
该算法就是不断使用dfs寻找路径,找到一条路径后返回并修改相关的边与反向边。
然而一次dfs只能找到一条路径,而且复杂度及其不靠谱。
Edmonds-Karp
#include<bits/stdc++.h>
using namespace std;
const int N=210;
const int M=5e3+10;
struct{
int u,to,next,w;
}e[2*M];
int head[N],cnt=1;
void add(int u,int v,int w){
e[++cnt].to=v;
e[cnt].u=u;
e[cnt].next=head[u];
e[cnt].w=w;
head[u]=cnt;
}
int n,m,s,t;
int a[N],p[N];
long long ek(){
long long ans=0;
while(1){
queue<int>q;
memset(a,0,sizeof(a));
a[s]=INT_MAX;q.push(s);
while(q.size()){//开始bfs
int u=q.front();q.pop();
int v,w;
for(int i=head[u];i;i=e[i].next){
v=e[i].to;w=e[i].w;
if(!a[v]&&w){
p[v]=i;//记录路径
a[v]=min(a[u],w);
q.push(v);
}
}
if(a[t])break;
}
if(!a[t])break;//没有增广路径
for(int i=t;i!=s;i=e[p[i]].u){
e[p[i]].w-=a[t];
e[p[i]^1].w+=a[t];
}
ans+=a[t];
}
return ans;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
int u,v,w;
for(int i=1;i<=m;++i){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,0);
}
printf("%lld",ek());
return 0;
}
该算法使用bfs寻找路径,时间复杂度为\(O(VE^2)\)
Dinic
该算法的理论复杂度为\(O(V^2E)\),上面的算法遍历一次只能找的一条路径,而该算法遍历一次可以找多条增广路径。
如果一个点分配流量给一条路径后还有剩余流量,那么该点可以继续寻找其他可行路径。
但是如果不加限制的寻找路径很可能会在图里面转圈圈导致超时。
所以先使用bfs分层,在用dfs寻找路径。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=210;
const int M=2*(5e3+10);
int dep[N];
int head[N],cnt=1;
struct{
int v,next,w;
}e[M];
int n,m,s,t;
void add(int u,int v,int w){
e[++cnt].next=head[u];
e[cnt].v=v;
e[cnt].w=w;
head[u]=cnt;
}
bool bfs(){//先用bfs分层,防止dfs的时候死循环
memset(dep,0,sizeof(dep));
queue<int>q;
q.push(s);
dep[s]=1;
while(q.size()){
int v,w,u=q.front();q.pop();
for(int i=head[u];i;i=e[i].next){
v=e[i].v;w=e[i].w;
if(dep[v]||!w)continue;
dep[v]=dep[u]+1;
q.push(v);
}
}
return dep[t];//dep[t]的值可以直接表示是否存在增广路
}
ll dfs(int u,ll in){
if(u==t)return in;
ll out=0,v,w;
for(int i=head[u];i;i=e[i].next){//找到路径后不用返回,继续寻找其他路径
v=e[i].v;w=e[i].w;
if(dep[v]!=dep[u]+1||!w)continue;
int res=dfs(v,min(w*1ll,in));
in-=res;
out+=res;
e[i].w-=res;
e[i^1].w+=res;
}
if(out==0)dep[u]=0;//out=0表示该点的流量不能到大汇点,下次dfs到它时不再继续寻找
return out;
}
ll din(){
ll ans=0;
while(bfs())
ans+=dfs(s,1e10);
return ans;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
int u,v,w;
for(int i=1;i<=m;++i){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,0);
}
printf("%lld",din());
return 0;
}
分层只是为了dfs不陷入死循环,并不会影响答案的正确性,因为一个图若存在增广路,那么也一定能被bfs找到。
标签:cnt,最大,int,res,ll,路径,网络,流量 来源: https://www.cnblogs.com/hetailang/p/16556981.html