圖論4——探索網絡流的足跡:Dinic算法
1. 網絡流:定義與簡析
1.1 網絡流是什麽?
網絡流是一種“類比水流的解決問題方法,與線性規劃密切相關”(語出百度百科)。
其實,在信息學競賽中,簡單的網絡流並不需要太高深的數學知識。
首先我們需要知道一些名詞是什麽意思:
- 點(\(node\))。就是一個節點。點集通常用\(V\)表示。其中,有一個源點\(s\)和一個匯點\(t\),所有的流都從源點\(s\)出發,經過一些邊之後到達匯點\(t\)。
- 邊(\(edge\))。這個東西和大家在其他圖論知識中所用到的差不多,用於連接兩個點。邊集通常用\(E\)表示。圖\(G(V,E)\)表示由點集\(V\)和邊集\(E\)組成的圖\(G\)。在接下來講到的有關網絡流的問題中,邊都是有向的。
- 容量(\(capacity\),簡稱\(cap\))。對於每一條邊,我們都有一個容量,表示這條邊最多能經過的流。
1.2 問題簡述
顯然,根據如上定義,到達匯點\(t\)的流是有限的。網絡流問題(NetWork Flow Problem
)就是如何合理安排一種方式,使得從源點\(s\)到匯點\(t\)的流最多。
感覺枯燥嗎?其實我們可以這麽感性的理解。
你的家裏有\(10^{10^{10^{10}}}\)箱蘋果,然後你要通過一些高速公路把他們運到你的親戚家。由於某些限制,連接\(u\)和\(v\)的高速公路\((u,v)\)一天只能允許載有不超過\(cap(u,v)\)的卡車通過。顯然,一天之內,能運送的蘋果數量是有限的,因為這些高速路的限制只能讓你運送其中的一些。那麽如何安排這些蘋果的運送方式,使得一天之內最後運到你親戚家的蘋果最多?
顯然,即使你把所有的\(10^{10^{10^{10}}}\)箱蘋果都運送給你的親戚,但是最後絕大多數都會在高速路收費站被攔下來——超載(並且最後被工作人員吃掉)。最多能夠送給親戚的蘋果數(即在最優方案下到達\(t\)的流)就稱之為最大流。
2. 網絡流:嘗試與解決
2.1 貪心分析
A:這個問題不是可以貪心一遍然後\(O(n+m)\)就解決了嗎?
B:怎麽貪心?你做一遍試一下啊!
A:就是每一次都盡量多地往匯點送,如果下一條邊不能再運送這麽多的容量就分到其他條邊不就好了嗎?
B:(隨手一個圖)
。
A:然後我開始詳細講我的貪心算法了。
\(\quad\)首先我們從\(s\to 2\to 5\to t\)
\(\quad\)然後……\(s\to 3\to 5\to t\)就不行了……
\(\quad\)最後\(s\to 4\to 5\to t\)還有\(7\)箱。這樣合計\(11\)箱。(強行掩飾)那你有更好地解法嗎?
B:顯然有。如果你沒有這麽貪心,從\(s\to 2\to 5\to t\)只運送\(2\)箱蘋果,剩下兩箱從\(s\to 2\to 6\to t\)走,這樣\(s\to 3\to 5\to t\)還可以運一箱。最後\(s\to 4\to 5\to t\)還有\(7\)箱。合計\(2+2+1+7=12\)箱。
通過這段對話,我們發現,由於輸入順序未知,貪心很容易產生問題(盡管在這張圖中你有一半的可能得到正確答案,而在數據量大的時候,不出錯的概率很小)。所以我們考慮正確的解法。
2.2 樸素算法
在學習樸素算法之前,我們需要補充一個網絡流中常用到的定理。
如果設\(f(u,v)\)為實際從這條邊上走過的流量大小,則:
- 容量原則:\(f(u,v)\le cap(u,v)\).這個是顯然的,你不可能實際流量比限制容量還大(否則你會因為超載被警察叔叔開罰單)。
- 反對稱性:\(f(u,v)=-f(v,u)\).這個也是顯然的,你送給親戚\(10\)個蘋果不就相當於你從親戚那裏拿到了\(-10\)個蘋果嗎?(盡管聽上去總感覺好像不太正常)
- 流量守恒:當\(u\ne s,t\)時,如果規定邊\((u,v)\)和\((v,u)\)都不存在時\(f(u,v)=f(v,u)=0\)的話,那麽\(\displaystyle\sum^{v\in V}_ {v} f(u,v) =0\)。這個並不是那麽好理解,簡單來說,因為從\(s\)點流出的流最終都匯入\(t\),所以中間所有邊都不會有剩下的流沒有出去。只有\(s\)和\(t\)會“制造”和“接受”流量。(你不想讓高速路的工作人員把你的蘋果吃掉,所以一定選取剛好的蘋果給親戚;自然親戚會全部收到,所以高速路的工作人員沒有辦法攔下你的蘋果)。
有了這三條定理,我們就開始講樸素算法。
我們首先給每一條邊連上反向邊。這條反向邊的性質就像上面定理2“反對稱性”中的反向邊一樣。
由於目前的網絡流算法都基於增廣路思想,我們還是要介紹一下增廣路。
增廣路思想
增廣路定義:一條從源點到匯點的路徑,使得路徑上任意一條邊的殘量\(>0\)(註意是\(>\)而不是\(\ge\),這意味著這條邊還可以分配流量),這條路徑便稱為增廣路。
我們設\(g_{u,v}\)表示\((u,v)\)這條邊上的殘量,即剩余流量。
- 找到一條增廣路,記這條路徑上的邊集為\(P\)。
- 記\(flow=\displaystyle\min_{(u,v)}^{(u,v)\in P} g_{u,v}\).
- 將這條路徑上的每一條有向邊\(u,v\)的殘量\(g_{u,v}\)減去\(flow\),同時對於反向邊\(v,u\)的殘量加上\(flow\)。
- 重復上述過程,直到找不出增廣路,此時我們就找到了最大流。
為什麽反向邊要加上\(flow\)呢?因為對於下面這個例子:
所以需要加上\(flow\)。
復雜度分析
這個復雜度是\(O(nm^2)\),而且往往不會少跑很多,所以只能用過\(n,m\le 300\)的數據。
Dinic算法
我們發現樸素算法有的時候會跑得特別特別的慢,因為增廣路徑查找得很不優秀。比如:
如果程序很不友好的在\((3,4)\)和\((4,3)\)中來回跑,那麽顯然復雜度會高到離譜。所以在Dinic中,我們引入分層圖的概念。
分層圖
對於每一個點,我們根據從源點\(s\)開始的bfs
序,為每一個點分配一個深度\(dep_i\),然後通過不斷dfs
尋找增廣路,每一次dfs
當且僅當\(dep_v=dep_u+1\)是才從\(u\)到\(v\),這樣上面的情況就避免了。
復雜度估計
那麽Dinic
的復雜度是多少呢?答案是\(O(n^2m)\)。事實上這只是最差情況下的估計,實際上遠沒有這麽大,對於\(n,m\le 10^5\)的數據往往是可以輕松過的。
代碼:
下面是代碼。其中\(d\)數組即\(dep_i\),每次通過bfs構造層次圖,然後dfs增廣。
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;
const int MAXN=100010;
const int INF=0x3f3f3f3f;
class Dinic
{
private:
struct edge
{
int from,to,cap,flow;
};
vector<edge>e;
vector<int>f[MAXN];
queue<int>q;
bool vis[MAXN];
int d[MAXN];
int cur[MAXN];
bool bfs()
{
memset(vis,0,sizeof(vis));
while(!q.empty())
q.pop();
q.push(s);d[s]=0;vis[s]=1;
while(!q.empty())
{
int x=q.front();q.pop();
for(int i=0;i<f[x].size();i++)
{
edge &y=e[f[x][i]];
if(!vis[y.to]&&y.flow<y.cap)
{
vis[y.to]=1;
d[y.to]=d[x]+1;
q.push(y.to);
}
}
}
return vis[t];
}
int dfs(int x,int a)
{
if(x==t)
return a;
if(a==0)
return 0;
int flow=0,r;
for(int &i=cur[x];i<f[x].size();i++)
{
edge &y=e[f[x][i]];
if(d[x]+1==d[y.to]&&(r=dfs(y.to,min(a,y.cap-y.flow)))>0)
{
y.flow+=r;
e[f[x][i]^1].flow-=r;
flow+=r;
a-=r;
if(a==0)
break;
}
}
return flow;
}
public:
int n,m,s,t;
void adde(int u,int v,int cap)
{
e.push_back((edge){u,v,cap,0});
e.push_back((edge){v,u,0,0});
this->m=e.size();
f[u].push_back(m-2);
f[v].push_back(m-1);
}
int maxflow()
{
int res=0;
while(bfs())
{
memset(cur,0,sizeof(cur));
res+=dfs(s,INF);
}
return res;
}
};
如果需要使用這個模板,只需要輸入\(n,s,t\),而\(m\)將根據加邊次數計算。
這個模板的技巧在於,存反向邊的時候利用了位運算\(xor\)的技巧:如果邊的編號從\(0\)計數,那麽相鄰的邊編號就是\(2x,2x+1\),在計算機中\(2x\ xor\ 1=2x+1\),而\((2x+1)\ xor\ 1=2x\)。這樣可以很方便的讀取反向邊。
圖論4——探索網絡流的足跡:Dinic算法