網絡流


網絡流

 

網絡流的概念

 

    在一個有向圖上選擇一個源點,一個匯點,每一條邊上都有一個流量上限(以下稱為容量),

   即經過這條邊的流量不能超過這個上界,同時,除源點和匯點外,所有點的入流和出流都相等

   而源點只有流出的流,匯點只有匯入的流。這樣的圖叫做網絡流

 

網絡流的相關定義

 

  •   源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點

  •   匯點:另一個點也很特殊,只進不出,叫做匯點

  •   容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[i,j]表示,流量則通常是f[i,j].

  通常可以把這些邊想象成道路,流量就是這條道路的車流量,容量就是道路可承受的最大的車流量

  很顯然的,流量<=容量。而對於每個不是源點和匯點的點來說,可以類比的想象成沒有存儲功能的貨物的中轉站,

  所有“進入”他們的流量和等於所有從他本身“出去”的流量。

  •   最大流:把源點比作工廠的話,問題就是求從工廠最大可以發出多少貨物,是不至於超過道路的容量限制,也就是,最大流

 

  Ford-Fulkerson 增廣路算法

     該方法通過尋找增廣路來更新最大流,有 EK,dinic,SAP,ISAP 主流算法。

      求解最大流之前,我們先認識一些概念。

   最常用的就是dinic(據說隔壁treaker只會這一種)。

   但是費用流需要用到EK算法,so,要學會EK算法,dinic;

   增廣路:在圖中若一條從源點到匯點的路徑上所有邊的 剩余容量都大於 0 (注意是大於不是大於等於),這條路被稱為增廣路。

   

   EK算法O(nm2)

 

   求解思路:

   從圖中找一條增廣路,然后增廣,怎么找?

   1.從源點開始bfs,找到到匯點的一條路徑,並記錄這條路徑上所有邊剩余流量最小值,因為要找增廣

   路,所以我們在bfs時要判斷一下邊的剩余容量是否為0,記得用一個pre數組記錄下路徑。

   2.找到路徑后,對其進行增廣(代碼里的up函數),增廣就是把這條路徑的每條邊都減去這

   些邊中剩余流量的最小值(bfs時記錄),反向邊加上這個最小值(關於方向邊下面再解釋)。

   3.一直找增廣路增廣,直到不能增廣為止(找不到增廣路)。

   可以看下面這張圖。

   感謝SYCstudio的圖

   感謝SYCstudio的圖(本人較菜不會畫)。

   上面我們提到了反向邊,下面我們解釋下為什么要建反向邊。(放圖,簡單圖我還是可以的現學的

   

   像我們上面這張圖,因為我們bfs時不能確定第一次走哪條邊,要是你像bmf一樣運氣不好,

   如果bfs第一次找到的增廣路是1→3→2→1的話,我們最后求得的最大流就是1.

   但是很明顯這張圖最大流是2,所以我們發現第一次找的增廣路不是最優的,這時候你就涼了。

   那我們怎么解決呢反向邊,反向邊其實就是一個反悔的機會,也就是讓流過的流量流回去。

   如果還不明白的話還學什么網絡流,下面模擬一下這個過程。

   先說一下反向邊的一些東西,反向邊初始化為0,當正向邊剩余流量減少的是時候,

   反向邊流量增加同樣的值,因為我們可以反悔的流量等於已經從這條邊正向流過的流量。

   下面看一下我們是如何通過反向邊反悔的又要做圖qaq

   因為我不會畫反向邊,所以我們假設‘1/0’左邊那個數字表示正向變邊權,右邊是反向邊。

   下面這張圖就是建完邊后的圖。

   

   如果我們第一次找到的增廣路是1→3→2→1的話,總流量+1=1,圖就變成了

   

   我們發現我們還可以找到增廣路1→2→3→4,總流量+1=2,圖變成

   

   然后發現,找不到增廣路了,程序結束不用作圖了,我們發現

   再找增廣路的過程中3→2這條邊正向一次,反向一次,相當於流量為0.

   這就是反向邊的作用。

   另外提供一種小技巧,使用鄰接表建圖的話,可以邊的編號從2開始建,我們知道

   2^1=3,3^1=2……

   這樣的話我們可以通過異或得到反向邊的編號(記得建完正向邊,緊接着就建反向邊),具體看代碼

   時間復雜度為O(nm2)至於為什么,本人很菜不會,另外,一般時間復雜度是遠遠達不到這個值的。

   代碼(EK)

   

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define LL long long
using namespace std;
const int inf=1<<29;
const int N=207;
const int M=5e3+7;
int n,m,s,t,cnt=1;//從編號2開始建邊 
LL maxf;//最大流 
int head[N],vis[N],pre[N];
LL incf[N];
LL v[N][N]; 
struct edge{
    int v,nxt;
    LL w;
}e[M<<1];//因為要建反向邊,所以開二倍空間 
void add_edge(int u,int v,LL w){//存邊 
    cnt++;
    e[cnt].v=v;
    e[cnt].w=w;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}
bool bfs(){
    memset(vis,0,sizeof(vis));
    queue<int>q;
    q.push(s);
    vis[s]=1;
    incf[s]=inf;
    while(q.size()){
        int now=q.front();q.pop();
        for(int i=head[now];i;i=e[i].nxt){
            if(e[i].w==0||vis[e[i].v])continue;
            int to=e[i].v;
            incf[to]=min(incf[now],e[i].w);//記錄路徑最小邊的流量 
            pre[to]=i;//記錄路徑邊的編號 
            q.push(to);
            vis[to]=1;
            if(to==t)return 1;
        }
    }
    return 0;
}
void up(){
    int x=t;
    while(x!=s){
        int i=pre[x];
         e[i].w-=incf[t];
         e[i^1].w+=incf[t];//反向邊加上正向邊減少的流量 
         x=e[i^1].v;
    }
    maxf+=incf[t];
} 
inline int read()
{
    int x = 0 , f = 1;  char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-')  f = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
    return x * f;
}
int main(){
    n = read(); m = read(); s = read(); t = read();
    for(int i=1;i<=m;i++){
        int x,y,z;
        x = read(); y = read(); z = read();
        v[x][y] += z;
    }
    for(int i = 1;i <= n;i ++) for(int j = 1;j <= n;j ++) if(v[i][j]) add_edge(i,j,v[i][j]),add_edge(j,i,0);
    while(bfs())up();
    cout<<maxf<<endl;
}//可能跟你們碼風不同,╮(╯▽╰)╭

 

  Dinic

   會了EK還學dinic有什么用呢,有用,我們來分析下下面這張圖不小心把顏色改了下,懶得再做一張

   

   

   如果你運氣不好的話像bmf一樣,若你每次找到的增廣路都經過了2→3或3→2這條邊的話你又涼了

   所以這時候就用到了我們的Dinic算法。

   Dinic 算法 的過程是這樣的:每次增廣前,我們先用 BFS 來將圖分層。設源點的層數為1

   那么一個點的層數便是它離源點的最近距離。

   層次用數組dep表示。分層圖對於每條邊滿足dep[v]=dep[u]+1。

   我們思考,EK算法每輪可能會遍歷整個圖,但只找出一條增廣路,屬於單路增廣。

   而Dinic屬於多路增廣,時間復雜度O(n2m)

   求解思路:

   1.bfs求出節點的層次,構建分層圖

   2.對於求出的分層圖,用dfs進行多路增廣,由於本人菜,講的不是很明白,我們可以看代碼

   3.當前弧優化 :cur[]數組的應用,如果一條邊已經被增廣過,那么它就沒有可能被增廣第二次。那么,我們下一次進行增廣的時候,

   就可以不必再走那些已經被增廣過的邊。

   

#include<bits/stdc++.h>
#define LL long long
const int N=210;
const int M=5e3+7;
using namespace std;
int n,m,s,t,cnt=1;
LL max_flow;
int dep[N],head[N],cur[N];
struct edge{
    int v,w,nxt;
}e[M<<1];//數組含義與上一篇EK含義一樣,cur[]數組是dinic的一個優化,下面會提到 
void add_edge(int u,int v,int w){
    cnt++;
    e[cnt].v=v;
    e[cnt].w=w;
    e[cnt].nxt=head[u];
    head[u]=cnt;
}

int bfs(){//構建分層圖 
    for(int i=1;i<=n;i++) dep[i]=0;//每個節點層次初始化 
    queue<int>q;
    q.push(s);
    dep[s]=1;//源點初始化為1; 
    while(q.size()){
        int now=q.front();
        q.pop();
        for(int i=head[now];i;i=e[i].nxt){
            int to=e[i].v,val=e[i].w;
            if(val&&(!dep[to])){//構建分層圖的時候要保證邊不為0,如果dep[]已經被更新就不用更新了 
                q.push(to);
                dep[to]=dep[now]+1;
                if(to==t)return 1;//如果到達匯點,進行dfs 
            }
        }
    }
    return 0;
}

int dfs(int u,int flow){
    if(u==t)return flow;
    int rest=flow,k;//rest表示當前這個節點最大允許通過流量 
    for(int i=cur[u];i&&rest;i=e[i].nxt){ 
        cur[u]=i;//當前弧優化
        int to=e[i].v,val=e[i].w;
        if(val&&(dep[to]==dep[u]+1)){
            int k=dfs(to,min(rest,val));//尋找增廣路 
            if(!k)dep[to]=0;//如果在to之后的路徑找不到增廣路,踢出分層圖 
            e[i].w-=k;//回溯時更新邊權 
            e[i^1].w+=k;
            rest-=k;//當前節點最大允許通過流量減去這次通過的流量 
        }
    }
    return flow-rest; 
}

int main(){
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(int i=1;i<=m;i++){
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add_edge(x,y,z);
        add_edge(y,x,0);
    }
    int flow=0;
    while(bfs()){
        for(int i=1;i<=n;i++){
            cur[i]=head[i]; //當前弧初始化 
        }
        while(flow=dfs(s,1<<29))max_flow+=flow;
    }
    cout<<max_flow<<"\n";
}

 最小割

   最小割問題是指:給出一種有向圖(無向圖)和兩個點s,t以及圖中的邊的邊權,

   求一個權值和最小的邊集,使得刪除這些邊之后是s,t不連通。這類問題,一般運用最

   大流等於最小流定理,求出最大流來解決。證明自行百度百科

   附上代碼

   

int bfs(){
    for(int i=1;i<=2*n;i++)dep[i]=0;
    queue<int>q;
    q.push(s);
    dep[s]=1;
    while(q.size()){
        int now=q.front();
        q.pop();
        for(int i=head[now];i;i=e[i].nxt){
            int to=e[i].v,val=e[i].w;
            if(val&&!dep[to]){
                q.push(to);
                dep[to]=dep[now]+1;
                if(to==t)return 1;
            }
        }
    }
    return 0;
}
int dfs(int u,int flow){
    if(u==t)return flow;
    int rest=flow,k;
    for(int i=cur[u];i;i=e[i].nxt){
        cur[u]=i;
        int to=e[i].v,val=e[i].w;
        if(val&&(dep[to]==dep[u]+1)){
            k=dfs(to,min(val,rest));
            if(!k)dep[to]=0;
            e[i].w-=k;
            e[i^1].w+=k;
            rest-=k;
        }
    }
    return flow-rest;
}

 

 

 

費用流

   費用流就是每條邊除了有容量限制外,還有一個給定的單位費用 w(x,y),

   當流過(x,y)這條邊時,要花費 f(x,y)*w(x,y)的費用

   一般求解的問題是最小費用最大流最大費用最大流,

   基於之前提到的Ek算法,把bfs改成spfa即可,

   就是每次先增廣費用最小的流。。。

   

int spfa(){//最小費用最大流
    for(int i=0;i<=n;i++)dis[i]=inf,vis[i]=0;
    queue<int>q;
    q.push(s);
    dis[s]=0;
    vis[s]=1;
    incf[s]=inf;
    while(q.size()){
        int now=q.front();
        vis[now]=0;
        q.pop();
        for(int i=head[now];i;i=e[i].nxt){
            int to=e[i].v,val=e[i].w;
            if(!val)continue;
            if(dis[to]>dis[now]+e[i].f){
                dis[to]=dis[now]+e[i].f;
                incf[to]=min(incf[now],val);
                pre[to]=i;
                if(!vis[to]){
                    vis[to]=1;
                    q.push(to);
                }
            }
        }
    }
    if(dis[t]==inf)return 0;
    return 1;
}
int up(){
    int x=t;
    max_flow+=incf[t];
    min_cost+=dis[t]*incf[t];
    while(x!=s){
        int i=pre[x];
        e[i].w-=incf[t];
        e[i^1].w+=incf[t];
        x=e[i^1].v;
    }
}

 

 

 

   end......

   

   

 


免責聲明!

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



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