劍指offer——網絡的流程——一生必看的C++經典算法

網絡流

0.序言

聽起來這個名字如此高級,其實不然,大家要抱着一種開放包容的心態去學它。

一.基本概念

先來見一下網絡流的基本圖形:
在這裏插入圖片描述
可以看到,網絡流的圖形有一個源點s和一個匯點t,每一條邊都有一個容量和流量,流量永遠小於或等於容量。

二.預備知識

網絡流必須要用數組模擬指針的方法去存圖,不能用vector喲。
推薦博起:數據結構之鄰接表

三.最大流

1.概念

在這裏插入圖片描述
最大流就是說要求從源點到匯點最大的流量,這個圖顯而易見,最大流就是15,圖中已經標出來了。
那麼怎麼計算這個最大流呢?

2. EK算法

1.思想

很容易想到一個有些暴力的算法:從源點開始,暴力的找出所有到匯點的路徑,再把每條路徑上最小的邊的容量加起來就行了。
每條能貢獻流量的路就叫做增廣路。
但是這種方法的正確性有待考究,比如這個圖:
在這裏插入圖片描述
假如我們先走s→1→4→t,圖就變成了這樣:
在這裏插入圖片描述
然後走s→2→4→t,發現4→t的時候路被堵了,但是上一條路徑明明可以改成s→1→3→t,這樣這一條路就可以順利走通了,並且找到了最大流,就是20,如下圖:
在這裏插入圖片描述
現在最難得問題就是怎麼來實現這個算法呢?
現在,引入一種算法:建反邊
那麼,原圖就變成了這個樣子(初始化所有反邊容量爲0):
在這裏插入圖片描述
每走過一條邊,就讓你走的方向的邊的容量減去流量,讓反向的邊的容量加上流量就行了。最後你找最大流的時候,就直接在圖中從s到t走,找出所有增廣路(你建的所有的邊都可以走),像最初我們的想法那樣做就行了。
總結起來,分三步:
1.建反向邊
2.找所有增廣路
3.統計最大流

這就叫做EK算法。
那爲什麼要建反邊呢?個人認爲是爲了讓路徑上遇到容量不允許的邊能夠改變原來走那條邊的路徑,讓那條路徑去找其它可能的邊去走,如果找不到,就讓這條路徑終止,那條路徑不變。額。。。大家可以去找找其它的證明康康。

2.實現

先說找增廣路:
因爲我們要存儲這條路徑,所以要用pre[x]去存經過點x的路徑的前一個點,和點x與前一個點相連的邊的編號(要用數組模擬指針去存圖喲)。
上代碼:

struct node {
    int x, edge;
    node (){};
    node (int X, int Edge){
        edge = Edge;
        x = X;
    }
}pre[N];
bool find (){
    queue <int> Q;
    Q.push (s);
    memset (vis, 0, sizeof vis);
    memset (pre, -1, sizeof pre);
    vis[s] = 1;
    while (! Q.empty ()){
        int f = Q.front ();
        Q.pop ();
        for (int i = first[f]; i != -1; i = next[i]){
            if (! vis[v[i]] && w[i]){
                pre[v[i]].x = f;
                pre[v[i]].edge = i;
                if (v[i] == t)
                    return 1;
                vis[v[i]] = 1;
                Q.push (v[i]);
            }
        }
    }
    return 0;
}

再來說說統計最大流:
很簡單,先沿着找到的增廣路找出最小容量的邊的容量作爲流量,再把路徑上的邊的容量減去流量,反邊加上流量就行了。

void EK (){
    while (find ()){
        int minn = INF;
        for (int i = t; i != s; i = pre[i].x){
            minn = min (minn, w[pre[i].edge]);
        }
        for (int i = t; i != s; i = pre[i].x){
        	//printf ("%d ", i);
        	w[pre[i].edge] -= minn;
        	w[pre[i].edge ^ 1] += minn;
		} 
		//printf ("%d\n", s); 
		ans += minn;
    }
}

3.Code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

#define M 200005
#define N 10005
#define INF 0x3f3f3f3f

struct node {
    int x, edge;
    node (){};
    node (int X, int Edge){
        edge = Edge;
        x = X;
    }
}pre[N];
int n, m, s, t, u[M], v[M], w[M], first[N], next[M], k, ans;
bool vis[N];

