最大流算法的選擇:Dinic還…

 這個是網易達人的博客上的一篇最大流算法的文章,看了覺得博主是個大牛,沒法收藏,就copy到我自己這裏了,肯定對我有用,希望對朋友們也有用

最大流是oi中經常用到的工具之一(尤其是近幾年),所以任何一個OIer必然都要背誦一個

代碼短、速度快、便於記憶的最大流代碼。
曾經某位神牛說”poj3469我試了所有最大流算法,只有dinic過了"
於是,我便毫不猶豫地選擇了dinic,不停地實踐,直至滾瓜爛熟地背誦下了全部的21行代碼。
int level[NMax];
int mkLevel(){
for (int i=(level[0]=0)+1;i<N;i++)level[i]=-1;
static int Q[NMax],bot;
Q[(bot=1)-1]=0;
for (int top=0;top<bot;top++){int x=Q[top];
for (edge *p=E[x];p;p=p->next)if (level[p->e]==-1 && p->f)
level[Q[bot++]=p->e]=level[x]+1;
}
return level[N-1]!=-1;
}
int extend(int a,int b){
int r=0,t;
if (a==N-1)return b;
for (edge *p=E[a];p && r<b;p=p->next)if (p->f && level[p->e]==level[a]+1){
t=p->f;if (t>b-r)t=b-r;t=extend(p->e,t);
r+=t;p->f-=t;OPT(p)->f+=t;
}
if (!r)level[a]=-1;
return r;
}
int Dinic(){int ret=0,t;
while (mkLevel())while ((t=extend(0,1000000000)))ret+=t;
return ret;
}

然而,情況有了變化。

