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)上的流量。 - 源點流出量等於整個網絡的流量;匯點流入量等於整個網絡的流量;中間點總流入量等於總流出量
- 每條弧(u,v)上給定一個實數f(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)
。