1. 程式人生 > 其它 >[圖論入門]網路最大流 - 增廣路演算法

[圖論入門]網路最大流 - 增廣路演算法

#1.0 基本概念

先來介紹一下這個基本概念。

網路流是演算法競賽中的一個重要的模型,它分為兩部分:網路

網路,其實就是一張有向圖,其上的邊權稱為容量。額外地,它擁有一個源點匯點

,顧名思義,就像水流或電流,也具有它們的性質。如果把網路想象成一個自來水管道網路,那流就是其中流動的水。每條邊上的流不能超過它的容量,並且對於除了源點和匯點外的所有點(即中繼點),流入的流量都等於流出的流量。

源點可以無限量的向外提供流量,而匯點可以無限量的接受流量。

#2.0 最大流

這是一個比較常見的問題,也是本篇部落格主要討論的問題。

假設源點提供的流量足夠多,問匯點最多可以接收到多少流量。

#2.1 Ford-Fullkerson 演算法

\(\texttt{Ford-Fullkerson}\) 演算法(\(\texttt{FF}\) 演算法)是一個最大流的基礎演算法,其核心思想為尋找圖中的增廣路(Augmenting Path),實際就是網路中從源點到匯點的仍有剩餘流量的路徑。

我們來看一個例子:

在上圖中,\(1\to3\to2\to4\) 是一條增廣路,我們可以用這條路來更新殘量網路,這時網路變為了

但是不難發現,我們如果開始選擇 \(1\to3\to4,\ 1\to2\to4\) 這兩條增廣路,所得到的答案會更優,所以我們如果想要反悔這一操作,那麼,我們可以加入反向邊

反向邊最初的容量為 \(0\),但是如果該反向邊對應的邊容量減少了 \(a\)

,那麼就給反向邊的容量加上 \(a\),這樣我們就可以進行反悔,上面的例子變為

於是就有了這樣一條增廣路

然後我們這張網路的最大流就求得了。

\(\texttt{Ford-Fullkerson}\) 找增廣路的過程是 \(\texttt{DFS}\),這裡不加以實現。因為一(jue)般(dui)用不到。

#2.2 Edmond-Karp 演算法

實際上,\(\texttt{Edmond-Karp}\) 演算法(\(\texttt{EK}\) 演算法)本身只是 \(\texttt{FF}\) 演算法的 \(\texttt{BFS}\) 實現。但由於 \(\texttt{DFS}\) 找到的增廣路可能是七拐八繞的,而 \(\texttt{BFS}\)

卻可以保證找到的增廣路是最短的,因而在實際的時間複雜度上有了一定的優化,而且省去了遞迴導致的一系列問題。

當然,反向邊的編號可以是運用成對變換的方法,即 \(0\ \hat{}\ 1=1,1\ \hat{}\ 1=0,2\ \hat{}\ 1=3,3\ \hat{}\ 1=1,\cdots\) 同時,我們也需要記錄我們找到的增廣路,為了更新殘量網路。時間複雜度為 \(O(ve^2).\)

const ll N = 100010;
const int INF = 0x3fffffff;

struct Edge{
    int u,v;
    int nxt;
    ll w;
};
Edge e[N << 1];

ll n,m,cnt,head[N],vis[N],incf[N],pre[N],s,t,maxflow;

inline ll Min(const ll &a,const ll &b){
    return a < b ? a : b;
}

inline void add(const int &u,const int &v,const ll &w){
    e[cnt].u = u;e[cnt].v = v;e[cnt].w = w;
    e[cnt].nxt = head[u];head[u] = cnt ++;

    e[cnt].u = v;e[cnt].v = u;e[cnt].w = 0;
    e[cnt].nxt = head[v];head[v] = cnt ++;
}

inline bool EK(){
    mset(vis,0);queue <int> q;
    q.push(s);vis[s] = true;
    incf[s] = INF;
    while (q.size()){
        int x = q.front();q.pop();
        for (int i = head[x];i != -1;i = e[i].nxt)
          if (e[i].w){
              if (vis[e[i].v]) continue;
              incf[e[i].v] = Min(incf[x],e[i].w);
              pre[e[i].v] = i;
              q.push(e[i].v);vis[e[i].v] = true;
              if (e[i].v == t) return true;
          }
    }
    return false;
}

inline void update(){
    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;
    }
    maxflow += incf[t];
}

int main(){
    mset(head,-1);
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (int i = 1;i <= m;i ++){
        int u,v;ll w;
        scanf("%d%d%lld",&u,&v,&w);
        add(u,v,w);
    }
    while (EK()) update();
    printf("%lld",maxflow);
    return 0;
}

