[網絡流]一篇文章搞定網絡流

0.網絡流的相關概念

  • 網絡流(network-flows):求源點到匯點間的最大水流量
    在有向圖G=(V,E)中:

    • 容量限制:f[u,v] (邊初始流量) <= c[u,v] (邊最大容量)
    • 反對稱性:f[u,v] = - f[v,u]
    • 流量平衡:結點的流量和等於流出該結點的流量和

    滿足上述三個性質就是一個合法的網絡流了。

  • 可行流:

    • 每條弧(u,v)上給定一個實數f(u,v),滿足:有0<= f(u,v) <= c(u,v)(容量),則稱爲弧(u,v)上的流量。
    • 源點流出量等於整個網絡的流量;匯點流入量等於整個網絡的流量;中間點總流入量等於總流出量

最大流相關算法有兩種解決思想, 一種是增廣路算法思想, 另一種是預流推進算法思想。
增廣路算法(Ford-Fulkerson思想):關鍵在於如何找出增廣路徑,如何更新流量。
預流推進算法思想:

1. 基於增廣路算法思想的最大流算法

增廣路徑就是:找出一條流量不滿,未達到容量上限。然後通過bfs或dfs算法來找出增廣路徑來更新流量。
直接來樣例:(參考網上的博客以及百度文庫,文章後面已經標明)
在剛開始存邊的時候,我們會初始化正向邊爲其初始流量,反向邊初始爲0(反向邊是原圖沒有的,模擬的)。在增廣路徑更新流量的過程中,正向邊流量減去路徑最小流量,反向邊流量添加路徑最小流量。爲什麼要添加反向邊,爲了後面我們不流正向邊或把正向邊的一些流量分配給其他邊。
在這裏插入圖片描述
☞ 第一條增廣路徑: 1→2→3→5,路徑最小流量爲2,整個網絡的最大流量Maxflow =2,然後更新路徑上每條邊的流量。然後2→3邊容量爲0了。
在這裏插入圖片描述
☞ 第二條增廣路徑: 1→2→4→5,路徑最小流量爲2,整個網絡的最大流量Maxflow =2+2,然後更新路徑上每條邊的流量。然後1→2邊的容量爲0了。
在這裏插入圖片描述
☞ 第三條增廣路徑: 1→3→4→5,路徑最小流量爲1,正向邊回退並分配1流量給2→4邊,我們默認由1→5邊減少了1流量,整個網絡的最大流量Maxflow =2+2+1,然後更新路徑上每條邊的流量。
在這裏插入圖片描述
☞ 第四條增廣路徑: 1→3→5,路徑最小流量爲2,整個網絡的最大流量Maxflow =2+2+1+2,然後更新路徑上每條邊的流量。再然後3→5邊的容量爲0了。
在這裏插入圖片描述
到這裏我們基於搜索的所有增廣路徑全部找完,也就是找不到一條路徑容量不爲0的增廣路徑。
在這裏插入圖片描述

1.1 Edmonds-Karp算法(EK算法,SAP)

Edmonds-Karp算法:從源點開始做bfs,不斷地修改delta量,直到到匯點與源點不連通,也就是找不到增廣路徑爲止。
每進行一次增廣需要的時間複雜度爲 bfs 的複雜度 + 更新殘餘網絡的複雜度, 大約爲 O(m)(m爲圖中的邊的數目), 需要進行多少次增廣呢, 假設每次增廣只增加1, 則需要增廣 nW 次(n爲圖中頂點的數目, W爲圖中邊上的最大容量), .
EK算法時間複雜度:O(n * m * m)

  • 基於鄰接矩陣實現的EK算法:
#include<stdio.h>
#include<queue>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 300;///鄰接矩陣適合點比較小的圖
const int MAX = ((1<<31)-1);
int n;
int pre[maxn];///存儲當前邊的起點
bool vis[maxn];//標記訪問點
int mp[maxn][maxn];///記錄每條邊的流量
bool bfs(int s,int t){
   queue<int>que;
   memset(vis,0,sizeof(vis));
   memset(pre,-1,sizeof(pre));
   pre[s] = s;
   vis[s] = true;
   que.push(s);

   while(!que.empty()){
      int u = que.front();
      que.pop();
      for(int i=1; i<=n; i++){
         if(mp[u][i]&&!vis[i]){
            pre[i] = u;
            vis[i] = true;///標記節點
            if(i==t) return true;///如果到達匯點,一條增廣路徑就找到了。
            que.push(i);
         }
      }
   }
   return false;
}
int EK(int s,int t){
  int ans = 0;
  while(bfs(s,t)){
      int d = MAX;
      for(int i = t;i != s; i = pre[i])
        d = min(d,mp[pre[i]][i]);///找出當前增廣路徑中最小的流量

       for(int i = t;i != s; i = pre[i]){
           mp[pre[i]][i] -= d;///正向邊流量量更新
           mp[i][pre[i]] += d;///反向邊流量更新
       }
       ans += d;
  }
  return ans;
}
int main(){
   int m,s,t;
   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);
      mp[x][y]+=z;
   }
   int ans = EK(s,t);
   printf("%d\n",ans);
}

  • 基於鄰接表實現的EK算法:
#include<stdio.h>
#include<queue>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 1e5+10;
const int MAX = 0x3f3f3f3f;
int n,cnt,m;
bool vis[maxn];
int head[maxn];

struct Edge{
    int v;
    int w;
    int nxt;
}edge[maxn];

void addEdge(int u,int v,int w){
   edge[cnt].v = v;///cnt從0開始,
   edge[cnt].w = w;
   edge[cnt].nxt = head[u];
   head[u] = cnt++;
}
struct Node{
   int v;///存儲當前邊的起點
   int id;///邊的id
}pre[maxn];

void init(){
   cnt = 0;
   memset(edge,0,sizeof(edge));
   memset(head,-1,sizeof(head));
}

bool bfs(int s,int t){
   queue<int>que;
   memset(vis,0,sizeof(vis));
   memset(pre,-1,sizeof(pre));
   pre[s].v = s;
   vis[s] = true;
   que.push(s);

   while(!que.empty()){
      int u = que.front();
      que.pop();
      for(int i = head[u]; i != -1; i=edge[i].nxt){
         int v = edge[i].v;
         if(!vis[v]&&edge[i].w){
            pre[v].v = u;
            pre[v].id = i;
            vis[v] = true;
            if(v==t) return true;///到達匯點
            que.push(v);
         }
      }
   }
   return false;
}
int EK(int s,int t){
  int ans = 0;
  while(bfs(s,t)){
      int d = MAX;
      for(int i = t;i != s; i = pre[i].v)
        d = min(d,edge[pre[i].id].w);

       for(int i = t;i != s; i = pre[i].v){
          edge[pre[i].id].w -= d;
          edge[pre[i].id^1].w += d;
       }
       ans += d;
  }
  return ans;
}
int main(){
   int m,s,t;
   init();
   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);
      addEdge(x,y,z);
      addEdge(y,x,0);
   }
   int ans = EK(s,t);
   printf("%d\n",ans);
   return 0;
}

1.2 Dinic 算法

Dinic算法的主要思想:

dinic在找增廣路的時候也是找的最短增廣路, 與 EK 算法不同的是dinic 算法並不是每次 bfs 只找一個增廣路, 他會首先通過一次 bfs 爲所有點添加一個標號, 構成一個層次圖, 然後在層次圖中通過dfs來尋找增廣路進行更新。

