[圖論入門]網路最大流 - 增廣路演算法
#1.0 基本概念
先來介紹一下這個基本概念。
網路流是演算法競賽中的一個重要的模型,它分為兩部分:網路和流。
網路,其實就是一張有向圖,其上的邊權稱為容量。額外地,它擁有一個源點和匯點。
流,顧名思義,就像水流或電流,也具有它們的性質。如果把網路想象成一個自來水管道網路,那流就是其中流動的水。每條邊上的流不能超過它的容量,並且對於除了源點和匯點外的所有點(即中繼點),流入的流量都等於流出的流量。
源點可以無限量的向外提供流量,而匯點可以無限量的接受流量。
#2.0 最大流
這是一個比較常見的問題,也是本篇部落格主要討論的問題。
假設源點提供的流量足夠多,問匯點最多可以接收到多少流量。
#2.1 Ford-Fullkerson 演算法
\(\texttt{Ford-Fullkerson}\) 演算法(\(\texttt{FF}\) 演算法)是一個最大流的基礎演算法,其核心思想為尋找圖中的增廣路(Augmenting Path),實際就是網路中從源點到匯點的仍有剩餘流量的路徑。
我們來看一個例子:
在上圖中,\(1\to3\to2\to4\) 是一條增廣路,我們可以用這條路來更新殘量網路,這時網路變為了
但是不難發現,我們如果開始選擇 \(1\to3\to4,\ 1\to2\to4\) 這兩條增廣路,所得到的答案會更優,所以我們如果想要反悔這一操作,那麼,我們可以加入反向邊。
反向邊最初的容量為 \(0\),但是如果該反向邊對應的邊容量減少了 \(a\)
於是就有了這樣一條增廣路
然後我們這張網路的最大流就求得了。
\(\texttt{Ford-Fullkerson}\) 找增廣路的過程是 \(\texttt{DFS}\),這裡不加以實現。因為一(jue)般(dui)用不到。
#2.2 Edmond-Karp 演算法
實際上,\(\texttt{Edmond-Karp}\) 演算法(\(\texttt{EK}\) 演算法)本身只是 \(\texttt{FF}\) 演算法的 \(\texttt{BFS}\) 實現。但由於 \(\texttt{DFS}\) 找到的增廣路可能是七拐八繞的,而 \(\texttt{BFS}\)
當然,反向邊的編號可以是運用成對變換的方法,即 \(0\ \hat{}\ 1=1,1\ \hat{}\ 1=0,2\ \hat{}\ 1=3,3\ \hat{}\ 1=1,\cdots\) 同時,我們也需要記錄我們找到的增廣路,為了更新殘量網路。時間複雜度為 \(O(ve^2).\)
const ll N = 100010;
const int INF = 0x3fffffff;
struct Edge{
int u,v;
int nxt;
ll w;
};
Edge e[N << 1];
ll n,m,cnt,head[N],vis[N],incf[N],pre[N],s,t,maxflow;
inline ll Min(const ll &a,const ll &b){
return a < b ? a : b;
}
inline void add(const int &u,const int &v,const ll &w){
e[cnt].u = u;e[cnt].v = v;e[cnt].w = w;
e[cnt].nxt = head[u];head[u] = cnt ++;
e[cnt].u = v;e[cnt].v = u;e[cnt].w = 0;
e[cnt].nxt = head[v];head[v] = cnt ++;
}
inline bool EK(){
mset(vis,0);queue <int> q;
q.push(s);vis[s] = true;
incf[s] = INF;
while (q.size()){
int x = q.front();q.pop();
for (int i = head[x];i != -1;i = e[i].nxt)
if (e[i].w){
if (vis[e[i].v]) continue;
incf[e[i].v] = Min(incf[x],e[i].w);
pre[e[i].v] = i;
q.push(e[i].v);vis[e[i].v] = true;
if (e[i].v == t) return true;
}
}
return false;
}
inline void update(){
int x = t;
while (x != s){
int i = pre[x];
e[i].w -= incf[t];
e[i ^ 1].w += incf[t];
x = e[i ^ 1].v;
}
maxflow += incf[t];
}
int main(){
mset(head,-1);
scanf("%d%d%d%d",&n,&m,&s,&t);
for (int i = 1;i <= m;i ++){
int u,v;ll w;
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
}
while (EK()) update();
printf("%lld",maxflow);
return 0;
}
#2.3 Dinic 演算法
\(\texttt{EK}\) 演算法似乎已經能滿足我們了。。。嗎?如果是稠密圖,看起來 \(\texttt{EK}\) 演算法就要爆炸了,那麼我們就需要更優的演算法。
\(\texttt{Dinic}\) 演算法採用了分層圖的思想,將圖中的每一個點按照距離源點的遠近進行分層。每次查詢增廣路時僅找比當前節點層數加一的節點進行擴充套件。
分層時採用 \(\texttt{BFS}\),保證分層遠近的正確性,一個點能被分層的前提是通向這個點的邊仍有殘量,這樣可以保證沒有殘量的邊不會被遍歷,當我們分層到匯點就可以結束了,因為其他的沒被分層的點所在層數必然比匯點要遠,根據我們上面的實現思路,這樣的點是不會擴充套件到匯點的。
擴展采用 \(\texttt{DFS}\) 實現,所以不用 update()
函式,可直接進行更新。注意進行一次分層後可以進行多次找增廣路的操作,直到不能找到新的流量。
還有一點小優化:
- 如果在 \(\texttt{DFS}\) 的過程中,發現某一點返回的可拓展的流量為 \(0\),那麼就可以將該點的層數設定為 \(0\),因為顯然已經不可能通過該點更新殘量網路了。
- 記錄一個
now[x]
,表示點 \(x\) 當前可以從哪一條相連的邊開始進行探索,在後面的過程中,到點 \(x\) 便可以從編號為now[x]
的邊開始。原因也很簡單:探索某條邊時必然會將當前分層圖上從這條邊能得到的流量都拿到,之後再次探索這條邊就沒有意義了。
const int N = 100010;
const int INF = 0x3fffffff;
struct Edge{
int u,v;
int nxt;
ll val;
};
Edge e[N << 1];
int n,m,s,t;
ll maxflow;
int cnt,head[N],d[N],now[N];
queue <int> q;
inline void add(const int &u,const int &v,const ll &w){
e[cnt].u = u;e[cnt].v = v;e[cnt].val = w;
e[cnt].nxt = head[u];head[u] = cnt ++;
e[cnt].u = v;e[cnt].v = u;e[cnt].val = 0;
e[cnt].nxt = head[v];head[v] = cnt ++;
}
inline bool bfs(){
mset(d,0);
while (q.size()) q.pop();
q.push(s);d[s] = 1;now[s] = head[s];
while (q.size()){
int x = q.front();q.pop();
for (int i = head[x];i != -1;i = e[i].nxt)
if (e[i].val && !d[e[i].v]){
q.push(e[i].v);
now[e[i].v] = head[e[i].v];
d[e[i].v] = d[x] + 1;
if (e[i].v == t) return true;
}
}
return false;
}
inline ll dinic(int x,ll flow){
if (x == t) return flow;
ll rest = flow,k,i;
for (i = now[x];(i != -1) && rest;i = e[i].nxt){
if (e[i].val && d[e[i].v] == d[x] + 1){
k = dinic(e[i].v,min(rest,e[i].val));
if (!k) d[e[i].v] = 0;
e[i].val -= k;e[i ^ 1].val += k;
rest -= k;
}
now[x] = i;
}
return flow - rest;
}
int main(){
mset(head,-1);
scanf("%d%d%d%d",&n,&m,&s,&t);
for (int i = 1;i <= m;i ++){
int u,v;ll w;
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
}
ll flow = 0;
while (bfs()) while (flow = dinic(s,INF))
maxflow += flow;
printf("%lld",maxflow);
return 0;
}
時間複雜度為 \(O(v^2e)\)。值得注意的一點是,\(\texttt{Dinic}\) 演算法在 二分圖最大匹配 上使用的時間複雜度是 \(O(v\sqrt e)\),優於匈牙利演算法。
#3.0 將二分圖匹配最大匹配轉化為最大流
對於一張二分圖,我們將左圖和右圖之間的邊容量設為 \(1\),在左邊加一個源點,向左圖所有點連一條容量為 \(1\) 的邊,在右邊加匯點,所有右圖的點向匯點連一條容量為 \(1\) 的邊,在該圖上跑最大流,得到的結果就是該圖的最大匹配。
正確性顯然,不證。