Dinic算法(研究總結,網絡流)
網絡流是信息學競賽中的常見類型,筆者剛學習了最大流Dinic算法,簡單記錄一下
網絡流基本概念
什么是網絡流
在一個有向圖上選擇一個源點,一個匯點,每一條邊上都有一個流量上限(以下稱為容量),即經過這條邊的流量不能超過這個上界,同時,除源點和匯點外,所有點的入流和出流都相等,而源點只有流出的流,匯點只有匯入的流。這樣的圖叫做網絡流。
所謂網絡或容量網絡指的是一個連通的賦權有向圖 D= (V、E、C) , 其中V 是該圖的頂點集,E是有向邊(即弧)集,C是弧上的容量。此外頂點集中包括一個起點和一個終點。網絡上的流就是由起點流向終點的可行流,這是定義在網絡上的非負函數,它一方面受到容量的限制,另一方面除去起點和終點以外,在所有中途點要求保持流入量和流出量是平衡的。(引自百度百科)
定義
我們定義:
源點:只有流出去的點
匯點:只有流進來的點
流量:一條邊上流過的流量
容量:一條邊上可供流過的最大流量
殘量:一條邊上的容量-流量
幾個基本性質
基本性質一:
對於任何一條流,總有流量<=容量
這是很顯然的
基本性質二
對於任何一個不是源點或匯點的點u,總有$$\sum_{p\in E}k[p][u]==\sum_{q\in E}k[u][q] \text{(其中k[i][j]表示i到j的流量)}$$
這個也很顯然,即一個點(除源點和匯點)的入流和出流相等
基本性質三
對於任何一條有向邊(u,v),總有$$k[u][v]==-k[v][u]$$
這個看起來並不是很好理解,它的意思就是一條邊的反邊上的流是這條邊的流的相反數,可以這么想,就是如果有k[u][v]的流從u流向v,也就相當於有-k[v][u]的流從v流向u。這條性質非常重要。
網絡流最大流
網絡流的最大流算法就是指的一個流量的方案使得網絡中流量最大。
網絡流最大流的求解
網絡流的所有算法都是基於一種增廣路的思想,下面首先簡要的說一下增廣路思想,其基本步驟如下:
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算法
Dinic算法
為了解決我們上面遇到的低效方法,Dinic算法引入了一個叫做分層圖的概念。具體就是對於每一個點,我們根據從源點開始的bfs序列,為每一個點分配一個深度,然后我們進行若干遍dfs尋找增廣路,每一次由u推出v必須保證v的深度必須是u的深度+1。下面給出代碼
一些變量的定義
int s,t;//源點和匯點
int cnt;//邊的數量,從0開始編號。
int Head[maxN];//每一個點最后一條邊的編號
int Next[maxM];//指向對應點的前一條邊
int V[maxM];//每一條邊指向的點
int W[maxM];//每一條邊的殘量
int Depth[maxN];//分層圖中標記深度
Dinic主過程:
int Dinic()
{
int Ans=0;//記錄最大流量
while (bfs())
{
while (int d=dfs(s,inf))
Ans+=d;
}
return Ans;
}
bfs分層圖過程
bool bfs()
{
queue<int> Q;//定義一個bfs尋找分層圖時的隊列
while (!Q.empty())
Q.pop();
memset(Depth,0,sizeof(Depth));
Depth[s]=1;//源點深度為1
Q.push(s);
do
{
int u=Q.front();
Q.pop();
for (int i=Head[u];i!=-1;i=Next[i])
if ((W[i]>0)&&(Depth[V[i]]==0))//若該殘量不為0,且V[i]還未分配深度,則給其分配深度並放入隊列
{
Depth[V[i]]=Depth[u]+1;
Q.push(V[i]);
}
}
while (!Q.empty());
if (Depth[t]==0)//當匯點的深度不存在時,說明不存在分層圖,同時也說明不存在增廣路
return 0;
return 1;
}
dfs尋找增廣路過程
int dfs(int u,int dist)//u是當前節點,dist是當前流量
{
if (u==t)//當已經到達匯點,直接返回
return dist;
for (int i=Head[u];i!=-1;i=Next[i])
{
if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))//注意這里要滿足分層圖和殘量不為0兩個條件
{
int di=dfs(V[i],min(dist,W[i]));//向下增廣
if (di>0)//若增廣成功
{
W[i]-=di;//正向邊減
W[i^1]+=di;反向邊加
return di;//向上傳遞
}
}
}
return 0;//否則說明沒有增廣路,返回0
}
把上面的內容都封裝到類中:
class Graph
{
private:
int s,t;
int cnt;
int Head[maxN];
int Next[maxM];
int V[maxM];
int W[maxM];
int Depth[maxN];
public:
int n;
void init(int nn,int ss,int tt)//初始化
{
n=nn;
s=ss;
t=tt;
cnt=-1;
memset(Head,-1,sizeof(Head));
memset(Next,-1,sizeof(Next));
return;
}
void _Add(int u,int v,int w)
{
cnt++;
Next[cnt]=Head[u];
V[cnt]=v;
W[cnt]=w;
Head[u]=cnt;
}
void Add_Edge(int u,int v,int w)//加邊,同時加正向和反向的
{
_Add(u,v,w);
_Add(v,u,0);
}
int dfs(int u,int dist)
{
//cout<<"Dfs:"<<u<<' '<<dist<<endl;
if (u==t)
return dist;
for (int i=Head[u];i!=-1;i=Next[i])
{
if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))
{
int di=dfs(V[i],min(dist,W[i]));
if (di>0)
{
W[i]-=di;
W[i^1]+=di;
return di;
}
}
}
return 0;
}
int bfs()
{
//cout<<"Bfs.begin:"<<endl;
queue<int> Q;
while (!Q.empty())
Q.pop();
memset(Depth,0,sizeof(Depth));
Depth[s]=1;
Q.push(s);
do
{
int u=Q.front();
//cout<<u<<endl;
Q.pop();
for (int i=Head[u];i!=-1;i=Next[i])
{
if ((W[i]>0)&&(Depth[V[i]]==0))
{
Depth[V[i]]=Depth[u]+1;
Q.push(V[i]);
}
}
}
while (!Q.empty());
//cout<<"Bfs.end"<<endl;
if (Depth[t]>0)
return 1;
return 0;
}
int Dinic()
{
int Ans=0;
while (bfs())
{
while (int d=dfs(s,inf))
Ans+=d;
}
return Ans;
}
};
Dinic算法的優化
Dinic算法還有優化,這個優化被稱為當前弧優化,即每一次dfs增廣時不從第一條邊開始,而是用一個數組cur記錄點u之前循環到了哪一條邊,以此來加速
總代碼如下,修改的地方已在代碼中標出:
class Graph
{
private:
int cnt;
int Head[maxN];
int Next[maxM];
int W[maxM];
int V[maxM];
int Depth[maxN];
int cur[maxN];//cur就是記錄當前點u循環到了哪一條邊
public:
int s,t;
void init()
{
cnt=-1;
memset(Head,-1,sizeof(Head));
memset(Next,-1,sizeof(Next));
}
void _Add(int u,int v,int w)
{
cnt++;
Next[cnt]=Head[u];
Head[u]=cnt;
V[cnt]=v;
W[cnt]=w;
}
void Add_Edge(int u,int v,int w)
{
_Add(u,v,w);
_Add(v,u,0);
}
int dfs(int u,int flow)
{
if (u==t)
return flow;
for (int& i=cur[u];i!=-1;i=Next[i])//注意這里的&符號,這樣i增加的同時也能改變cur[u]的值,達到記錄當前弧的目的
{
if ((Depth[V[i]]==Depth[u]+1)&&(W[i]!=0))
{
int di=dfs(V[i],min(flow,W[i]));
if (di>0)
{
W[i]-=di;
W[i^1]+=di;
return di;
}
}
}
return 0;
}
int bfs()
{
queue<int> Q;
while (!Q.empty())
Q.pop();
memset(Depth,0,sizeof(Depth));
Depth[s]=1;
Q.push(s);
do
{
int u=Q.front();
Q.pop();
for (int i=Head[u];i!=-1;i=Next[i])
if ((Depth[V[i]]==0)&&(W[i]>0))
{
Depth[V[i]]=Depth[u]+1;
Q.push(V[i]);
}
}
while (!Q.empty());
if (Depth[t]>0)
return 1;
return 0;
}
int Dinic()
{
int Ans=0;
while (bfs())
{
for (int i=1;i<=n;i++)//每一次建立完分層圖后都要把cur置為每一個點的第一條邊 感謝@青衫白敘指出這里之前的一個疏漏
cur[i]=Head[i];
while (int d=dfs(s,inf))
{
Ans+=d;
}
}
return Ans;
}
};