其他分享
首页 > 其他分享> > 网络最大流

网络最大流

作者:互联网

最大流是网络中的基本问题,它是基于带权有向图的。
【模板】网络最大流

对于上面的图,可以想象成输水系统。源点为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