先是在和其他省的選手交流的時候聽說了"SAP"這個最大流算法,並且聽說它的”常數比dinic好“
我當時將信將疑,穩妥起見,決定不改變自己使用dinic的習慣。
說些題外話。今天,我看《introduction to algorithms》終於看到了最大流那章,並且學習了
Relabel To Front這個算法。帶着好奇,我實現了Relabel To Front的代碼
(dinic、sap、RelabelToFront最大流代碼合集:http://cid-354ed8646264d3c4.office.live.com/self.aspx/.Public/MaximumFlow.cpp)
用poj3469測試,果不其然地tle了。
但是,看着RelabelToFront的代碼,我忽然覺得有點眼熟......

SAP?

是的,sap和RelabelToFront在很多細節上有神似的地方:
都有”距離標號“,都有”當前弧“,都是每次針對一條邊操作......
但他們還是本質不同的,一個是ford-fulkerson方法,一個是push-relabel方法。
在更大的好奇的驅使下,我打開了《算法藝術與信息學競賽》,正式學習了一下SAP。

其實sap非常簡單!
它的基礎思想還是增廣路,不過每次都選”最短“的一條增廣路(和dinic其實一樣...)
sap的優勢就是每次計算距離的時候不是像dinic那樣重新bfs計算,而是充分利用以前的距離標號的信息。
就像RelableToFront中一樣,sap的距離標號只是一個當前節點到匯的距離的下界,只有在無法根據當前標號增廣的時候
纔去更改它,使得我們能夠找到當前節點的“下家”
代碼:
int SAP(){
static int d[NMax],g[NMax+1],Q[NMax];
static edge *c[NMax],*pre[NMax];
int ret=0,x=0,bot;
for (int i=0;i<N;i++)c[i]=E[i],d[i]=g[i]=0;
pre[g[N]=0]=NULL;
Q[(bot=1)-1]=N-1;
for (int i=0;i<bot;i++)for (edge *p=E[Q[i]];p;p=p->next)
if (OPT(p)->f && p->e!=N-1 && d[p->e]==0)d[Q[bot++]=p->e]=d[Q[i]]+1;
for (int i=0;i<N;i++)g[d[i]]++;
while (d[0]<N){
while (c[x] && (!c[x]->f || d[c[x]->e]+1!=d[x]))c[x]=c[x]->next;
if (c[x]){
pre[c[x]->e]=OPT(c[x]);
x=c[x]->e;
if (x==N-1){
int t=~0u>>1;
for (edge *p=pre[N-1];p;p=pre[p->e])if (t>OPT(p)->f)t=OPT(p)->f;
for (edge *p=pre[N-1];p;p=pre[p->e])
p->f+=t,OPT(p)->f-=t;
ret+=t;
x=0;
}
}else{
int od=d[x];
g[d[x]]--;
d[x]=N;
for (edge *p=c[x]=E[x];p;p=p->next)if (p->f && d[x]>d[p->e]+1)d[x]=d[p->e]+1;
g[d[x]]++;
if (x)x=pre[x]->e;
if (!g[od])break;
}
}
return ret;
}
注意,SAP的實現有無數需要注意的細節,一旦一個細節沒有處理,都會導致“災難”性後果。
這些細節有:一定要bfs求初始標號、一定要保存當前弧、一定要在標號出現“斷層”的時候結束、小心地計算當前節點的位置......
但是,戰戰兢兢地實現代碼是值得的!
在poj3469上,
dinic : 3698ms
sap: 2402ms
整整快了一半!
還有一個更迷人的特性:sap的代碼是非遞歸的!
想象這樣一個圖:由20000個節點組成的從源到匯的鏈。
dinic會棧溢出,普通的初始標號都是0的sap會tle,只有我上面那個用bfs計算初始標號的代碼可以輕鬆+愉快的秒殺。

現在,終於切入正題了!
在生產生活(包括OI)中,到底應該使用dinic 還是sap算法呢?

先讓我們列一下兩個算法各自的優勢
1.sap比dinic快  這是經過了很多人(包括我)檢驗的,甚至快的可以不是一星半點兒。
2.sap的非遞歸實現比dinic好寫(我上面的代碼就是非遞歸的)
3.該dinic了。dinic的代碼長度短(字節數比sap少1/4)
4.dinic的代碼行數比sap少(大約少1/2)
爲什麼要分別計算代碼字節數和行數呢?
這主要是因爲我自己的編程習慣:
儘量把目的一致、結構相同的代碼擠在一行。
而代碼行數少就等價於:需要背誦的內容少、代碼的複雜程度低、調試難度小。
而代碼字節少就等價於:需要敲得鍵盤次數少、代碼實現時間短。
別不信,再看一眼dinic和sap的代碼,你肯定願意抄寫dinic並調試。
這也就讓人產生了糾結。爲了常數,我們可能要用更多的時間去寫代碼,並且sap的代碼細節衆多,
有一堆繁瑣的小句子(不像dinic的全部語句就是4個for2個while3個if),調試起來很有風險。

在dinic和sap之間的抉擇彷彿就變成了在heapsort和quicksort之間的抉擇。
1.良好實現的heapsort在最壞情況下速度比quicksort快的是有本質區別的(隨機化?不怕別人challenge 你的代碼嗎?)
2.heapsort本身就是非遞歸的
3.quicksort代碼量小
4.quicksort代碼行數少
結果呢?大家最後都達成了共識:
1.在寫一些無關緊要的小題或數據很小時,使用單純版的quicksort
2.在寫一些oj上數據很大的題時,使用加了各種抗噁心(隨機化)的quicksort
3.在寫一些關乎生死存亡、重大利益的代碼時,使用stl中集各種排序之大成於一身的sort函數
4.在寫一些關乎生死存亡、重大利益的代碼,又禁了stl的時候,用heapsort

於是,我做出了以下選擇:
1.同時掌握dinic和sap的代碼
2.在小數據或無關痛癢的題目上,使用dinic
3.在noi及以上的正式比賽中,使用sap
4.爲了能在關鍵時刻寫出正確的sap,平常也要“勤練兵”,有事沒事也寫一寫。

這個問題似乎也就被解決了,不過是用了一種痛苦的方法:同時掌握兩種算法。
其實同時掌握兩種算法是有風險的,不過,比起將來可能帶來的收益,是非常值得的。

每個人都可以有不同的風格,以上觀點僅供參考。
發佈了45 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章