網絡流從入門到放棄
by 沉迷流體力學無心刷題的rvalue
€€£ WARNING: 前方多圖殺貓
關於這篇文章
本來這個是18年12月在校內講課用的課件...本來打算當時就放到博客上但是因為里面有大量mermaid圖就沒敢放qaq...然而突然發現cnblogs是滋磁mermaid圖的於是就丟出來了qaq
如果有錯漏之處還請在評論區指正.
好像由於目錄結構太大所以cnblogs生成不出來了...大綱是長這樣的:
+ 網絡流從入門到放棄
+ 何謂網絡流
+ 最大流
+ Ford-Fulkerson算法(增廣路算法)
+ 實現
+ 局限
+ Edmonds-Karp算法(EK算法)
+ 代碼實現
+ 局限
+ Dinic算法
+ 舉個栗子
+ 阻塞流
+ 代碼實現
+ 時間復雜度分析
+ 真·時間復雜度分析
+ 真·代碼實現
+ 一些題外問題
+ 最大流建模舉例
+ 圓桌問題
+ 公平分配問題
+ 星際轉移問題
+ 收集者的難題
+ 最大流與最小割
+ 流與割的關系
+ 最大流最小割定理
+ 最小割建模舉例
+ 花園的守護之神
+ 王者之劍
+ Number
+ 最小割
+ 最大權閉合子圖
+ 切糕
+ 最小費用最大流
+ EK費用流
+ 反向邊權
+ 關於SPFA
+ 時間復雜度分析
+ 消圈算法
+ 消圈定理
+ ZKW費用流
+ 代碼實現
+ 關於當前弧優化
+ 時間復雜度分析
+ Dijkstra費用流
+ 最小費用最大流建模舉例
+ 南極科考旅行
+ 餐巾
+ 負載平衡
+ 最長k可重區間集
+ 平方費用最小費用最大流
文章長度比較勸退...但是應該能真正讓讀者入門(至少能打出復雜度比較正確的板子).
上下界網絡流部分由於一些歷史原因咕給另一個聚聚了...
何謂網絡流
假設 \(G = (V,E)\) 是一個有限的有向圖,它的每條邊 \((u,v) \in E\) 都有一個非負值實數的容量$ c(u, v)\(。如果\) (u, v) \not \in E\(,我們假設\) c(u, v) = 0$。我們區別兩個頂點:一個源點 \(s\) 和一個匯點$ t\(。一道網絡流是一個對於所有結點\) u$ 和$ v \(都有以下特性的實數函數\) f:V \times V \rightarrow \mathbb{R}\(: **容量限制(Capacity Constraints)**:\) f(u, v) \leq c(u, v)\(一條邊的流不能超過它的容量。 **斜對稱(Skew Symmetry)**:\) f(u, v) = - f(v, u)$由 \(u\)到 \(v\)的凈流必須是由 \(v\)到 \(u\)的凈流的相反(參考例子)。
流守恆(Flow Conservation): 除非 \(u = s\)或 \(u = t\),否則 \(\sum_{w \in V} f(u, w) = 0\)一結點的凈流是零,除了“制造”流的源點和“消耗”流的匯點。
即流守恆意味着:$ \sum_{(u,v) \in E} f(u,v) = \sum_{(v,z) \in E} f(v,z)$ ,對每個頂點$ v \in V\setminus{s,t}\( 注意\) f(u,v)$ 是由 \(u\) 到 $v \(的凈流。如果該圖代表一個實質的網絡,由\) u \(到\) v \(有4單位的實際流及由\) v$ 到 $u $有3單位的實際流,那么 $f(u, v) = 1 $及 \(f(v, u) = -1\)。
基本上,我們可以說,物理網絡的網絡流是從$ s = \sum_{(s,v)\in E} f(s,v) \(出發的流 邊的**剩余容量(residual capacity)**是\) c_f(u, v) = c(u, v) - f(u, v)$。這定義了以 $G_f(V, E_f) $表示的剩余網絡(residual network),它顯示可用的容量的多少。留意就算在原網絡中由 \(u\) 到 $v \(沒有邊,在剩余網絡仍可能有由\) u \(到\) v $的邊。因為相反方向的流抵消,減少由 $v \(到\) u$ 的流等於增加由$ u$ 到 $v \(的流。**增廣路(augmenting path)**是一條路徑\) (u_1, u_2, \dots, u_k)$,而 \(u_1 = s\)、$ u_k = t $及 \(c_f(u_i, u_{i+1}) > 0\),這表示沿這條路徑發送更多流是可能的。當且僅當剩余網絡$ G_f $沒有增廣路時處於最大流。
因此如下使用圖 \(G\) 創建 $ G_f \(: \)G_f = V $的頂點
定義如下的 $G_f = E_f \(的邊 對每條邊\) (x,y) \in E$
若$ f(x,y) < c(x,y)\(,創建容量為\) c_f = c(x,y) - f(x,y)$ 的前向邊 \((x,y) \in E_f\)。
若$ f(x,y) > 0\(,創建容量為\) c_f = f(x,y) $的后向邊 \((y, x) \in E_f\)。
這個概念用在計算流量網的最大流的Ford–Fulkerson算法中。
有時需要對有多於一個源點的網絡,於是就引入了圖的超源點。這包含了一個與無限容量的邊連接的每個源點,以作為一個整體源點。匯點也有類似的構造稱作超匯點。
以上是摘自Wikipedia的定義
實際上重點有三:
- 容量限制(Capacity Constraints):$ f(u, v) \leq c(u, v)$一條邊的流不能超過它的容量。
- 斜對稱(Skew Symmetry):$ f(u, v) = - f(v, u)$由 \(u\)到 \(v\)的凈流必須是由 \(v\)到 \(u\)的凈流的相反(參考例子)。
- 流守恆(Flow Conservation): 除非 \(u = s\)或 \(u = t\),否則 \(\sum_{w \in V} f(u, w) = 0\)一結點的凈流是零,除了“制造”流的源點和“消耗”流的匯點。
想象一個不可壓縮的流體的運輸管網, 每個管道都有防倒流閥門 (保證有向) , 每個管道還有一個單位時間內的流量限制, 那就是一個網絡流模型的樣子了
最大流
最大流問題其實就是字面意思, 求 \(s\) 到 \(t\) 的最大流量.
比如對於下面這個流量網絡:
它的最大流是 \(23\):
Ford-Fulkerson算法(增廣路算法)
對於最大流問題, 我們有一個直觀的思路, 就是找一條從 \(s\) 到 \(t\) 而且剩余容量非空的路徑, 然后把它跑滿並累加答案
而這個"從 \(s\) 到 \(t\) 而且剩余容量非空的路徑"就是增廣路. 因為它將原有的流擴充(或者說"增廣")了.
但是這個時候我們會發現一些問題: 如果不巧找到了一個增廣路把本來不該在最大流里的邊給增廣了怎么破?
比如下圖中的增廣路:
如果我們繼續增廣, 則我們得到的"最大流"只有 \(20\).
這時我們考慮以前講過的"可撤銷貪心", 建立一條反向邊來允許撤銷.
記得定義中的一句"斜對稱"么? 也就是 \(f(u,v)=-f(v,u)\), 於是我們可以定義反向邊上增加一單位流量都代表原邊上減少一單位流量. 如果增廣出了 \((u,v)\) 之間的雙向流量實際上可以將經過 \(u,v\) 的流量交換來抵消成單向流量. 比如下圖:
實際上我們相當於是增加反方向的流量來把原來的正向流量"推回去".
或者一個更形象化的解釋, 每條邊代表着一個里面流動着不可壓縮流體的具有流速限制的管道, 那么反向增廣就有點像是反向加壓來嘗試把流體壓回原來的點, 總的效果就是讓這條邊里的流速減緩或反向, 而讓流返回原來的點重新決策.
這其實也告訴我們, 最大流必定是無環的. 因為循環流無源無匯, 不會對 \(s\verb|-|t\) 流量產生貢獻, 卻會白白占用容量, 一定不會優.
實際上我們對這個"消除環流"的過程的實現方式是"只要給一條邊加載上流量, 就必須在其對應的反向邊上擴充等量的容量".
這個過程中引入一個概念: 殘量網絡. 可以理解為意思就是把滿流的邊扔掉之后, 邊權為剩余流量的圖.
於是我們加載流量的過程就可以轉化為邊權降低, 擴容的過程就可以轉化為邊權增加.
實際上整個最大流的過程中我們我們並沒有必要一直在容量和當前流量這兩個值上做文章. 我們在整個算法中全程只關心它們的差值: 剩余容量. 所以我們其實只記錄它就可以了. 然后殘量網絡就可以定義成只包含剩余容量非 \(0\) 的邊的圖.
實現
建立流量網絡, 對於每條邊記錄一下它的出入結點/剩余容量/反向邊, 然后我們進行一次只走剩余流量非 \(0\) 的邊的DFS來查找增廣路徑, 過程中順便維護一下路徑上的最小剩余容量 (顯然我們只要找到一條增廣路徑之后把它壓榨干凈再繼續找下一條是最優策略), 回溯的時候進行 "減少當前邊剩余容量, 增加反向邊剩余容量" 的操作, 結束后把答案累加起來就好了. 代碼實現就是返回一個值表示找到的增廣路的流量, 如果為 \(0\) 表示沒有找到增廣路. 只要返回非 \(0\) 值就執行縮小剩余容量的操作並回溯. 單次增廣是 \(O(V+E)\) 的.
不難發現整個過程中一直保持着流量守恆的原則: 每次我們都是在一整條路徑上搬移流量, 所以中間結點完全不會累積流量.
局限
我們發現增廣路算法的運行時間基本上全靠RP: 看你增廣路選得怎么樣. 選得好沒准一次就跑出來了, 選差了就得一直把流推來推去. 比如下面這張圖:
我們一眼就能看出這圖最大流是 \(46666666\) , 但是假如你RP爆炸一直在和邊 \((1,2)\) 斗智斗勇的話...請允許我做一個悲傷的表情...
不過增廣路算法是一定會結束的, 因為你每次找到一個增廣路都會讓流量增加, 最終一定能達到最大流.
時間復雜度是 O(值域) 的, 屬於指數級算法. (題外話: 其實O(值域)的算法全是指數算法...我才不會告訴你今年聯賽day1考了個NPC問題呢)
但是這個算法是幾乎所有實用網絡流算法的基礎. 它們本質上都是增廣路算法的優化.
Edmonds-Karp算法(EK算法)
EK算法其實就是在增廣路算法的基礎上加了一層優化: 每次增廣最短的增廣路.
這樣的話它的時間復雜度便有了保障, 是 \(O(VE^2)\) 的. 大體的證明思路是這樣的:
首先每次我們找到一條增廣路的時候肯定會把它壓榨干凈, 也就是說至少有一條邊在這個過程中被跑滿了. 我們把這樣的邊叫做關鍵邊. 增廣之后, 這些邊在殘量網絡中都會被無視掉. 也就是說這條路徑就這么斷掉了. 而我們每次都增廣最短路, 也就是說我們每次都在破壞最短路. 所以 \((s,t)\) 之間的最短路單調遞增.
因為增廣路必然伴隨至少一條關鍵邊出現, 所以我們可以把增廣過程的迭代次數上界轉化為每條邊成為關鍵邊的次數.
因為關鍵邊會從殘量網絡中消失, 直到反向邊上有了流量才會恢復成為關鍵邊的可能性, 而當反向邊上有流量時最短路長度一定會增加. 而最短路長度不會超過 \(V\), 所以總迭代次數是 \(O(VE)\) 的.
因為EK算法中殘量網絡上的最短路是 \(0/1\) 最短路, 直接BFS實現的話時間復雜度是 \(O(E)\) 的, 於是EK算法總時間復雜度 \(O(VE^2)\) , 證畢.
實現上把DFS改成BFS並且在第一次訪問到 \(t\) 時就跳出就行了, 實際上沒啥好講的...
代碼實現
下面這個實現是去年粘的某剛哥屆學長的課件里的...懶得自己寫了(其實是不會)
int Bfs() {
memset(pre, -1, sizeof(pre));
for(int i = 1 ; i <= n ; ++ i) flow[i] = INF;
queue <int> q;
pre[S] = 0, q.push(S);
while(!q.empty()) {
int op = q.front(); q.pop();
for(int i = 1 ; i <= n ; ++ i) {
if(i==S||pre[i]!=-1||c[op][i]==0) continue;
pre[i] = op; //找到未遍歷過的點
flow[i] = min(flow[op], c[op][i]); // 更新路徑上的最小值
q.push(i);
}
}
if(flow[T]==INF) return -1;
return flow[T];
}
int Solve() {
int ans = 0;
while(true) {
int k = Bfs();
if(k==-1) break;
ans += k;
int nw = T;
while(nw!=S) {//更新殘余網絡
c[pre[nw]][nw] -= k, c[nw][pre[nw]] += k;
nw = pre[nw];
}
}
return ans;
}
局限
雖然EK算法成功把時間復雜度降到了一個多項式級別, 但是它 \(O(VE^2)\) 的上界實際上相當於 \(O(V^5)\) 的級別(\(E\) 與 \(V^2\) 可以是同階的), 並不是非常能夠令人接受.
我們來看一看為啥EK依然這么慢.
為啥呢?
我們再看最開始的流量網絡, 跑一跑EK的BFS求最短路, 找到一條最短增廣路 \(s\rightarrow 1 \rightarrow 3 \rightarrow t\) 之后:
我們增廣掉這條路徑上的 \(12\) 單位流量...然后再來一遍BFS求最短路...
先等一等!
我們一眼就能就發現: 這 \(s \rightarrow 2 \rightarrow 4 \rightarrow t\) 明明也是一條最短增廣路啊!
EK算法的劣勢就在於, 每次遍歷了一遍殘量網絡之后只能求出一條增廣路來增廣.
於是我們針對這一點進行優化, 我們就得到了一個更加優秀的替代品.
Dinic算法
沒有什么是一個BFS或一個DFS解決不了的;如果有,那就兩個一起。
Dinic算法又稱為"Dinic阻塞流算法", 這個算法的關鍵就在於"阻塞流".
首先我們順着EK算法的思路, 每次增廣最短路上的邊來在一定程度上保證時間復雜度.
這時我們引入一個概念: 分層圖. 它的一個不嚴謹的定義就是: 對於每個點, 按照從 \(s\) 到它的最短路長度分組, 每組即為"一層".
其實這個"分層"也可以理解為深度...
舉個栗子
回到最開始的那個流量網絡, 我們把它BFS一遍按照 \(s\) 最短路分層, 得到下面的圖:
層內的邊和反向邊不在最短路上所以增廣時都會被我們被無視, 我們在示意圖中刪掉它們, 於是就變成:
分好層之后, 我們非常偷稅地發現:
- 所有存在於分層圖上的殘余邊都在一條殘量網絡的最短路上
- 反向邊都去和梁非凡共進晚餐了
因為不用擔心在處理分層圖的時候流會被反向邊退回, 所以我們只管放心大膽地只用一遍DFS來增廣就好了.
但是這並不意味着我們在邊上加載流量的時候可以不管反向邊, 反向邊的殘量變更還是要算的, 因為以后的DFS可能會把流再推回去.
阻塞流
這一節剛開始的時候說Dinic的關鍵就在於"阻塞流". 阻塞流是啥?
我們嘗試在上圖中增廣, 然后得到下面的殘量網絡:
我們發現把當前分層圖上的最大流完全增廣之后, \(s\) 和 \(t\) 在分層圖上一定會不連通 (增廣路算法找不到新的增廣路就是因為殘量網絡不連通), 我們稱其為阻塞增廣. 這樣增廣出來的流就是阻塞流.
代碼實現
int Dinic(int s,int t){
int ans=0;
while(BFS(s,t))
ans+=DFS(s,INF,t);
return ans;
}
int DFS(int s,int flow,int t){
if(s==t||flow<=0)
return flow;
int rest=flow;
for(Edge* i=head[s];i!=NULL&&rest>0;i=i->next){
if(i->flow>0&&depth[i->to]==depth[s]+1){
int k=DFS(i->to,std::min(rest,i->flow),t);
rest-=k;
i->flow-=k;
i->rev->flow+=k;
}
}
return flow-rest;
}
bool BFS(int s,int t){
memset(depth,0,sizeof(depth));
std::queue<int> q;
q.push(s);
depth[s]=1;
while(!q.empty()){
s=q.front();
q.pop();
for(Edge* i=head[s];i!=NULL;i=i->next){
if(i->flow>0&&depth[i->to]==0){
depth[i->to]=depth[s]+1;
if(i->to==t)
return true;
q.push(i->to);
}
}
}
return false;
}
BFS
函數非常好說, 就是一個純粹的 \(0/1\) 最短路
DFS
函數有幾個點需要說一下. 先說參數. 首先 s
, t
是字面意思, 然后是flow
參數, 代表"上游的邊對增廣路上的流量的限制". 因為增廣路上任意一條邊都不能超流. 接着有一個局部變量 rest
, 表示"上游的流量限制還有rest
單位沒有下傳". 因為要滿足流量守恆, 我們只能把流入的流量分配到后面, 所以這個 rest
實際上保存的就是最大可能的流入流量.
最后返回的是flow-rest
, 把所有后繼結點都訪問過之后的 rest
值即為無法排出的流入量的值, 我們返回的是增廣量所以肯定不能讓它不能排出, 所以我們把上游流入量減去不能排出的量即為可行流量.
或者說, 參數 flow
是"推送量", 返回的是"接受量"或"成功傳遞給 \(t\) 的流量", rest
是"淤積量".
(能量流動學傻了.png)
時間復雜度分析
由於阻塞增廣之后不再存在原長度的最短路, 最短路的長度至少 \(+1\). 所以阻塞增廣會進行進行 \(O(V)\) 次.
進行一次阻塞增廣只要一次DFS就可以實現, 而一次DFS的時間復雜度小學生都知道是 \(O(E)\) 的.
於是Dinic的時間復雜度是 \(O(VE)\) 的! 比EK優秀到不知道哪里去了!
然而是假的
真·時間復雜度分析
其實多路增廣的同時我們發現一個問題: 這個DFS求阻塞增廣的復雜度其實和普通DFS完全不同. 想一想, 為什么.
EK算法中計算一條增廣路是嚴格BFS一遍, 時間復雜度嚴格 \(O(E)\), 但是這次的DFS就不是這樣了.
我們會有重復DFS一個結點這種操作.
比如下面這個分層圖:
我們就會發現一開始從 \(s \rightarrow 1 \rightarrow 3\) 這條路徑過來的時候, 上游的殘量已經把最大增廣量卡到了 \(4\). 所以我們只能在 \(3\) 號點后增廣 \(4\) 個單位的流量, 但是 \(3\) 號點依然有繼續增廣的空間, 我們不能打個vis
就跑路. 接着從 \(s \rightarrow 2 \rightarrow 3\) 過來的時候, 就會繼續從 \(3\) 號點向下增廣.
然而不加vis
的DFS是指數級的.
所以這個鬼Dinic又是一個辣雞指數算法?
沒那么簡單.
我們在DFS的時候加兩個非常簡單但是很重要的優化:
int DFS(int s,int flow,int t){
if(s==t||flow<=0)
return flow;
int rest=flow;
for(Edge*& i=cur[s];i!=NULL;i=i->next){
if(i->flow>0&&depth[i->to]==depth[s]+1){
int tmp=DFS(i->to,std::min(rest,i->flow),t);
if(tmp<=0)
depth[i->to]=0;
rest-=tmp;
i->flow-=tmp;
i->rev->flow+=tmp;
if(rest<=0)
break;
}
}
return flow-rest;
}
如果我們令 depth=0
, 那么不可能會有哪個前驅結點滿足 \(d(u)+1=d(v)\) 了, 也就是說我們把這個點無視掉了.
為啥呢?
因為你現在找不到妹子以后也一樣找不到
(這是剛哥的比喻(逃))
因為如果你現在嘗試給 i->to
推送一個大小非 \(0\) 的流量, 然而它卻無情地返回了一個 \(0\) 作為接受量的話, 只能說明: i->to
結點從此刻開始無法再推送更多流量到 \(t\) 了. 現在不能, 以后也不能. 於是我們刪掉它作為優化.
然后最關鍵的決定時間復雜度的優化是當前弧優化, 我們每次向某條邊的方向推流的時候, 肯定要么把推送量用完了, 要么是把這個方向的容量榨干了. 除了最后一條因為推送量用完而無法繼續增廣的邊之外其他的邊一定無法繼續傳遞流量給 \(t\) 了. 這種無用邊會在尋找出邊的循環中增大時間復雜度(記得那個歐拉路題么?), 必須刪除.
最后再看重新這一整個DFS的過程, 如果當前路徑的最后一個點可以繼續擴展, 則肯定是在層間向匯點前進了一步, 最多走 \(V\) 步就會到達匯點. 在前進過程中, 我們發現一個點無法再向 \(t\) 傳遞流量, 我們就刪掉它. 根據我們在分析EK算法時間復雜度的時候得到的結論, 我們會找到 \(O(E)\) 條不同的增廣路, 每條增廣路又會前進或后退 \(O(V)\) 步來更新流量, 又因為我們加了當前弧優化所以查找一條增廣路的時間是和前進次數同階的, 於是單次阻塞增廣DFS的過程的時間上界是 \(O(VE)\) 的.
於是Dinic算法的總時間復雜度是 \(O(V^2E)\) 的.
這個上界非常非常松 (王逸松的松). 松到什么程度?
LOJ最大流板子, \(V=100, E=5000\) , 計算得 \(V^2E=5\times 10^7\) , 而我這份充滿STL和遞歸的板子代碼實際上跑得最慢的點只跑了 \(25\texttt{ms}\).
順便說這個Dinic在容量 \(0/1\) 以及層數不多的圖上跑得更快, 二分圖匹配問題上甚至被證明了一個 \(O(\sqrt{V}E)\) 的上界
實戰應用網絡流建模的時候因為是自己構圖, 一般層數都不會非常大而且結構是自己定的, 所以跑得會更快~ (一般\(800\) 到 \(1000\) 個點, 邊數 \(1\times 10^4\) 左右的圖都是能跑的)
真·代碼實現
以下是LOJ#101 最大流的AC板子
#include <bits/stdc++.h>
const int MAXV=110;
const int MAXE=10010;
const long long INF=1e15;
struct Edge{
int from;
int to;
int flow;
Edge* rev;
Edge* next;
};
Edge E[MAXE];
Edge* head[MAXV];
Edge* cur[MAXV];
Edge* top=E;
int v;
int e;
int s;
int t;
int depth[MAXV];
bool BFS(int,int);
void Insert(int,int,int);
long long Dinic(int,int);
long long DFS(int,long long,int);
int main(){
scanf("%d%d%d%d",&v,&e,&s,&t);
for(int i=0;i<e;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
Insert(a,b,c);
}
printf("%lld\n",Dinic(s,t));
return 0;
}
long long Dinic(int s,int t){
long long ans=0;
while(BFS(s,t))
ans+=DFS(s,INF,t);
return ans;
}
bool BFS(int s,int t){
memset(depth,0,sizeof(depth));
std::queue<int> q;
q.push(s);
depth[s]=1;
cur[s]=head[s];
while(!q.empty()){
s=q.front();
q.pop();
for(Edge* i=head[s];i!=NULL;i=i->next){
if(i->flow>0&&depth[i->to]==0){
depth[i->to]=depth[s]+1;
cur[i->to]=head[i->to];
if(i->to==t)
return true;
q.push(i->to);
}
}
}
return false;
}
long long DFS(int s,long long flow,int t){
if(s==t||flow<=0)
return flow;
long long rest=flow;
for(Edge*& i=cur[s];i!=NULL;i=i->next){
if(i->flow>0&&depth[i->to]==depth[s]+1){
long long tmp=DFS(i->to,std::min(rest,(long long)i->flow),t);
if(tmp<=0)
depth[i->to]=0;
rest-=tmp;
i->flow-=tmp;
i->rev->flow+=tmp;
if(rest<=0)
break;
}
}
return flow-rest;
}
void Insert(int from,int to,int flow){
top->from=from;
top->to=to;
top->flow=flow;
top->rev=top+1;
top->next=head[from];
head[from]=top++;
top->from=to;
top->to=from;
top->flow=0;
top->rev=top-1;
top->next=head[to];
head[to]=top++;
}
注意到我把當前弧優化的重賦值部分寫在了 BFS
里, 這樣可以做到"按需賦值". 因為Dinic本來上界就松得一匹, BFS的過程中不連通的點根本就不用再管了...
以及如果你用數組邊表的話, 可以選擇讓下標從 \(0\) 開始, 保證一次加入兩個邊, 這樣的話只要 \(\text{xor}\,1\) 就可以算出反向邊的編號了
一些題外問題
到現在估計jjm已經吵了好幾次"這是個黑盒算法不用理解"之類的話了.
為啥我要把一個Dinic講得這么細呢? 況且它確實真的只是個建模之后跑一下的工具?
因為如果你不理解Dinic的過程與復雜度分析, 你幾乎一 定 會 寫 假.
有些看起來很不起眼的小細節可能影響着整個算法的時間復雜度.
首先就是當前弧優化的跳出條件, 我為啥要把"除了最后一條邊之外"那句話加粗呢? 因為你如果把跳出判定寫在for
循環里會慢 \(10\) 倍以上, 根本不是常數問題, 是復雜度出了鍋. 因為你會漏掉最后那個可能沒跑滿的弧, 而分層圖BFS會在當前圖沒有被割斷的時候一直跑跑跑, 於是就鍋了.
其次有人把無用點優化當成當前弧優化的替代品, 實際上無用點優化並不能保證時間復雜度. 無用點優化可以看做是當前弧優化的一個弱化版, 區別只是在於是出邊全都無法傳遞流量的時候再刪還是一條邊無法傳遞流量時就刪.
甚至有人直接不加當前弧優化和無用點優化, 這是我最 \(F_2\) 的...
網上把Dinic寫假的真不少. 很多Dinic教程上的板子都是假的.
本着防止大家好不容易建出圖來結果因為板子一直寫的是假的結果炸飛的態度 (堅信一個復雜度是假的的東西復雜度是真的, 這和在普通最短路中用SPFA有什么區別) , 我決定從仔細講好Dinic開始.
以及為啥不講ISAP?
因為我不是ISAP選手所以不會ISAP
因為ISAP其實在很多用途上其實並沒有Dinic靈活. 有些玄學建圖騷操作ISAP很難跑得起來. 今年省選D2T1如果不是因為出題人是ISAP選手而特意構造了一個ISAP能跑的題的話很可能就會無意間卡掉ISAP選手 (這不是玩笑, 多半正經出題人都是Dinic選手, 一個不小心就會無意間卡掉ISAP). 所以我個人並不建議大家做ISAP選手.
當然你要是堅信ISAP跑得比較快而且不會被卡而去學ISAP我也不攔你...柱子恆就是ISAP選手
最大流建模舉例
當你能熟練而正確地打出最大流板子的時候你就會發現: 你並不能做出幾道網絡流題目.
網絡流的精髓在於建圖.
我們先從最基礎的"網絡流24題"開始.
大家先來看幾個例題理解一下建圖的基本套路
圓桌問題
假設有來自 \(n\) 個不同單位的代表參加一次國際會議。每個單位的代表數分別為 \(r_i\) 。會議餐廳共有 \(m\) 張餐桌,每張餐桌可容納 \(c_i\) 個代表就餐。 為了使代表們充分交流,希望從同一個單位來的代表不在同一個餐桌就餐。 試給出滿足要求的代表就餐方案。
這題建圖感覺還是很顯然的.
首先可以看出一個明顯的二分圖模型, 單位是一種點, 餐桌是另一種點, 代表數量可以看做是流量.
從 \(s\) 連到所有單位點, 容量為代表數量. 單位點和餐桌點之間兩兩連一條容量為 \(1\) 的邊代表"同一個單位來的代表不能在同一個餐桌就餐"的限制. 餐桌點再向 \(t\) 連一條容量為餐桌大小的邊來限制這個餐桌的人數.
這樣的話, 每一單位流量都代表着一個代表. 它們流經的點和邊就代表了它們的特征. 而容量就是對代表的限制.
回想DP, DP的特點就是抽象出一個基本對象"狀態", 然后用若干維度來描述這個狀態的特征, 根據這些特征應用題目中的限制. 而網絡流的構圖, 則是用一單位流量流動的過程來刻畫特征.
這種"把一個基本對象的特征用一單位流量從 \(s\) 流到 \(t\) 的過程來刻畫"的思路, 就是網絡流的一般建圖策略.
至於輸出方案, 我們只要看從單位點到餐桌點的邊有哪些滿載了, 一條滿載的邊 \((u,v)\) 意義就是在最優方案中一個來自 \(u\) 代表的單位的代表坐到了 \(v\) 代表的餐桌上.
當然如果最大流沒有跑滿(最大流的值不等於代表數量之和)的話肯定有代表沒被分配出去, 判定無解.
公平分配問題
把 \(m\) 個任務分配給 \(n\) 個處理器. 其中每個任務有兩個候選處理器, 可以任選一個分配. 要求所有處理器中被分配任務最多的處理器分得的任務最少. 不同任務的候選處理器保證不同.
首先我們要讓最大值最小, 容易想到二分答案.
其次我們可以看到一個明顯的二分圖模型: \(m\) 個任務和 \(n\) 個處理器. 我們從 \(s\) 連容量為 \(1\) 的邊到所有任務, 從任務連邊到候選處理器, 從候選處理器連一條容量為二分答案的邊到 \(t\) . 只要最大流達到了 \(m\) , 就說明我們成功在任務數量最多處理器的任務數量不大於二分答案的情況下分配出了 \(m\) 個任務, 執行 \(O(\log m)\) 次即可.
星際轉移問題
現有 \(n\) 個太空站位於地球與月球之間,且有 \(m\) 艘公共交通太空船在其間來回穿梭。每個太空站可容納無限多的人,而每艘太空船 \(i\) 只可容納 \(H_i\) 個人。每艘太空船將周期性地停靠一系列的太空站,例如:\(1,3,4\) 表示該太空船將周期性地停靠太空站 \(134134134\cdots\) 每一艘太空船從一個太空站駛往任一太空站耗時均為 \(1\)。人們只能在太空船停靠太空站(或月球、地球)時上、下船。 初始時所有人全在地球上,太空船全在初始站。試設計一個算法,找出讓 \(k\) 人盡快地全部轉移到月球上的運輸方案。
\(n \leq 20, k\leq 50, m\leq 13\)
在這個題目中, 我們使用圖論算法的另一個套路: 把狀態抽象為結點.
我們把 "每一天的空間站/星球" 抽象為狀態, 從 \(s\) 連一條容量為 \(k\) 的邊到第 \(0\) 天的地球, 從所有月球點連接一條容量為 \(\infty\) 的邊到 \(t\) , 然后對於第 \(i\) 個飛船, 如果第 \(d\) 天停留在空間站 \(u\) 且下一輪要去 \(v\) 空間站, 則從第 \(d\) 天的 \(u\) 向第 \(d+1\) 天的 \(v\) 連一條容量為 \(H_i\) 的邊, 計算最大流是否為 \(k\) 即可判定是否能夠完全運輸.
網絡流判定, 那么二分答案根據答案建若干層點來判定?
太慢辣!
網絡流題很多(不包括上一題)都有個特點: 當你發現某個題需要二分的時候, 它多數情況下並不用二分.
因為你的殘量網絡還是可以懟幾條邊進去接着跑的.
所以你只要從小到大枚舉答案, 每次建當天的一層點, 増廣之后判斷一下是否滿流就行了.
增量増廣一般快的一匹.
收集者的難題
Bob和他的朋友從糖果包裝里收集貼紙. 這些朋友每人手里都有一些 (可能有重復的) 貼紙, 並且只跟別人交換他所沒有的貼紙. 貼紙總是一對一交換.
Bob比這些朋友更聰明, 因為他意識到指跟別人交換自己沒有的貼紙並不總是最優的. 在某些情況下, 換來一張重復的貼紙會更划算.
假設Bob的朋友只和Bob交換 (他們之間不交換), 並且這些朋友只會出讓手里的重復貼紙來交換它們沒有的不同貼紙. 你的任務是幫助Bob算出它最終可以得到的不同貼紙的最大數量.
首先我們發現, Bob所持有的貼紙數量是一定的, 可以轉化為流量, 所以我們只要建邊體現一下交換就行了.
因為某單位流量到底是什么類型的貼紙與交換過程有關, 所以我們對於每一種貼紙都建立一個結點, 代表"流量流到這里之后即為對應類型的貼紙". 然后我們只要把可能的類型轉移帶上容量建上去就行了. 從 \(s\) 連邊到貼紙類型點代表Bob一開始擁有的貼紙, 從貼紙類型點連邊到 \(t\) 代表到此為止不再繼續交換. 注意從 \(s\) 連出的邊容量為擁有的貼紙數量, 但因為我們要最大化種類數量, 所以連到 \(t\) 的邊必須容量為 \(1\). 最大流值即為最大種類數.
最大流與最小割
有時候我們會發現用最大流的思路並不能建出模型...考慮這樣的一個題目:
看着正在被上古神獸們摧殘的花園,花園的守護之神――小Bug同學淚流滿面。然而,OIER不相信眼淚,小bug與神獸們的戰爭將進行到底!
通過google,小Bug得知,神獸們來自遙遠的戈壁。為了扭轉戰局,小Bug決定拖延神獸增援的速度。從戈壁到達花園的路徑錯綜復雜,由若干段雙向的小路組成。神獸們通過每段小路都需要一段時間。小Bug可以通過向其中的一些小路投擲小xie來拖延神獸。她可以向任意小路投擲小Xie,而且可以在同一段小路上投擲多只小xie。每只小Xie可以拖延神獸一個單位的時間。即神獸通過整段路程的總時間,等於沒有小xie時他們通過同樣路徑的時間加上路上經過的所有小路上的小xie數目總和。
神獸們是很聰明的。他們會在出發前偵查到每一段小路上的小Xie數目,然后選擇總時間最短的路徑。小Bug現在很想知道最少需要多少只小Xie,才能使得神獸從戈壁來到花園的時間變長。作為花園中可愛的花朵,你能幫助她嗎?
這™是網絡流?
流與割的關系
我們發現其實上面那個題的本質就是: 找一些邊滿足從 \(s\) 到 \(t\) 的任意路徑都必須至少經過一條這些邊, 同時讓這些選中的邊的權值最小.
或者換一個說法, 將這些選中的邊刪去之后, \(s\) 和 \(t\) 不再連通, 點集 \(V\) 被分割為兩部分 \(V_s\) 和 \(V_t\) . 我們稱點集 \((V_s,V_t)\) 為流網絡的一個割, 定義它的容量為所有滿足 \(u\in V_s, v\in V_t\) 的邊 \((u,v)\) 的容量之和.
而對於一個割 \((V_s,V_t)\), 我們定義一個流的凈流量為所有滿足 \(u\in V_s, v\in V_t\) 的邊 \((u,v)\) 上加載的流量減去所有滿足 \(v\in V_s, u\in V_t\) 的邊 \((u,v)\) 上加載的流量.
我們重新拿出最開始的那個流網絡, 我們就可以做出這樣的一個割 \((S,T)\):
不難看出,這個割的容量為 \(35\), 最大流的凈流量為 \(12+12-1=23\) .
注意到凈流量可以因為反向流量而為負, 但是容量一定是非負的 (反向邊和 \(s\rightarrow t\) 的連通性無關)
我們換一種方式來割:
不難發現凈流量依然與網絡流的流量相等, 依然是 \(23\). 而這個割的容量則是 \(23\).
為啥凈流量一直是一樣的呢? 我們可以用下面這個不太嚴謹的證明感性理解一下
根據網絡流的定義,只有源點 \(s\) 會產生流量,匯點 \(t\) 會接收流量。因此任意非 \(s\) 和 \(t\) 的點 \(u\) ,其凈流量一定為 \(0\),也即是\(\sum f(u,v)=0\)。而源點 \(s\) 的流量最終都會通過割 \((S,T)\) 的邊到達匯點 \(t\),所以網絡流的流 \(f\) 等於割的靜流 \(f(S,T)\)。
也就是說任意一個割的凈流一定都等於當前網絡的流量.
而因為割的容量將所有可能的出邊都計入了, 所以任意一個割的凈流一定都小於等於這個割的容量. 而在所有的割中, 一定存在一個容量最小的割, 它限制了最大流的上界. 於是我們得到一個結論: 對於任意一個流網絡, 其最大流必定不大於其最小割.
然而這還不夠, 我們相當於只能用最大流算出最小割的一個下界. 我們如何證明這個下界一定能取到呢?
最大流最小割定理
最小割最大流定理的內容:
對於一個網絡流圖 \(G=(V,E)\),其中有源點 \(s\) 和匯點 \(t\),那么下面三個條件是等價的:
- 流 \(f\) 是圖 \(G\) 的最大流
- 殘量網絡 \(G_f\) 不存在增廣路
- 對於 \(G\) 的某一個割 \((S,T)\) ,此時流 \(f\) 的流量等於其容量
證明如下:
首先證明 \(1\Rightarrow 2\):
増廣路算法那的基礎, 正確性顯然, 不證了(咕咕咕
然后證明 \(2\Rightarrow 3\):
假設殘留網絡 \(G_f\) 不存在增廣路,所以在殘留網絡 \(G_f\) 中不存在路徑從 \(s\) 到達 \(t\) 。我們定義 \(S\) 集合為:當前殘留網絡中 \(s\) 能夠到達的點。同時定義 \(T=V-S\)。
此時 \((S,T)\) 構成一個割 \((S,T)\) 。且對於任意的 \(u\in S,v\in T\),邊 \((u,v)\) 必定滿流。若邊 \((u,v)\) 不滿流,則殘量網絡中必定存在邊 \((u,v)\),所以 \(s\) 可以到達 \(v\),與 \(v\) 屬於 \(T\) 矛盾。
因此有 \(f(S,T)=\sum f(u,v)=\sum c(u,v)=C(S,T)\)。
最后證明 \(3\Rightarrow 1\):
割的容量是流量的上界, 正確性顯然.
於是, 圖的最大流的流量等於最小割的容量.
最小割建模舉例
花園的守護之神
就是開頭那題
首先我們發現不在最短路上的邊都沒卵用, 於是我們把它們扔進垃圾桶.
剩下的邊組成的圖中求最小割.
有了最大流最小割定理, 直接跑一遍最大流就好辣~
王者之劍
這是在阿爾托利亞·潘德拉貢成為英靈前的事情,她正要去拔出石中劍成為亞瑟王,在這之前她要去收集一些寶石。
寶石排列在一個n*m的網格中,每個網格中有一塊價值為v(i,j)的寶石,阿爾托利亞·潘德拉貢可以選擇自己的起點。
開始時刻為0秒。以下操作,每秒按順序執行
1.在第i秒開始的時候,阿爾托利亞·潘德拉貢在方格(x,y)上,她可以拿走(x,y)中的寶石。
2.在偶數秒,阿爾托利亞·潘德拉貢周圍四格的寶石會消失
3.若阿爾托利亞·潘德拉貢第i秒開始時在方格(x,y)上,則在第i+1秒可以立即移動到(x+1,y),(x,y+1),(x-1,y)或(x,y-1)上,也可以停留在(x,y)上。
求阿爾托利亞·潘德拉貢最多可以獲得多少價值的寶石
首先有一個非常重要的套路: 對於網格圖網絡流來說, 黑白染色是一個很重要的事情. 因為你需要選一部分點和 \(s\) 相連, 一部分點和 \(t\) 相連, 然后再在它們之間各種連邊來表示貢獻.
然后我們來看題...
這出題人語文真棒
因為你可以任意停留或者行動而且行動時間不受限制, 其實意思就是選了一個點之后周圍的點都不能選了...
這個時候我們引入最小割建圖的一個重要元素: 無窮邊.
無窮邊顯然是不可能出現在最小割里的, 於是一條 \((u,v)\) 容量為 \(\infty\) 的邊的意思就是: \((u,v)\) 必須連通, 不可割斷.
同時由於我們要求這個圖上的一個割, 所以上面這句話等價於: 強制 \(s\verb|-| u\) 之間在最小割中被割斷或者 \(v\verb|-|t\) 在最小割中被割斷.
我們再來看這個題. 我們可以把問題從"我們最多拿多少"轉化為"我們最少要扔掉多少". 由於相鄰的點不能同時拿, 所以我們在它們之間連接一條 \(\infty\) 邊代表扔掉任意一個. 所以總的建圖就是:
從 \(s\) 向所有白點連邊, 從所有黑點向 \(t\) 連邊, 容量均為點權. 相鄰點之間從白點向黑點連接 \(\infty\) . 容易證明這張圖的最小割值即為最少需要放棄的點的價值.
總和減去這個最小放棄值即為答案.
實際上上面的最小割求的就是二分圖最小權值覆蓋, 減出來的答案就是二分圖的最大權獨立集.
Number
有 \(n\) 個正整數,需要從中選出一些數,使這些數的和最大。
若兩個數 \(a,b\)同時滿足以下條件,則 \(a,b\) 不能同時被選
- 存在正整數\(c\),使 \(a^2+b^2=c^2\)
- \(\gcd(a,b)=1\)
首先 "滿足一定條件則不能同時被選" 是一個典型的最小割問題, 計算舍棄掉的最小值即可.
但是網絡流建圖必須要在合適的地方放 \(s\) 和 \(t\) (於是全局最小割就成了個高端問題), 咋辦?
首先我們發現偶數和偶數之間不可能有限制條件, 因為它們的 \(\gcd\) 為 \(2\), 不滿足第二個條件.
接着我們發現奇數和奇數之間不可能滿足第一個條件. 為啥?
奇數的平方和在模 \(4\) 意義下一定是 \(2\), 而完全平方數在模 \(4\) 意義下必須是 \(0/1\). 枚舉 \([0,4)\) 之間的數字就證完了.
所以它是個二分圖! 我們就可以愉快地從 \(s\) 連邊到奇數, 從偶數連邊到 \(t\) , 中間同時滿足條件的再連幾條 \(\infty\) 邊就好了...么?
emmmm...
題面漏了一句話:
\(n \leq 3000\)
然而這並不是單位容量簡單網絡所以不適用 \(O(\sqrt{V}E)\) 的復雜度證明...
GG了?
我們必須要知道, Dinic是一種敢寫就會有奇跡的算法!
它A了.
最小割
A,B兩個國家正在交戰,其中A國的物資運輸網中有N個中轉站,M條單向道路。設其中第i (1≤i≤M)條道路連接了vi,ui兩個中轉站,那么中轉站vi可以通過該道路到達ui中轉站,如果切斷這條道路,需要代價ci。現在B國想找出一個路徑切斷方案,使中轉站s不能到達中轉站t,並且切斷路徑的代價之和最小。 小可可一眼就看出,這是一個求最小割的問題。但愛思考的小可可並不局限於此。現在他對每條單向道路提出兩個問題: 問題一:是否存在一個最小代價路徑切斷方案,其中該道路被切斷? 問題二:是否對任何一個最小代價路徑切斷方案,都有該道路被切斷? 現在請你回答這兩個問題。
首先我們為了求出這個最小割肯定要先跑最大流, 跑完最大流之后的殘量網絡就是這題的突破口.
我們分析一下這個殘量網絡有什么性質:
- \(s\) 與 \(t\) 一定不在同一SCC里
- 對於某條滿流邊 \((u,v)\) , 若 \(u\) 與 \(s\) 在同一SCC, \(v\) 與 \(t\) 在同一SCC, 則它必定會出現在最小割中.
- 對於某條滿流邊 \((u,v)\) , 若 \(u\) 與 \(v\) 不在同一SCC, 則它可能出現在最小割中.
結論1非常顯然, 最大流的話 \(s\) 和 \(t\) 直接不連通更不要說在同一SCC里了.
結論2的話, 如果將一條滿足該限制的邊容量增大, 那么 \(s\rightarrow t\) 重新連通, 於是就會增加最大流的流量, 也相當於增加了最小割的容量. 所以這條邊必定會出現在最小割中.
結論3的話, 我們把SCC縮到一個點里, 得到的新圖就只有滿足條件的滿流邊了. 縮點后的任意一個割顯然就對應着原圖的一個割, 所以這些滿流邊都可以出現在最小割中.
這三條結論有時候在最小割建圖輸出方案的時候會用到.
最大權閉合子圖
給定一個有向圖, 頂點帶權值. 你可以選中一些頂點, 要求當選中一個點 \(u\) 的時候, 若存在邊 \(u\rightarrow v\) 則 \(v\) 也必須選中. 最大化選中的點的總權值.
選中一個點后可以推導出另一個點也必須選中, 同時最優化一個值, 我們考慮用最小割的 \(\infty\) 邊來體現這一點.
我們像剛剛王者之劍那道題一樣將價值轉化為代價. 這樣當不選一個正權點時相當於付出 \(val_i\) 的代價, 選中一個負權點時相當於付出 \(-val_i\) 的代價.
我們設割斷后和 \(s\) 相連的點是被選中的, 和 \(t\) 相連的點是被扔掉的, 那么當一個正權點和 \(s\) 割斷時要付出 \(val_i\) 的代價, 我們從 \(s\) 連一條容量為 \(val_i\) 的邊到這個點. 類似的, 當一個負權點和 \(t\) 割斷時(等價於和 \(s\) 相連, 也就是被選中了)要付出 \(-val_i\) 的代價, 從這個點連一條容量為 \(-val_i\) 的邊到 \(t\) 即可.
原圖中的邊不能割斷, 所以我們連 \(\infty\) 邊.
最后用所有正權點的權值和減去最小割就是答案了.
切糕
經過千辛萬苦小 A 得到了一塊切糕,切糕的形狀是長方體,小 A 打算攔腰將切糕切成兩半分給小 B 。出於美觀考慮,小 A 希望切面能盡量光滑且和諧。於是她找到你,希望你能幫她找出最好的切割方案。
出於簡便考慮,我們將切糕視作一個長 \(P\) 、寬 \(Q\) 、高 \(R\) 的長方體點陣。我們將位於第 \(z\) 層中第 \(x\) 行、第 \(y\) 列上 \((1 \le x \le P, 1 \le y \le Q, 1 \le z \le R)\) 的點稱為 \((x,y,z)\),它有一個非負的不和諧值 \(v(x,y,z)\) 。一個合法的切面滿足以下兩個條件:
- 與每個縱軸(一共有 \(P\times Q\) 個縱軸)有且僅有一個交點。即切面是一個函數 \(f(x,y)\),對於所有 \(1 \le x \le P, 1 \le y \le Q\) ,我們需指定一個切割點 \(f(x,y)\) ,且 \(1 \le f(x,y) \le R\) 。
- 切面需要滿足一定的光滑性要求,即相鄰縱軸上的切割點不能相距太遠。對於所有的 \(1 \le x,x’ \le P\) 和 \(1 \le y,y’ \le Q\) ,若 \(|x-x’|+|y-y’|=1\) ,則 \(|f(x,y)-f(x’,y’)| \le D\)∣ ,其中 \(D\) 是給定的一個非負整數。
可能有許多切面 \(f\) 滿足上面的條件,小 A 希望找出總的切割點上的不和諧值最小的那個,即 \(\sum\limits_{x,y}{v(x, y, f (x, y))}\) 最小。
給定一個立方體, 然后你要對於每一個 \((x,y)\) 選擇一個切割高度 \(f(x,y)\) , 而選中某個特定切點之后會產生一定的花費, 同時相鄰兩個切點的高度差不能超過 \(D\) , 求花費最小的切割方案.
題都告訴你要求一個花費最小的切割方案了當然要選擇最小割啦
首先我們不考慮高度差限制, 這樣的話我們從每個位置開始掛一條長鏈, 鏈上邊的權值大小表示割斷對應位置的花費.
然后愉快地貪心最小割就好了啊~
然而現在考慮高度差限制.
這時候我們再次使用 \(\infty\) 邊來解決這個限制.
假設我們建出了這樣的兩條鏈:
然后假設高度差限制是 \(1\) , 則我們割這樣的兩條邊是不合法的:
回憶 \(\infty\) 邊的作用: 強制令 \(s\verb|-|u\) 與 \(v\verb|-|t\) 中任選一個割斷. 那么我們可以這樣建一條 \(\infty\) 邊:
那么我們發現, 這種情況變得不合法了: \(s\) 和 \(t\) 依然連通.
我們發現, 只要上面那條鏈上割斷的是 \((3,4)\), 那么 \((5,6)\) 必定不會出現在最小割中. 因為下面那條鏈里顯然只會割掉 \(6\verb|-|t\) 上的某一條邊, 割兩條邊肯定沒有割一條邊更優.
或者說, \(s\verb|-|u\) 割斷的話, 對第二條鏈在這個高度上不起限制, 第二條鏈依然可以割斷任意一條邊.
如果 \(s\verb|-|u\) 未割斷的話, 一定是 \(u\verb|-|t\) 割斷了, 那么這條邊就會強制 \(v\verb|-|t\) 割斷, 那么 \(s\verb|-|v\) 割斷一定不優於是不會出現在最小割中.
上限同理, 但是實際上如果所有方向都考慮的話就是換個方向的下限限制, 建邊都一樣就可以了.
最小費用最大流
有時候我們會發現這樣的一類問題:
公司有 \(m\) 個倉庫和 \(n\) 個零售商店。第 \(i\) 個倉庫有 \(a_i\) 個單位的貨物;第 \(j\) 個零售商店需要 \(b_j\) 個單位的貨物。貨物供需平衡,即 \(\sum\limits_{i = 1} ^ m a_i = \sum\limits_{j = 1} ^ n b_j\) 。從第 \(i\) 個倉庫運送每單位貨物到第 \(j\) 個零售商店的費用為 \(c_{ij}\) 。試設計一個將倉庫中所有貨物運送到零售商店的運輸方案,使總運輸費用最少。
我們看到"貨物供需平衡"這個關鍵字其實就已經可以料想到這是個流問題了. 但是不同的是它給每單位流量都增加了一個費用, 怎么辦呢?
EK費用流
首先我們需要意識到一個事情: 最小費用最大流, 是在最大流的基礎上取最小費用, 所以我們是必須要跑到最大流的(這一點也可以在構圖中用來表示限制). 於是我們假裝沒有費用先算増廣路.
對於一條増廣路 \(p\) 來說, 我們設増廣了 \(f\) 單位的流量, 則總花費為 \(\sum\limits_{i\in p} d_if\), 實際上就等於 \(f\sum\limits_{i\in p}d_i\), 也就等於把費用看做距離的路徑長度乘以流量大小.
所以我們繼續沿用EK最大流的思路, 每次増廣以費用為邊權的最短路徑即可.
反向邊權
我們嘗試擴展最大流時的反向邊建法: 建立一個殘量為 \(0\) 的邊. 費用呢? 最大流里面所有的邊都相當於是單位邊權的費用, 所以我們可以假裝反向邊的費用和正向邊一樣~
襠燃是假的辣!
費用流里求的流量在最終累加答案的時候都乘了一個費用系數, 於是這次我們推流的時候不僅要把流推走, 還要把貢獻的費用減少. 所以反向邊的權值其實是正向邊的相反數.
於是費用流中, 負權邊不可避免. 所以我們使用SPFA來求最短路.
關於SPFA
它死了(划掉
Q: 為啥要用SPFA呢? 這玩意復雜度不是玄學么?
A: 現在有負權你不得不用了...SPFA復雜度雖然是玄學但是它還是有一個 \(O(VE)\) 的科學上界的, 所以在負權圖上跑SPFA也不失為一個很好的選擇(當然也有更優秀但是有一些限制的Dijkstra費用流, 不過一般用不着...)
Q: 你這個費用流里既然會冒出負權來, 那要是推反向邊的時候在殘量網絡里増廣出負環了怎么破?
A: EK費用流的過程每次只増廣最短路, 所以任意時刻増廣出來的流一定都是最小費用流. 但是如果殘量網絡中存在負環, 那么我們顯然可以讓一部分流量改道流經這個負環來讓費用減少, 這樣就矛盾了. 所以一定不會増廣出負環.
時間復雜度分析
因為要把一次BFS尋找増廣路換成SPFA+DFS, 於是復雜度上界從 \(O(E)\) 升高到 \(O(VE)\) , 其余的EK最大流復雜度分析理論上依然使用於此, 所以總時間復雜度上界為 \(O(V^2E^2)\).
消圈算法
這鍋本來扔給chr了...然而他覺得這很毒瘤就又丟回來了...
這個算法基於下面這個定理:
消圈定理
流量為 \(f\) 的流是最小費用流當且僅當對應的殘量網絡中不存在負費用增廣圈。
感性證明:
如果在一個流網絡中求出了一個最大流,但對於一條增廣路上的某兩個點之間有負權路,那么這個流一定不是最小費用最大流,因為我們可以讓一部分流從這條最小費用路流過以減少費用,所以根據這個思想,可以先求出一個最大初始流,然后不斷地通過負圈分流以減少費用,直到流網絡中不存在負圈為止。關鍵在於負圈內所有邊流量同時增加是不會改變總流量的,卻會降低總費用。
於是我們就可以直接先跑一遍最大流, 然后在殘量網絡里用 Bellman-Ford 找負環然后沿着負環増廣一發就可以了. 當然如果用SPFA的話大概會跑得比 \(\varTheta(VE)\) 的 Bellman-Ford 要快點吧.
網上說按照一定順序消圈的話復雜度是 \(O(VE^2\log V)\) 的...但是本來這不是我的鍋所以並沒有仔細搞
具體實現找chr鋦鍋.
ZKW費用流
其實ZKW費用流和EK費用流的關系就跟Dinic和EK最大流的關系差不多...
就是加了個對所有符合 \((u,v)\) 在最短路上的邊進行多路増廣...
但是由於存在負權, 所以増廣時可行的邊組成的圖並不像Dinic那樣是分層圖.
Dinic的BFS
部分直接改成SPFA求最短路就好了, 這個沒啥好說的.
DFS
部分要注意一點, 因為可行邊組成的圖(以下簡稱"可行圖")並不一定是DAG, 於是我們可能可以從一個費用為負的邊跑回原來的地方, 於是就會在一個 \(0\) 環上轉來轉去死遞歸.
解決方案是加一個 vis
數組, 只要DFS到了這個點就打個標記, 同時在DFS的時候判斷一下出邊是否會跑到一個已經打了標記的點, 如果沒有打上標記再DFS.
為了保證多路増廣的優越性, 這個 vis
需要在回溯時撤銷.
代碼實現
以下是 LOJ #102 最小費用流的AC代碼
#include <bits/stdc++.h>
const int MAXV=5e2+10;
const int MAXE=1e5+10;
const int INFI=0x7F7F7F7F;
struct Edge{
int from;
int to;
int dis;
int flow;
Edge* rev;
Edge* next;
};
Edge E[MAXE];
Edge* head[MAXV];
Edge* top=E;
int v;
int e;
int p,m,f,n,s;
int dis[MAXV];
bool vis[MAXV];
bool SPFA(int,int);
int DFS(int,int,int);
void Insert(int,int,int,int);
std::pair<int,int> Dinic(int,int);
int main(){
scanf("%d%d",&v,&e);
for(int i=0;i<e;i++){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
Insert(a,b,d,c);
}
std::pair<int,int> ans=Dinic(1,v);
printf("%d %d\n",ans.first,ans.second);
return 0;
}
std::pair<int,int> Dinic(int s,int t){
std::pair<int,int> ans;
while(SPFA(s,t)){
int flow=DFS(s,INFI,t);
ans.first+=flow;
ans.second+=flow*dis[t];
}
return ans;
}
int DFS(int s,int flow,int t){
if(s==t||flow<=0)
return flow;
int rest=flow;
vis[s]=true;
for(Edge* i=head[s];i!=NULL;i=i->next){
if(i->flow>0&&dis[s]+i->dis==dis[i->to]&&(!vis[i->to])){
int k=DFS(i->to,std::min(rest,i->flow),t);
rest-=k;
i->flow-=k;
i->rev->flow+=k;
if(rest<=0)
break;
}
}
vis[s]=false; // 這里不太對頭
return flow-rest;
}
bool SPFA(int s,int t){
memset(dis,0x7F,sizeof(dis));
std::queue<int> q;
vis[s]=true;
dis[s]=0;
q.push(s);
while(!q.empty()){
s=q.front();
for(Edge* i=head[s];i!=NULL;i=i->next){
if(i->flow>0&&dis[s]+i->dis<dis[i->to]){
dis[i->to]=dis[s]+i->dis;
if(!vis[i->to]){
vis[i->to]=true;
q.push(i->to);
}
}
}
q.pop();
vis[s]=false;
}
return dis[t]<INFI;
}
inline void Insert(int from,int to,int dis,int flow){
// printf("Insert %d -> %d : dis=%d flow=%d\n",from,to,dis,flow);
top->from=from;
top->to=to;
top->dis=dis;
top->flow=flow;
top->rev=top+1;
top->next=head[from];
head[from]=top++;
top->from=to;
top->to=from;
top->dis=-dis;
top->flow=0;
top->rev=top-1;
top->next=head[to];
head[to]=top++;
}
關於當前弧優化
大家發現我的代碼里並沒有加當前弧優化. 為什么?
剛剛說到了, ZKW費用流有一個sb特點就是可行圖不是分層圖. 那么就有可能有這種操作(下面只畫可行圖, 沒有帶權):
然后我們DFS増廣, 可能會遇到這樣的東西:
我們發現 \((4,2)\) 這條邊到達了一個已經被標記過的點, 於是它不會產生流量貢獻. 但是如果我們加了當前弧優化, 我們就會在以后訪問到 \(4\) 結點時跳過 \((4,2)\) 這條出弧, 於是我們就否定掉了下面這種情況:
於是我們DFS的過程就會提前認為當前已經阻塞, 返回進行新一輪SPFA. 於是我們便進行了多余的SPFA操作, 這與多路増廣"為了減少BFS次數"的出發點相悖, 同時也不能保證一個較低的時間復雜度.
實測結果的話, 使用LOJ #102的最后三個測試點, 發現不加當前弧優化時SPFA執行次數為 \(400\) 左右, 而加入當前弧優化之后執行次數上升到了 \(2700\) 次, 實際運行時間慢了一倍多.
時間復雜度分析
證了證發現因為不能當前弧優化所以並不能保證比EK更優的時間復雜度...但是可以假裝它上界在非源非匯點度數比較小的時候可以有一個接近 \(O(VE^2)\) 的依然很松的上界. 實測在LOJ板子上比EK費用流要快, 網絡流構圖可能會更快一些.
還有就是關於 visit
標記的清空問題. 上面的板子里在DFS結束之后清空了 visit
標記, 這在一些情況下會讓程序變快, 但是有些情況下可能會被卡成指數(捂臉)...建議大家DFS后不清空上面的 visit
標記而是在每次DFS前清空.
Dijkstra費用流
為啥要講一下這玩意呢?
因為這玩意可以提高你的費用流暴力在費用流轉貪心的題中得到的期望分數
整個的過程和EK/ZKW其實是一樣的, 不過這次把最短路換成Dijkstra了.
為啥能用Dijkstra呢? 它其實基於一個事實: 你已經求過原來的一個最短路了, 而最短路是滿足三角形不等式的. 其次加入的反向邊的權值都剛好是正向邊的相反數.
我們為每個點\(i\)賦點權\(h[i]\)為\(dis[i]\),並視每條連接\(u,v\)的邊\(i\)的邊權\(w'[i]\)為\(w[i]+h[u]-h[v]\),由於對於任意兩點\(u,v\),有\(h[u]-h[v]>=w[u][v]\),所以\(w'[i]>=0\),這樣一來新圖上的\(dis'[i]\)就等於\(dis[i]+h[S]-h[i]\)(對於路徑上除起點和終點以外的點\(i\),其入邊的\(-h[i]\)與出邊的\(+h[i]\)抵消),由於每次跑最短路時\(h[i]\)都是不變的,所以求出了\(dis'[i]\)也就求出了\(dis[i]\)(\(dis[i]=dis'[i]-h[S]+h[i]\),其實很顯然\(h[S]=0\))
但是跑完之后需要加入反向邊,原來的\(h[i]\)可能會不適用,所以我們需要更新\(h[i]\) 對於最短路上每一條連接\((u,v)\)的邊,顯然有
\[dis'[u]+w'[u][v]=dis'[v] \]從而
\[dis'[u]+h[u]-h[v]+w[u][v]=dis'[v] \]\[(dis'[u]+h[u])-(dis'[v]+h[v])+w[u][v]=0 \]\[∵w[u][v]=-w[v][u] \]\[∴(dis'[v]+h[v])-(dis'[u]+h[u])+w[v][u]=0 \]所以我們只要對於每個點\(i\)將\(h[i]\)加上\(dis'[i]\)即可.
所以整個的實現就是: 先SPFA跑最短路算出 \(h\) , 然后在Dijkstra中把參考的邊權加上一個\(h[u]\)再減去一個\(h[v]\), 最后把計算出的 \(dis\) 累加上 \(h\) 就行了.
最小費用最大流建模舉例
當你會了費用流之后你能做的題就會瞬間多一個數量級了...
南極科考旅行
小美要在南極進行科考,現在她要規划一下科考的路線。
地圖上有 N 個地點,小美要從這 N 個地點中 x 坐標最小的地點 A,走到 x 坐標最大的地點 B,然后再走回地點 A。
請設計路線,使得小美可以考察所有的地點,並且在從 A 到 B 的路程中,考察的地點的 x 坐標總是上升的,在從 B 到 A 的過程中,考察的地點的 x 坐標總是下降的。
求小美所需要走的最短距離(歐幾里得距離)。
3 <= N <= 300
1 <= x <= 10000, 1 <= y <= 10000
乍一看好像是DP?
實際上網絡流是可以做的
首先肯定是要按橫坐標排序, 然后我們發現一去一回的方向並沒有什么卵用, 找到一個環和找到兩條路徑是等價的. 這樣我們可以發現, 每個結點都必須選擇兩條邊, 其中 \(x\) 最小的結點兩條都是出邊, 最大的結點兩條都是入邊, 其他結點一條入邊一條出邊. 我們可以嘗試分配這些入度和出度讓總費用最小. 這樣的話我們可以得到這樣的構圖:
其中較粗的邊容量為 \(2\), 較細的邊容量為\(1\), \(s\) 連出的邊和連向 \(t\) 的邊費用為 \(0\) , 實際結點間邊的距離即為歐幾里得距離.
餐巾
一個餐廳在相繼的 \(n\) 天里,每天需用的餐巾數不盡相同。假設第 \(i\) 天需要 \(r_i\) 塊餐巾。餐廳可以購買新的餐巾,每塊餐巾的費用為 \(P\) 分;或者把舊餐巾送到快洗部,洗一塊需 \(M\) 天,其費用為 \(F\) 分;或者送到慢洗部,洗一塊需 \(N\) 天,其費用為 \(S\) 分(\(S < F\))。
每天結束時,餐廳必須決定將多少塊臟的餐巾送到快洗部,多少塊餐巾送到慢洗部,以及多少塊保存起來延期送洗。但是每天洗好的餐巾和購買的新餐巾數之和,要滿足當天的需求量。
試設計一個算法為餐廳合理地安排好 \(n\) 天中餐巾使用計划,使總的花費最小。
這道題使用了另一個費用流套路: 強行補充流量.
首先按照以往的套路, 我們會選擇用一單位流量來表示一塊餐巾的整個生命周期: 從被購買到被使用再到被清洗最后被丟棄.
於是我們從 \(s\) 連接容量為 \(\infty\) 費用為購買餐巾單價的邊到每天的決策點, 然后再亂搞?
然而這題直接這么建模會GG.
注意最小費用最大流是在最大流基礎上最小費. 如果餐巾量和流量相等的話那可就變成買的餐巾越多越好了.
接着我們發現這題的最大難點在於: 洗了之后的餐巾可以再用.
於是我們不再用一單位流量流經的邊來表示花費: 我們用每一單位流量表示一個餐巾被用了一次. 而這個一單位流量的來源則用來表達這一份餐巾的花費, 不管是買的還是洗的還是從地上撿的. 於是我們就可以用最大流這個限制來強制每天的餐巾必須夠用.
其次, 因為每天都會產生一定量的臟餐巾, 我們直接從源點 \(0\) 費用把這些流量送達指定時間段日期之后的決策點來決定是否要洗. 因為網絡流有流守恆的限制, 我們可以像 "抽水" 一樣來控制以前的決策.
但是直接連邊給以后的決策點還是會GG. 因為我們發現因為轉運的過程不能增加費用, 於是我們直接 \(0\) 費用把 \(s\) 連到了 \(t\).
如果我們直接從 \(s\) 連一些費用為快/慢洗的邊的話又無法控制它們的總流量了(你總不能洗出來的比當天用的還多吧)
這個時候我們按照套路選擇拆點. 我們拆一些新的點負責把洗出來的臟餐巾流量轉運到后面, 從 \(s\) 連接一條容量為當天餐巾用量的邊到這個 tRNA 點, 然后從這個 tRNA 點出發連接兩條費用為洗餐巾花費的邊到當天快洗/慢洗洗完那天的決策點.
不過因為洗完的餐巾以后還能用, 為了體現這一點, 每天的決策點還應該向第二天連一條 \(0\) 費用 \(\infty\) 容量的邊.
於是總的建圖就是長這樣的:
負載平衡
公司有 \(n\) 個沿鐵路運輸線環形排列的倉庫,每個倉庫存儲的貨物數量不等。如何用最少搬運量可以使 \(n\) 個倉庫的庫存數量相同。搬運貨物時,只能在相鄰的倉庫之間搬運。
注1: 搬運量即為貨物量與搬運距離之積.
注2: (大概) 保證貨物數量的總和是 \(n\) 的倍數.
這道題建圖其實比較直觀.
首先求出目標貨物數量(也就是平均數), 然后我們發現, 對於 \(x_i>\bar{x}\), 它一定需要向外運輸, 我們從 \(s\) 引一條流量為 \(x_i - \bar x\) 的邊到這個點. 而對於 \(x_i < \bar x\), 它一定是要接受貨物的. 於是我們引一條流量為 \(\bar x - x_i\) 的邊到 \(t\) , 最后把相鄰的倉庫之間都連一條費用為 \(1\) 容量為 \(\infty\) 的邊跑費用流就可以了.
最長k可重區間集
給定實直線 \(L\) 上 \(n\) 個開區間組成的集合 \(I\),和一個正整數 \(k\),試設計一個算法,從開區間集合 \(I\) 中選取出開區間集合 \(S\in I\),使得在實直線 \(L\) 的任何一點 \(x\),\(S\) 中包含點 \(x\) 的開區間個數不超過 \(k\) 。且 \(∑\limits_{z\in S}|z|\) 達到最大。這樣的集合 \(S\) 稱為開區間集合 \(I\) 的最長 \(k\) 可重區間集。 \(∑\limits_{z∈S}|z|\) 稱為最長 \(k\) 可重區間集的長度。 對於給定的開區間集合 \(I\) 和正整數 \(k\),計算開區間集合 \(I\) 的最長 \(k\) 可重區間集的長度。
首先因為是實數所以得離散化...這可真蠢...
然后由於這是開區間, 我們不需要考慮端點覆蓋的次數, 於是我們可以對於每個區間, 從左端點到右端點連一條容量為 \(1\) , 費用為區間價值 (也就是長度) 的邊, 然后從源點到第一個點/從第 \(i\) 個點到第 \(i+1\) 個點/從最后一個點到匯點都連一條容量為 \(k\) 費用為 \(0\) 的邊, 跑最大費用最大流即可.
撕烤這樣為啥是對的?
每個點最多覆蓋 \(k\) 次, 而一次覆蓋相當於一次分流, 最多分流 \(k\) 次(因為你並不能搞出負流來).
這個用分流次數來表示限制的思想有時候也會用到.
平方費用最小費用最大流
給定一個流網絡, 每條邊 \((u,v)\) 有一個容量 \(c\) 和一個費用系數 \(w\) . 當邊 \((u,v)\) 上加載了 \(f\) 單位流量的時候, 產生的花費為 \(f^2w\) . 求最小費用最大流.
\(c\leq 100\)
題意非常明確, 但是這費用並不是線性的...
這題用到了費用流建圖的一個技巧: 拆費用. 我們把一條 \((u,v)\) 的容量為 \(c\) 的平方費用邊拆成 \(c\) 條容量為 \(1\) 的線性費用邊. 就像這樣:
第 \(i\) 條邊的費用正好是平方費用邊加載上第 \(i\) 單位流量的時候產生的費用貢獻.
因為 \(c\) 並不大, 所以直接按照上面方法暴力建邊就可以了.