總評一句:Dinic算法的基本思想比較好理解,就是它的當前弧優化的思想,網上的資料也不多,所以對於當前弧的優化,我還是費了很大的功夫的,現在也一知半解,索性就寫一篇博客,來發現自己哪里的算法思想還沒理解透徹,並解決他
https://www.cnblogs.com/SYCstudio/p/7260613.html
這篇博客對於Dinic的算法思想介紹的非常詳細,一些專有名詞什么的也是很專業
網絡流:在一個有向圖上選擇一個源點,一個匯點,每一條邊上都有一個流量上限(以下稱為容量),即經過這條邊的流量不能超過這個上界,同時,除源點和匯點外,所有點的入流和出流都相等,而源點只有流出的流,匯點只有匯入的流。這樣的圖叫做網絡流。
就相當於生活中的排水或管道運輸,我一次性運輸的絕對不能超過任何一節管道的限流大小,所以一次性的,所有店的入流和出流都相等
我們定義:
源點:只有流出去的點
匯點:只有流進來的點
流量:一條邊上流過的流量
容量:一條邊上可供流過的最大流量
殘量:一條邊上的容量-流量
殘量也就是一個邊上再不超過限制的情況下還能流的大小,生活中也是如此,1 - 2邊限制為5,2 - 3邊限制為4管道相連的方向我可以從1 - 2 - 3 流量為 4,我還可以1 - 2 - 4 流量為1,所以這個並不是一個邊只能用一次。每次用完我們可能會記錄參量,並進行其他的相關操作
最大流的求解:
網絡流的所有算法都是基於一種增廣路的思想,下面首先簡要的說一下增廣路思想,其基本步驟如下:
1.找到一條從源點到匯點的路徑,使得路徑上任意一條邊的殘量>0(注意是小於而不是小於等於,這意味着這條邊還可以分配流量),這條路徑便稱為增廣路 2.找到這條路徑上最小的F[u][v](我們設F[u][v]表示u->v這條邊上的殘量即剩余流量),下面記為flow 3.將這條路徑上的每一條有向邊u->v的殘量減去flow,同時對於起反向邊v->u的殘量加上flow(為什么呢?我們下面再講) 4.重復上述過程,直到找不出增廣路,此時我們就找到了最大流
這個算法是基於增廣路定理(Augmenting Path Theorem): 網絡達到最大流當且僅當殘留網絡中沒有增廣路(由於筆者知識水平不高,暫且不會證明)
舉個例子:
為什么要連反向邊
我們知道,當我們在尋找增廣路的時候,在前面找出的不一定是最優解,如果我們在減去殘量網絡中正向邊的同時將相對應的反向邊加上對應的值,我們就相當於可以反悔從這條邊流過。 比如說我們現在選擇從u流向v一些流量,但是我們后面發現,如果有另外的流量從p流向v,而原來u流過來的流量可以從u->q流走,這樣就可以增加總流量,其效果就相當於p->v->u->q,用圖表示就是:
圖中的藍色邊就是我們首次增廣時選擇的流量方案,而實際上如果是橘色邊的話情況會更優,那么我們可以在v->u之間連一條邊容量為u->v減去的容量,那我們在增廣p->v->u->q的時候就相當於走了v->u這條"邊",而u->v的流量就與v->u的流量相抵消,就成了中間那幅圖的樣子了。 如果是v->u時的流量不能完全抵消u->v的,那就說明u還可以流一部分流量到v,再從v流出,這樣也是允許的。
一個小技巧
雖然說我們已經想明白了為什么要加反向邊,但反向邊如何具體實現呢?筆者在學習網絡流的時候在這里困擾了好久,現在簡要的總結在這里。 首先講一下鄰接矩陣的做法,對於G[u][v],如果我們要對其反向邊進行處理,直接修改G[v][u]即可。 但有時會出現u->v和v->u同時本來就有邊的情況,一種方法是加入一個新點p,使u->v,而v->u變成v->p,p->u。 另一種方法就是使用鄰接表,我們把邊從0開始編號,每加入一條原圖中的邊u->v時,加入邊v->u流量設為0,那么這時對於編號為i的邊u->v,我們就可以知道i^1就是其反向邊v->u。
朴素算法的低效之處
雖然說我們已經知道了增廣路的實現,但是單純地這樣選擇可能會陷入不好的境地,比如說這個經典的例子:
我們一眼可以看出最大流是999(s->v->t)+999(s->u->t),但如果程序采取了不恰當的增廣策略:s->v->u->t
我們發現中間會加一條u->v的邊
而下一次增廣時:
若選擇了s->u->v->t
然后就變成
這是個非常低效的過程,並且當圖中的999變成更大的數時,這個劣勢還會更加明顯。 怎么辦呢? 這時我們引入Dinic算法
最大流的求法:就是求1 - n的最大總流量(給你已知地圖)
Dinic算法是比較高效的解決算法
為了解決我們上面遇到的低效方法,Dinic算法引入了一個叫做分層圖的概念。具體就是對於每一個點,我們根據從源點開始的bfs序列,為每一個點分配一個深度,然后我們進行若干遍dfs尋找增廣路,每一次由u推出v必須保證v的深度必須是u的深度+1。下面給出代碼
一些變量的定義
以下給出的代碼,是按我的方式寫出並解釋的,大家如果想先看看大佬的,那就click here!
#include <iostream> #include <string.h> #include <queue> #include <cstdio> #define inf 0x3f3f3f3f using namespace std; const int maxn = 220; const int maxm = 4e4 + 4e3; int m,n; struct node { int pre; int to,cost; }edge[maxm]; int id[maxn],cnt; int flor[maxn]; void init() { memset(id,-1,sizeof(id)); cnt = 0; } int cur[maxn]; void add(int a,int b,int x) { edge[cnt].to = b; edge[cnt].cost = x; edge[cnt].pre = id[a]; id[a] = cnt++; swap(a,b); edge[cnt].to = b; edge[cnt].cost = 0; edge[cnt].pre = id[a]; id[a] = cnt++; }
這一部分代碼比較基礎,也沒什么算法在里面,第一個就是鏈式前向星存儲邊的關系,flor是為了Dinic算法存儲結點的層號,cur數組時當前弧的優化,這個我們稍后再談談,add時加邊炒作,用到了一個反向邊的思想,為什么要有反向邊上面大佬說的也比較清楚了,我前面ek算法的學習記錄也有我自己的理解在里面~~在這就不提了。還要注意的一點就是這樣的加邊方法,對於正向邊edge[i]那么edge[i^1]就是反向邊的數據,很是方便,后面會用
在這我直接來想一想帶有當前弧優化的Dinic算法
int Dinic(int s,int t) { int ret = 0; while(bfs(s,t)) { for(int i = 1;i <= n;i++) { cur[i] = id[i]; } ret += dfs(s,t,inf); } return ret; }
大體架構是這樣的,先利用bfs為當前圖層分層,分完層之后,是當前弧優化的初始化操作,我看見過很多版本,沒有一一深究,先理解學習的這一個你可以看到,id數組是鏈式前向星的每一個點邊數據的鏈尾點,很明顯,當前弧的優化肯能要對我存儲的邊來做做手腳了
然后我對當前分好層的 圖進行dfs尋找可行路徑,並返回最大流量
先來看看bfs分層,這個還比較中規中矩
int bfs(int s,int t) { queue<int>q; while(q.size())q.pop(); memset(flor,0,sizeof(flor)); flor[s] = 1; q.push(s); while(q.size()) { int now = q.front();q.pop(); if(now == t)return 1; for(int i = id[now];~i;i = edge[i].pre) { int to = edge[i].to; if(flor[to] == 0 && edge[i].cost > 0) { flor[to] = flor[now] + 1; q.push(to); //找到一條邊就返回 if(to == t)return 1; } } } return flor[t] != 0; // return 0; }
繼續bfs尋找的條件是沒有被拜訪過(flor數組還起到了vis數組的作用,並且該店還有流量,還能走),到了就返回1沒到其實返回0就行了
我在1532的樣例中過了,我也沒覺得有啥問題,我一開始初始化肯定是flor[t] = 0,我只有訪問過t才能對flor【t】進行賦值,一旦賦值不久代表to == t 肯定就返回1了,沒毛病~~
接下來就是重頭戲dfs,本來dfs就是遞歸,再加上當前弧優化也在這,一團漿糊扔過來我也就只剩漿糊了,但是,但是,我也得弄啊~~
int dfs(int s,int t,int value) { int ret = value,a; if(s == t || value == 0)return value; //當前弧優化 for(int &i = cur[s];~i;i = edge[i].pre)//每次dfs的時候記錄遍歷優化到那條邊了,如果再次遍歷這個點的時候就可以直接取拜訪那條邊 { int to = edge[i].to; if(flor[to] == flor[s] + 1 && (a = dfs(to,t,min(ret,edge[i].cost)))) { edge[i].cost -= a; edge[i^1].cost += a; ret -= a; if(!ret)break; } } if(ret == value)flor[s] = 0; return value - ret; /*for(int i = id[s];~i;i = edge[i].pre) { int to = edge[i].to; int cost = edge[i].cost; if(flor[s] == flor[to] - 1 && cost > 0 && (int w = dfs(to,t,min(value - ret,cost))))//不加上括號不能識別,會報錯 { edge[i].cost -= w; edge[i^1].cost += w; ret += w; // return ret; if(ret == value)break; } } if(!ret) d[s] = -1; return ret; return 0;*/ }
注釋得是本來沒有當前弧優化得代碼
s是起點,t是終點,value是當前得流量
ret存儲value副本,a用來接收遞歸返回的值,遞歸結束得條件:到達了終點,或者當前得流量已經為0就直接返回就好——就是1.路通了到了2.路不通沒路了,在這兩個情況下返回值
接下來我們用到了當前弧得優化,引用cur數組,作用是i變化,cur也變化,就相當與,這個點變關系每一條邊在這次所有的dfs中只會出現一次,
當你判斷to那個點可行得化,那就dfs遞歸求區to 到 終點t得value,這樣我們會一直遞歸到t,返回了value,表示這一條通路上的最大流量,由a接着,並回溯對a那條邊進行正反邊得優化,正向邊減限制,反向邊增加限制,ret為什么是-a呢
先來看看ret表示得是由起始點s 到當前點to得value,w是加上了(連上了,dfs連上得)終點后的value,有一點1他倆相等,這很好我們就可以直接返回了,最后就是直接返回value去進行后續得邊得優化
2.他倆不相等呢(ret > a),就不會退出,會根據to得邊的記錄繼續dfs,這樣應該就不會dfs到終點了,但是為什么還要繼續dfs就是為了當前弧度得優化,它的記錄會讓我dfs不會再走一遍,掃完之后還得出來不是,出來后ret表示得就是兩個value得差值,就是相當於把w帶了出來value - ret = a
if(ret == value)flor[s] = 0;這一個是干什么得呢:也是一個優化,你得看看ret何時 == value
兩種可能把;1.到了終點t但是最大流量是0,那么我這個中間點s是不是就廢了,已經斷流了啊,標記以下,下次dfs不再管它
2.沒到終點,走了這么久都沒到終點,這個中間的s是不是也廢了,下次我也不再走它~~
其實這里還是比較好懂得,對於我,我覺得int &i = cur[s];~i;i = edge[i].pre這個是最難懂的了,想了很久,在這里和大家交流一下,這里得目的都很明確,加上這一條就是表示這一條邊我只來一次,可是我就提出反例了
生活中也是如此,1 - 2邊限制為5,2 - 3邊限制為4管道相連的方向我可以從1 - 2 - 3 流量為 4,我還可以1 - 2 - 4 流量為1,所以這個並不是一個邊只能用一次
我上面剛剛說過一個邊可能用多次,正好不久和這個沖突了嗎,我就想,是不是回溯得時候他做了手腳了,唉,還真有手腳,就是ret,他執行完ret - a得操作后,表示得是什么:(我有點激動,我好像寫着寫着明白了,我要把我腦子里的圖畫出來)
ret表示得是從s到to得最大流量,a呢是目前這個路程得最大流量(ret >= a)注意:T不一定非得是終點,這也就保證了,每一步得回溯使得結果無誤
ret - a就是s 到 to點所有點都至少剩余得流量,這就回溯回來了,我們就可以用這個ret繼續尋找to得另外得邊,看看還有沒有別的可行得路
又是兩種情況:1.另一條邊可行,ret被優化到了0,我們久break,返回值,沒什么毛病
2.沒有可行邊,當前弧繼續優化並進行回溯,往回走
我又有了一個問題,就是如果可行了,返回了ret是零,奧,那么對於我前面所有得邊得正向邊至少都會-value,因為我是一步一步對邊進行回溯更新得,這里得操作都市對當前邊得優化,對於后續邊,如果限制流量滿了,那么我就返回value讓他取優化,而不必去管他是流了幾次才滿得,沒有滿呢,我就會先去嘗試還能不能流向別處,如果不行我會先記錄該點位費點,然后返回他倆之間得差值,因為我盡力了,他還是沒能流滿,如果能流向別處,好像又是一波循環了,到此我覺得我算是明白這個算法了,我感覺可以自己想着寫一下了
————————————————————————————————————————————————
加油!Butterflier