• (菜鸟都能看懂的)网络最大流最小割,Ford-Fulkerson及Dinic详解


    关于网络流:

      1.定义

      个人理解网络流的意思便是由一条条水管以及一个源点S一个汇点T和一些节点组成的一张图,现在要从S点流水到T点,问怎么流才能让流到T的流量最大。边权表示的是这条水管的最大流量,假设一条水管的边权是4,那么如果往这个水管里流5那么自然就会炸掉。 

      

      关于网络流一些文字上的概念,和一张图,这张图的最大流显然是3。

      增广路:从s到t的一条简单路径,且每一条边的容量都大于0。 

      流(flow):每条边对应其流量的集合。

       可行流:从s到t的一个流,且每条边的流量不超过其容量限制。

      最大流:流量最大的可行流。

      那么怎么找最大流呢,不断找增广路,直到找不到为止?    

      这个算法很明显是错误的,用刚刚那个图就可以说明。盲目地找一条增广路,显然是不够“聪明”的。 但我们似乎没办法让它变得更“聪明”。那我们不妨给他一个”反悔“的就会。

      假设有有一条路径从u->v那么我们就建一条v->u的有向边,称之为反向边,他的边权是0。为什么是0?因为刚开始还没水流从u->v流过,所以没有水可以反悔,假若从u->v流过x的水,那么v->u的边权就是x,因为他可以反悔x的水。

                             

      这张图就说明了他的反向边地优越性两条流向撞还是两条流!在做的各位可能跟我刚开始一样起了疑问,两条流撞在一起万一水管小不会爆掉吗?嗯,其实他指的是流量,一道水流过这条水管后,他还会流到别的水管,难道流过一次水管就不能流了吗?我们考虑的是流量,这就要追究的问题,他问的不是一堆水从S点流向其他管子里,问流到T的最多是多少,而是流量最多是多少。

      那么如果以这个想法,之前那么想法显然就是对的了,这种算法叫做Ford-Fulkerson算法,但是如果单纯的这么做显然时间复杂度不允许,算一算他的复杂度是O(maxflow),他的时间复杂度由边权决定。但是还是得学。我们把它简称为FF算法。

      FF算法的流程:

      1.建出带反向边的网络。

      2.不断Dfs找增广路,然后沿着增广路流。

      3.直到找不到增广路。

      完美

      代码实现:

      1.首先初始化:

    const int oo=0x7fffffff;  //可爱的无限大
    struct e{           //边
        int cap,to,from;     //cap是边权,to是到达的点,from是从哪里来
        int next,rev;       //rev是反向边的下标。
    }edge[200005];
    int head[200005];
    bool vis[200005];      //记录每个点访问了没有。
    int n,m,s,f;         //n是点个数,m是边个数,s是起点,f是终点
    int cur,ans;         //ans自然就是答案

      2.用链式前向星存边:

    void Scanf(int x,int y,int z){  //x是来自何方,y是到何方,z是权值
        cur++;              //大写的Scanf格外诱人
        edge[cur].cap=z;
        edge[cur].to=y;
        edge[cur].from=x;
        edge[cur].next=head[x];
        head[x]=cur;
        edge[cur].rev=cur+1;    //指向他的反向边
        cur++;
        edge[cur].cap=0;        //反向边
        edge[cur].to=x;
        edge[cur].from=y;
        edge[cur].next=head[y];
        head[y]=cur;
        edge[cur].rev=cur-1;    //指向他的反向边
    }

      3.接着是核心部分:

    int Search_flow(int node,int flow){//找增广路用的DFS
        if(node==f||flow==0)return flow;//如果水都流没了或者当前搜索到的点已经是终点那么自然就返回流量。
        vis[node]=true;          //标记该点走过
        int tre=0;   //记录流量
        for(int p=head[node];p;p=edge[p].next){
            if(!vis[edge[p].to]&&edge[p].cap){//如果没访问过而且边权大于0
                int fuck=Search_flow(edge[p].to,min(flow,edge[p].cap));//fuck不要介意,就接着找增广路。
                flow-=fuck;            //流量减掉已经流掉的
                tre+=fuck;           //加上流过的水量
                edge[p].cap-=fuck;      //减掉流过的
                edge[edge[p].rev].cap+=fuck;//给他反悔的机会反向边加上流掉的量
            }
        }
        vis[node]=false;//将此点还原
        return tre;   //返回流量
    }
    int Search_ans(){
        int flow=0;
        while(1){
            for(int i=1;i<=n;i++)vis[i]=false;  //清0重要
            int new_flow=Search_flow(s,oo);    //一直找增广路
            if(new_flow>0)flow+=new_flow;      //要是找得到那么最大流加上这个数
            else break;        //否则说明没路可走退出
        }
        return flow;
    }

      完整代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int oo=0x7fffffff;
    struct e{
        int cap,to,from;
        int next,rev;
    }edge[200005];
    int head[200005];
    bool vis[200005];
    int n,m,s,f;
    int cur,ans;
    void Scanf(int x,int y,int z){
        cur++;
        edge[cur].cap=z;
        edge[cur].to=y;
        edge[cur].from=x;
        edge[cur].next=head[x];
        head[x]=cur;
        edge[cur].rev=cur+1;
        cur++;
        edge[cur].cap=0;
        edge[cur].to=x;
        edge[cur].from=y;
        edge[cur].next=head[y];
        head[y]=cur;
        edge[cur].rev=cur-1;
    }
    int Search_flow(int node,int flow){
        if(node==f||flow==0)return flow;
        vis[node]=true;
        int tre=0;
        for(int p=head[node];p;p=edge[p].next){
            if(!vis[edge[p].to]&&edge[p].cap){
                int fuck=Search_flow(edge[p].to,min(flow,edge[p].cap));
                flow-=fuck;
                tre+=fuck;
                edge[p].cap-=fuck;
                edge[edge[p].rev].cap+=fuck;
            }
        }
        vis[node]=false;
        return tre;
    }
    int Search_ans(){
        int flow=0;
        while(1){
            for(int i=1;i<=n;i++)vis[i]=false;
            int new_flow=Search_flow(s,oo);
            if(new_flow>0)flow+=new_flow;
            else break;
        }
        return flow;
    }
    int main(){
        cin>>n>>m>>s>>f;
        for(int i=1;i<=m;i++){
            int ui,vi,si;
            cin>>ui>>vi>>si;
            Scanf(ui,vi,si);
        }
        int ans=Search_ans();
        cout<<ans<<endl;
        return 0;
    }
    View Code

      


      然后呢我们来讲讲更优的算法名字叫做Dinic算法。开一个数组level表示从源点到达当前点的最小步数。

      Dinic的核心思想:每次Dfs都只找经过边数最少的增广路。 具体实现方法是:在Dfs前,先用容量>0的边进行一次Bfs,定出每个点的level。在之后的Dfs中,只允许使用level[v]==level[u]+1的边u->v。 这样Dfs时,我们相当于在一个DAG上增广,所有增广路长度都是level[t]的。增广完所有长度为level[t]的增广路后停止。这样就会节省时间。

        Tip:Dinic的时间复杂度是O(nm²)。某种不成文的规定,凡是正解是最大流的题目,不允许卡Dinic.QWQ

      Dinic算法流程:

      用有容量的边进行Bfs,对网络中的点分层。

      只使用符合分层情况的边,进行一次Dfs(多路增广),增广完所有长度为level[t]的增广路。

      重复上面两个过程,直到s通过Bfs无法到达t。

      Dinic的当前弧优化:记录上次Dfs到这个点时,扫到哪一条边。下次再到这个点时,直接从该边开始,避免对一条边进行无用的检查。

      代码核心:

    int BFS(){            //BFS找到level
        for(int i=1;i<=n;i++){   //初始化
            level[i]=0;    
            nhead[i]=head[i];
        }
        queue<int> q;    //BFS的队列首先从起点s开始找
        q.push(s);
        level[s]=1;      //自己到自己设置为1
        while(!q.empty()){
            int x=q.front();
            q.pop();
            for(int p=head[x];p;p=e[p].next)
                if(e[p].cap&&!level[e[p].to]){  //如果当前边的边权>0并且没有标记过就标记
                    q.push(e[p].to);
                    level[e[p].to]=level[x]+1;    
                }
        }
        return level[t];  //返回终点的值如果
    }
    int DFS(int root,int flow){
        if(root==t||!flow)return flow;        //与FF中同理同理
        int tre=0;
        for(int p=nhead[root];p;p=e[p].next)        //当前弧优化
            if(e[p].cap&&level[root]+1==level[e[p].to]){
                int new_flow=DFS(e[p].to,min(flow,e[p].cap));
                flow-=new_flow;          //不解释,与FF同理
                tre+=new_flow;
                e[p].cap-=new_flow;
                e[e[p].rev].cap+=new_flow;
                if(!flow)break;        //如果已经流完了那么就跳出循环
                nhead[root]=p;      //当前弧优化
            }
        return tre;    
    }
    int Dinci(){
        int max_flow=0;
        while(BFS())max_flow+=DFS(s,oo);  //如果返回终点的值是0说明已经无法到达终点了,否则找增广路
        return max_flow;   //因此返回最大流输出
    }

      完整代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,s,t;
    const int oo=0x7fffffff;
    struct edge{
        int from,to,cap;
        int rev,next;
    }e[200005];
    int head[200005];
    int nhead[200005];
    int cur;
    int level[200005];
    void Scanf(int x,int y,int z){
        cur++;
        e[cur].from=x;
        e[cur].to=y;
        e[cur].cap=z;
        e[cur].next=head[x];
        head[x]=cur;
        e[cur].rev=cur+1;
        cur++;
        e[cur].from=y;
        e[cur].to=x;
        e[cur].cap=0;
        e[cur].next=head[y];
        head[y]=cur;
        e[cur].rev=cur-1;
    }
    int BFS(){
        for(int i=1;i<=n;i++){
            level[i]=0;    
            nhead[i]=head[i];
        }
        queue<int> q;
        q.push(s);
        level[s]=1;
        while(!q.empty()){
            int x=q.front();
            q.pop();
            for(int p=head[x];p;p=e[p].next)
                if(e[p].cap&&!level[e[p].to]){
                    q.push(e[p].to);
                    level[e[p].to]=level[x]+1;
                }
        }
        return level[t];
    }
    int DFS(int root,int flow){
        if(root==t||!flow)return flow;
        int tre=0;
        for(int p=nhead[root];p;p=e[p].next)
            if(e[p].cap&&level[root]+1==level[e[p].to]){
                int new_flow=DFS(e[p].to,min(flow,e[p].cap));
                flow-=new_flow;
                tre+=new_flow;
                e[p].cap-=new_flow;
                e[e[p].rev].cap+=new_flow;
                if(!flow)break;
                nhead[root]=p;
            }
        return tre;    
    }
    int Dinci(){
        int max_flow=0;
        while(BFS())max_flow+=DFS(s,oo);
        return max_flow;
    }
    int main(){
        scanf("%d%d%d%d",&n,&m,&s,&t);
        for(int i=1;i<=m;i++){
            int ui,vi,wi;
            scanf("%d%d%d",&ui,&vi,&wi);
            Scanf(ui,vi,wi);
        }
        printf("%d",Dinci());
        return 0;
    }
    View Code

      还有一个问题,没错就是最小割,你只要记住最大流=最小割,也就是叫你求最小割时你就求最大流就行了,网络流这个东西,最主要的是建图,模板的话每天打一两次就好了,提醒一下建图可以运用:拆点,超级源点,超级汇点等思想。

      Tip:超级源点,超级汇点指在题目中没有确切给出源点和汇点,然后通过自己的构思,创造出源点和汇点。

      那么简单讲讲最小割,就是说在一张最大流的图中求割掉一些边使得源点到不了汇点(或者说是最大流为0)求出割掉的边的边权和最小。

      这就是最小割,最小割也有变形,比如说题目说要割点不割边,像这种情况就可以利用拆点,也就是将一个点拆分成两个点,割掉两点中的线好比割掉了这个点,然后确定这条边的边权,再跑一遍最大流即可。

      不管是最大流还是最小割最关键的东西就是建图。

      转载的话请加上原文网址,哦

      谢谢您的观看,记得点个赞再走哦,有什么写得不好的多多指教。

      

      

    作者:BiuBiu_Miku

    -----------------------------------------------

    个性签名:天生我材必有用,千金散尽还复来!

    如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!

    万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!

  • 相关阅读:
    【刷题】LOJ 6009 「网络流 24 题」软件补丁
    lab 项目
    js内的时间戳指的是当前时间到1970年1月1日00:00:00 UTC对应的毫秒数,和 unix时间戳是对应的秒数,差了1000倍
    js 原生: 身份证脱敏、唯一随机字符串uuid、对于高 index 元素的隐藏与显示
    diy 滚动条 样式 ---- 核心代码
    PC_后台管理系统
    三端兼容项目
    阿里小程序
    到位App_jQuery_art-template
    一步一步 copy163: 网易严选 ---- vue-cli
  • 原文地址:https://www.cnblogs.com/BiuBiu-Miku/p/12252262.html
Copyright © 2020-2023  润新知