爲紀念自己花了三天入門了網絡流,特發此博客。
理解一個東西,首先我們得知道它是什麼?什麼是網絡流?先說幾個概念:
- 容量:相當於普通圖中的邊權。特別的,它表示的是經過此邊的東西的量的限制。記爲 。
- 流量:即有多少東西經過這裏。記爲 。
所謂的網絡流,就是一個邊有容量限制的圖。它具有兩個非常特殊的點:源點 和匯點 ,可以簡單的理解爲 bfs
的起點和終點。它應該滿足以下幾個條件:
- 容量限制:對於每條邊,流經該邊的流量不得超過該邊的容量。即 。
- 斜對稱性:每條邊的流量與其相反邊的流量之和爲 ,即 。
- 流守恆性:從源點流出的流量等於匯點流入的流量,即 。 表示邊集。
最大流是網絡流中的第一課(也就是我學了三天的才入門的那個)。它的目標是從 出發給到 最大的流量。我們約定除了 外的其它點需要滿足:流入它的流量和等於從它流出的流量和。
形象地理解一下: 就是一個自來水場, 是你家。由於你沒充錢,所以從自來水場流出的水必須先經過幾箇中間節點才能到你家。每天自來水管有水量的限制(多了會爆水管),中間點不能“創造”和“消耗”水(即上一段的約定),問你最多可以同時收到多少水?
如何解決?有很多種算法。最簡單的算法是利用殘餘網絡。什麼是殘餘網絡?很簡單,就是點集和邊集和原圖一樣,但是邊權爲 的圖。
我們記 (其中 都是原圖中的邊)爲一條路徑。如果這條路徑中邊權的最小值 ,就是增廣路。
增廣路有什麼作用?很簡單,有增廣路了就沒有求出最大流。爲什麼?因爲我們再沿着增廣路走,還可以獲得更多的水。
於是,我們每次求出增廣路更新答案就可以了!!!
但是,這樣是有反例的( 表示流量爲 ,容量爲 ):
如圖,如果我們走 ,最後的答案爲 。但是如果我們走兩條 ,答案爲 。
原因在於,我們沒有給程序一個反悔的機會。也就是說,程序執行了這一步之後,就再沒有撤銷的機會了。怎麼辦呢?
很簡單,我們給圖加上一個反向邊即可。反向邊的邊權就是正向邊流過的流量。比如這樣:
這樣,我們就可以算到正確的答案 了。我們直接按照這種方法就可以獲得正確的答案。如何找增廣路?用 dfs
或 bfs
就可以啦。時間複雜度爲 。 爲點數, 爲邊數,下同。
如何優化它?我們發現,對於這樣的圖,我們的算法就很慢:
原本,直接走 就可以一次獲得 的答案,但是程序偏偏選擇了最慢的路徑走,導致超時。
有沒有什麼方法?一個顯然的結論是經過的邊越多,對答案的貢獻越差(至少不優)。所以,我們選擇最短的路來增廣。
更進一步地,怎麼樣才能使路最短。假設 表示 到 的距離(即經過的邊數),一條路徑 是最短的必然有 。
於是我們可以提前用 bfs
算出所有的 ,然後在 dfs
時加上這個限制。加入了這個限制之後的算法就是 算法。可以證明,Dinic
算法的時間複雜度頂多是 。
還有一個優化叫做當前弧優化:很簡單,就是我們遍歷過邊 後,下次不再走了。另一個不知名的優化是:當 dfs
遍歷過一個點時,不再遍歷它。
-
這一個代碼表示鏈式前向星的建圖和加邊。沒什麼好說的,就是注意加反向邊且反向邊的初始邊權爲 。 -
這段代碼就是bfs
的分層。 數組就是 , 就是 。也沒什麼好講的。 -
這段代碼就是dfs
的計算增廣路。 表示可以給到 點的最大流量(可以不用完), 就是實際可以計算到的最大流量。注意遞歸調用(d=dfs(to,min(dist,e[i].dis))
)那裏,因爲邊 的流量限制是e[i].dis
,所以給到 的流量不能超過e[i].dis
(否則就爆水管了)。再略微注意一下e[i^1].dis+=d
,這個就是對反向邊的處理。因爲我們用i^1
表示邊 的反向邊,所以加邊的時候必須從偶數(通常爲 或 )開始給邊編號。提一下,for
那裏就是當前弧優化, 前面那個&
不能少。 -
這就是dinic
算法的主程序,也沒什麼好講(大不了背,反正這麼短),注意 的初始化即可。
最後,把所有的代碼打在一起就是完整的代碼啦,給一道模板題(洛谷 P3376
)的代碼給大家。
const int N=1e4+100;
const int M=1e5+100;
struct edge{
int next,to,dis;
}e[M<<1];int h[N],tot=1;
//注意,要用i^1表示邊i的反向邊
//則第1條邊的標號必須是偶數(2/0)
inline void add(int a,int b,int c){
e[++tot]=(edge){h[a],b,c};h[a]=tot;
}
int dep[N],cur[N],n,m,s,t,ans;
inline bool bfs(){//給圖分層
memset(dep,0,sizeof(dep));
queue<int> q;q.push(s);dep[s]=1;
while (!q.empty()){
int i,u=q.front();q.pop();
for(i=h[u];i;i=e[i].next){
register int to=e[i].to;
if (!dep[to]&&e[i].dis>0){
dep[to]=dep[u]+1;//計算深度
q.push(to);//第一次到,入隊
}
}
}
return dep[t]!=0;
}
int dfs(int u,int dist){//找增廣路
if (u==t) return dist;//到達終點
register int flow=0;//實際用流量
for(int &i=cur[u];i;i=e[i].next)
if (e[i].dis>0){//有找下去的意義
register int to=e[i].to,d;
if (dep[to]==dep[u]+1){
if ((d=dfs(to,min(e[i].dis,dist)))>0){
flow+=d;dist-=d;e[i].dis-=d;e[i^1].dis+=d;
if (dist==0) return flow;//流量耗盡,直接返回
}
}
}
return flow;//實際只能增加這麼多的流量
}
const int inf=0x3f3f3f3f;
inline int dinic_algorithm(){
while (bfs()){
for(int i=1;i<=n;i++)
cur[i]=h[i];
register int d=0;
while (d=dfs(s,inf))
ans+=d;
}
return ans;
}
int main(){
n=read();m=read();s=read();t=read();
for(int i=1,u,v,w;i<=m;i++){
u=read();v=read();w=read();
add(u,v,w);add(v,u,0);
}
cout<<dinic_algorithm();
return 0;
}
當然,dinic
算法通常是跑不到 的,它跟 spfa
一樣不穩定。