void addedge (int x, int y, int z){
	k ++;
	u[k] = x, v[k] = y, w[k] = z;
	next[k] = first[x];
	first[x] = k;
}
bool find (){
    queue <int> Q;
    Q.push (s);
    memset (vis, 0, sizeof vis);
    memset (pre, -1, sizeof pre);
    vis[s] = 1;
    while (! Q.empty ()){
        int f = Q.front ();
        Q.pop ();
        for (int i = first[f]; i != -1; i = next[i]){
            if (! vis[v[i]] && w[i]){
                pre[v[i]].x = f;
                pre[v[i]].edge = i;
                if (v[i] == t)
                    return 1;
                vis[v[i]] = 1;
                Q.push (v[i]);
            }
        }
    }
    return 0;
}
void EK (){
    while (find ()){
        int minn = INF;
        for (int i = t; i != s; i = pre[i].x){
            minn = min (minn, w[pre[i].edge]);
        }
        for (int i = t; i != s; i = pre[i].x){
        	//printf ("%d ", i);
        	w[pre[i].edge] -= minn;
        	w[pre[i].edge ^ 1] += minn;
		} 
		//printf ("%d\n", s); 
		ans += minn;
    }
}
int main (){
    scanf ("%d %d %d %d", &n, &m, &s, &t);
    k = -1;
    for (int i = 1; i <= n; i ++)
        first[i] = -1;
    for (int i = 1; i <= m; i ++){
        int u, v, w;
        scanf ("%d %d %d", &u, &v, &w);
        addedge (u, v, w);
        addedge (v, u, 0);
    }
    EK ();
    printf ("%d\n", ans);
    return 0;
}

4.運用

先給模板:最大流模板
二分圖匹配:完美的牛欄
會了最大流算法,就不怕二分圖匹配了。
我們建一個源點,向一個集合中的所有點連一條無限流量的邊,再建一個匯點,從另一個集合的每一個點向匯點連一條無限流量的邊,兩個集合之間的邊的容量就爲1,最後找一波最大流就是答案了。
雖然要慢一些,但是不要怕,好打。

四.最小費用最大流(費用流)

1.概念

每條邊加一個經過這條邊單位流量需要的費用,在保證最大流的情況下,要使花費的費用最小。

2.思想&實現

既然要費用最小,很容易想到各種求最短路的算法,這裏用spfa。
我們直接把原來的bfs改成spfa就完事了。
統計的時候把每條邊的費用加起來得到總費用,乘以流量就是這條增廣路的費用了。

3.Code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

#define N 10005
#define M 100005
#define INF 0x3f3f3f3f

struct node {
    int x, edge;
}pre[N];
int n, m, s, t, u[M], v[M], w[M], f[M], head[N], next[M], k = -1, ans1, ans2, dis[N];
bool vis[N];

void addedge (int x, int y, int z, int p){
    k ++;
    u[k] = x, v[k] = y, w[k] = z, f[k] = p;
    next[k] = head[x];
    head[x] = k;
}
bool find (){
    memset (pre, -1, sizeof pre);
    memset (dis, INF, sizeof dis);
    memset (vis, 0, sizeof vis);
    vis[s] = 1;
    queue <int> Q;
    Q.push (s);
    dis[s] = 0;
    while (! Q.empty ()){
        int p = Q.front ();
        Q.pop ();
        vis[p] = 0;
        for (int i = head[p]; i != -1; i = next[i]){
            if (w[i] && dis[v[i]] > dis[p] + f[i]){
                dis[v[i]] = dis[p] + f[i];
                pre[v[i]].x = p;
                pre[v[i]].edge = i;
                if (! vis[v[i]]){
                    vis[v[i]] = 1;
                    Q.push (v[i]);
                }
            }
        }
    }
    return dis[t] != INF;
}
void EK (){
    while (find ()){
        int minn = INF;
        for (int i = t; i != s; i = pre[i].x)
            minn = min (minn, w[pre[i].edge]);
        for (int i = t; i != s; i = pre[i].x){
            w[pre[i].edge] -= minn;
            w[pre[i].edge ^ 1] += minn;
        }
        ans1 += minn;
        ans2 += minn * dis[t];
    }
}
int main (){
    scanf ("%d %d %d %d", &n, &m, &s, &t);
    memset (head, -1, sizeof head);
    for (int i = 1; i <= m; i ++){
        int x, y, z, p;
        scanf ("%d %d %d %d", &x, &y, &z, &p);
        addedge (x, y, z, p);
        addedge (y, x, 0, -p);
    }
    EK ();
    printf ("%d %d\n", ans1, ans2);
    return 0;
}

4.運用

模板:最小費用最大流

五.最小割最大流

1.概念

最小割的概念就是把整個圖分成兩個點集,割掉邊讓兩集合的點不相連的最小費用和。

2.解法

給你個定理你就知道了:一個圖的最小割等於最大流
知道了吧。

六.運用

網絡流的運用其實挺容易看出來的,下面有幾道題可以借鑑:
費用流
切糕
Going Home

謝謝!

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