(菜鳥都能看懂的)網絡最大流最小割,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)求出割掉的邊的邊權和最小。

  這就是最小割,最小割也有變形,比如說題目說要割點不割邊,像這種情況就可以利用拆點,也就是將一個點拆分成兩個點,割掉兩點中的線好比割掉了這個點,然后確定這條邊的邊權,再跑一遍最大流即可。

  不管是最大流還是最小割最關鍵的東西就是建圖。

  轉載的話請加上原文網址,哦

  謝謝您的觀看,記得點個贊再走哦,有什么寫得不好的多多指教。

 

 

  

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM