網絡最大流問題就是給你一個有向圖,告訴你一個源點與一個匯點,並給每一條邊一個最大流量,需要你求出從源點最多能夠發出多少單位流量到匯點(哎呀我也說不清,就是給你一些或大或小的管道(每個管道都有最大秒流量),一些中轉站,一座供水塔以inf單位每秒的速度供水,問你家每秒最多得到多少單位水(中轉站、供水塔、你家由管道連通))
很顯然看起來我們可以從源點跑dfs,只要到下一節點的邊的邊權>0就跑到下一節點,一路跑過去,直到匯點,然後答案加上這一路上最小的管道流量,這一條路徑上所有管道減去這個流量,然後就沒了。
但是這樣子是有問題的。(藍模板怎麼可能這麼簡單)
舉個栗子:(將dfs路徑用紅色表示)
然後你會發現這樣的答案是2,然而正確的答案是4(s-2-t,s-3-t)
(不要說你的存邊是2-t比2-3優先,調個位置就卡掉了)
爲什麼會這樣?
因爲上圖中第一條dfs路徑把原本應是第二條路徑的一條邊給“佔用”了。
計算機可不是人,它無法判斷到底該怎麼跑dfs,所以這時候就需要我們人爲地給計算機一個“反悔”的機會。
重點:怎麼讓計算機“反悔”?
給每條邊建邊權爲0的反向邊,當每次跑到匯點時,在回溯給dfs路徑上的邊權減去最小邊權的時候還要給反向邊加上最小邊權。
先看效果:
然後答案就神奇地變成4了!
爲什麼?
用整體的思想來看:如果我正着經過一條邊,再反着經過,是不是相當於沒經過?好吧,我承認這不是很好理解
這一種“抵消”的思想應用十分廣泛,比如洛谷P1792 [國家集訓隊]種樹,就是一種利用類似“抵消”的方法來實現的可反悔的貪心。
沒理解也沒關係,反正就是要反向建邊,題做多了就理解了QAQ
使用了這種“抵消”思想的dfs,大概就是所謂的EK算法了。(即不停的尋找增廣路然後操作)
然後再介紹一種優化算法:Dinic算法(本質上其實差不多,只不過是一次尋找多條增廣路並更新圖)
PS:增廣路即能夠使答案增加的dfs路徑
大概步驟就是:
1.bfs——給之後尋找增廣路的dfs提供一個bfs序作爲擴展增廣路的依據,同時判斷是否還存在增廣路(如果遍歷不到匯點,就退出輸出答案)。
2.dfs——尋找增廣路(注意這裏一次尋找了多條)並更新答案。
3.重複1
dinic的代碼實現:(貼上我洛谷P3376 【模板】網絡最大流的代碼)
#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;
int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans;
void insert(int x,int y,int z) {
nt[++e]=bg[x];
to[e]=y;
w[e]=z;
bg[x]=e;
}
void bfs() {
int i,f,l,u,v;
f=l=1;
q[1]=s;
dep[s]=1;
while (f<=l) {
u=q[f++];
for (i=bg[u];i;i=nt[i]) {
v=to[i];
if (dep[v] || !w[i]) continue;
q[++l]=v;
dep[v]=dep[u]+1;
}
}
}
int dfs(int x,int s) {
int i,u,tmp,res=0;
if (x==t) return s;
if (!s) return 0;
for (i=bg[x];i;i=nt[i]) {
u=to[i];
if (dep[u]==dep[x]+1) {
tmp=dfs(u,min(s,w[i]));
if (!tmp) continue;
res+=tmp;
s-=tmp;
w[i]-=tmp;
w[i^1]+=tmp;
if (!s) break;
}
}
return res;
}
int main() {
int i,j,x,y,z;
scanf("%d%d%d%d",&n,&m,&s,&t);
for (i=1;i<=m;i++) {
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);
insert(y,x,0);
}
while (1) {
memset(dep,0,sizeof(dep));
bfs();
if (!dep[t]) break;
ans+=dfs(s,inf);
}
printf("%d\n",ans);
return 0;
}
例題鏈接:
洛谷P3355 騎士共存問題 暫時我沒寫題解QAQ
附:當前弧優化
聽名字似乎是一個很高級的東西,但其實很簡單,就幾句話。。。
我們維護一個now數組,在每次dfs前把鏈式前向星的bg數組(或者是head)拷貝一份到now上,然後在dfs枚舉邊找下一個節點時循環枚舉邊的編號不再從bg[x]開始了,而從now[x]開始。而now[x]不斷地更新,即i循環到哪,now[x]都更新爲i。(具體實現還是看代碼吧,我的語言表達能力一向不強)
這樣子可以大大提升程序效率。
代碼:
#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;
int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans,now[maxn];
void insert(int x,int y,int z) {
nt[++e]=bg[x];
to[e]=y;
w[e]=z;
bg[x]=e;
}
void bfs() {
int i,f,l,u,v;
for (i=1;i<=n;i++) now[i]=bg[i];
f=l=1;
q[1]=s;
dep[s]=1;
while (f<=l) {
u=q[f++];
for (i=bg[u];i;i=nt[i]) {
v=to[i];
if (dep[v] || !w[i]) continue;
q[++l]=v;
dep[v]=dep[u]+1;
}
}
}
int dfs(int x,int s) {
int i,u,tmp,res=0;
if (x==t) return s;
if (!s) return 0;
for (i=now[x];i;i=nt[i]) {
u=to[i];
if (dep[u]==dep[x]+1) {
tmp=dfs(u,min(s,w[i]));
if (!tmp) continue;
res+=tmp;
s-=tmp;
w[i]-=tmp;
w[i^1]+=tmp;
if (!s) break;
}
now[x]=i;
}
return res;
}
int main() {
int i,j,x,y,z;
scanf("%d%d%d%d",&n,&m,&s,&t);
for (i=1;i<=m;i++) {
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);
insert(y,x,0);
}
while (1) {
memset(dep,0,sizeof(dep));
bfs();
if (!dep[t]) break;
ans+=dfs(s,inf);
}
printf("%d\n",ans);
return 0;
}
實測效率截圖O_O!:
優化之前:
之後:
至於這個優化的正確性,蒟蒻引用一句很6的話(畢竟我描述不出來)——
“對於一次BFS而言,它確定的層次圖中每條邊若已經被走完了,那麼它就不可能再帶來增廣,下一次就直接從這條最後沒走完的邊走就可以了。”