#2.3 Dinic 演算法

\(\texttt{EK}\) 演算法似乎已經能滿足我們了。。。嗎?如果是稠密圖,看起來 \(\texttt{EK}\) 演算法就要爆炸了,那麼我們就需要更優的演算法。

\(\texttt{Dinic}\) 演算法採用了分層圖的思想,將圖中的每一個點按照距離源點的遠近進行分層。每次查詢增廣路時僅找比當前節點層數加一的節點進行擴充套件。

分層時採用 \(\texttt{BFS}\),保證分層遠近的正確性,一個點能被分層的前提是通向這個點的邊仍有殘量,這樣可以保證沒有殘量的邊不會被遍歷,當我們分層到匯點就可以結束了,因為其他的沒被分層的點所在層數必然比匯點要遠,根據我們上面的實現思路,這樣的點是不會擴充套件到匯點的。

擴展采用 \(\texttt{DFS}\) 實現,所以不用 update() 函式,可直接進行更新。注意進行一次分層後可以進行多次找增廣路的操作,直到不能找到新的流量。

還有一點小優化:

  • 如果在 \(\texttt{DFS}\) 的過程中,發現某一點返回的可拓展的流量為 \(0\),那麼就可以將該點的層數設定為 \(0\),因為顯然已經不可能通過該點更新殘量網路了。
  • 記錄一個 now[x],表示點 \(x\) 當前可以從哪一條相連的邊開始進行探索,在後面的過程中,到點 \(x\) 便可以從編號為 now[x] 的邊開始。原因也很簡單:探索某條邊時必然會將當前分層圖上從這條邊能得到的流量都拿到,之後再次探索這條邊就沒有意義了。
const int N = 100010;
const int INF = 0x3fffffff;

struct Edge{
    int u,v;
    int nxt;
    ll val;
};
Edge e[N << 1];

int n,m,s,t;
ll maxflow;
int cnt,head[N],d[N],now[N];
queue <int> q;

inline void add(const int &u,const int &v,const ll &w){
    e[cnt].u = u;e[cnt].v = v;e[cnt].val = w;
    e[cnt].nxt = head[u];head[u] = cnt ++;

    e[cnt].u = v;e[cnt].v = u;e[cnt].val = 0;
    e[cnt].nxt = head[v];head[v] = cnt ++;
}

inline bool bfs(){
    mset(d,0);
    while (q.size()) q.pop();
    q.push(s);d[s] = 1;now[s] = head[s];
    while (q.size()){
        int x = q.front();q.pop();
        for (int i = head[x];i != -1;i = e[i].nxt)
          if (e[i].val && !d[e[i].v]){
              q.push(e[i].v);
              now[e[i].v] = head[e[i].v];
              d[e[i].v] = d[x] + 1;
              if (e[i].v == t) return true;
          }
    }
    return false;
}

inline ll dinic(int x,ll flow){
    if (x == t) return flow;
    ll rest = flow,k,i;
    for (i = now[x];(i != -1) && rest;i = e[i].nxt){
        if (e[i].val && d[e[i].v] == d[x] + 1){
            k = dinic(e[i].v,min(rest,e[i].val));
            if (!k) d[e[i].v] = 0;
            e[i].val -= k;e[i ^ 1].val += k;
            rest -= k;
        }
        now[x] = i;
    }
    return flow - rest;
}

int main(){
    mset(head,-1);
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (int i = 1;i <= m;i ++){
        int u,v;ll w;
        scanf("%d%d%lld",&u,&v,&w);
        add(u,v,w);
    }
    ll flow = 0;
    while (bfs()) while (flow = dinic(s,INF))
      maxflow += flow;
    printf("%lld",maxflow);
    return 0;
}

時間複雜度為 \(O(v^2e)\)。值得注意的一點是,\(\texttt{Dinic}\) 演算法在 二分圖最大匹配 上使用的時間複雜度是 \(O(v\sqrt e)\),優於匈牙利演算法。

#3.0 將二分圖匹配最大匹配轉化為最大流

對於一張二分圖,我們將左圖和右圖之間的邊容量設為 \(1\),在左邊加一個源點,向左圖所有點連一條容量為 \(1\) 的邊,在右邊加匯點,所有右圖的點向匯點連一條容量為 \(1\) 的邊,在該圖上跑最大流,得到的結果就是該圖的最大匹配。

正確性顯然,不證。

參考資料

[1] 初探網路流:dinic/EK演算法學習筆記 - hyfhaha

[2] 演算法學習筆記(28): 網路流 - Pecco