看樣例吧!!!
☞ 第一次調用bfs構建層次圖:
在這裏插入圖片描述
第一次構建的層次圖的第一條增廣路徑:路徑上最小流量爲3,整個網絡的最大流量Maxflow =3,然後更新路徑上每條邊的流量。再然後2→4邊的容量爲0了。
在這裏插入圖片描述
第一次構建的層次圖的第二條增廣路徑:路徑上最小流量爲4,整個網絡的最大流量Maxflow =3+4,然後更新路徑上每條邊的流量。再然後3→5邊的容量爲0了。
在這裏插入圖片描述
到這裏我們就發現找不到到匯點的增廣路徑了,這時候我們不需要再重建層次圖找增廣路徑了。因爲容量不達到上限的路徑可以增廣了。注意:先bfs層次後dfs,我們就可以快點找到離匯點較近的邊,就能快速找到我們最大流量。這時我們在會看,是不是發現Dinic比EK效率高了很多。
在這裏插入圖片描述

#include<stdio.h>
#include<queue>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 200010;
const int INF = 0x3f3f3f3f;
int head[maxn];
int dis[maxn],cur[maxn];
int n,m,s,t,cnt;

struct Edge{
    int v;
    int w;
    int nxt;
}edge[maxn];
void init(){
   cnt = 0;
   memset(edge,0,sizeof(edge));
   memset(head,-1,sizeof(head));
}
void addEdge(int u,int v,int w){
   edge[cnt].v = v;
   edge[cnt].w = w;
   edge[cnt].nxt = head[u];
   head[u] = cnt++;
}
bool bfs(int s){
   queue<int>que;
   memset(dis,-1,sizeof(dis));
   dis[s] = 0;
   que.push(s);

   while(!que.empty()){
      int u = que.front();
      que.pop();
      for(int i = head[u]; i != -1; i=edge[i].nxt){
         int v = edge[i].v;
         if(dis[v]==-1&&edge[i].w){
            dis[v] = dis[u]+1;///建立層次編號
            que.push(v);
         }
      }
   }
   return dis[t]!=-1;
}
int dfs(int u,int flow){
    if(u==t) return flow;

    int detla = flow;///這個detla主要爲了不直接傳入參數flow,我們也可以直接傳入
    for(int i = cur[u]; i!=-1;  i=edge[i].nxt){
        cur[u] = edge[i].nxt;///
        int v = edge[i].v;
        if(dis[v]==dis[u]+1&&edge[i].w>0){
            int d = dfs(v,min(detla,edge[i].w));///找出路徑上權值最小的邊
            edge[i].w -= d;
            edge[i^1].w += d;

            detla -= d;
            if(detla==0) break;
            /*直接傳入參數可以這樣,直接返回
            當前路徑最小流量
            如果這裏沒有返回,只能最後返回0了
            if(d>0){
            edge[i].w -= d;
            edge[i^1].w += d;
            return d;
           }
           */
        }
    }
    return flow - detla;
}
int dinic(){
   int ans = 0;
   while(bfs(s)){
        for(int i=1; i<=n; i++) cur[i] = head[i];///初始化

        ans += dfs(s,INF);
   }
   return ans;
}
int main(){
   init();
   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);
      addEdge(x,y,z);
      addEdge(y,x,0);
   }
   printf("%d\n",dinic());
   return 0;
}

cur數組啥意思?我們知道head存儲的是輸入順序同起點的邊最後一條邊的編號,如:按順序輸入1→2,1→4,1→3這三條邊,head存儲的是第三條邊,然後通過nxt遍歷其他邊。我們的cur數組就是同起點的存儲下一條邊,也就不用每次都從head初始存儲的邊開始遍歷。

  • Dinic算法的時間複雜度:Dinic算法從源點到匯點建一次分層圖,然後進行dfs,尋找增廣路徑,每次增廣至少使分層圖中的一條邊容量爲0,並且複雜度爲O(增廣路徑長度)。當找不到增廣路時再進行下一輪,由於每輪結束後源點到匯點不再連通,因此源點到匯點的最短路徑增加1,最多有O(n)輪;每輪每次增廣至少使一條邊消失,所以增廣次數爲O(m);每次增廣最多經過n個頂點,所以其複雜度爲O(n^2m)

1.3 ISAP算法(sap改進版)

2. 基於預流推進思想的最大流算法

參考資料:
1.網絡流從入門到熟練
2.網絡流從入門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章