【網路流】Dinic演算法理解
EK演算法還是不夠優秀,我們還是要學習更加優秀的Dinic演算法才能解決更多要求較高的問題。
這裡確保大家都是懂得網路流的一些基本概念的,如果不懂的,這裡有一個連結,大家可以看一看。網路流詳解(顯然不是我寫的!)
他的EK演算法比我寫的好看的多,強勢給大家安利一波!!!
同時大家可以注意下這張圖,結合連結中的講解認真理解一下就可以懂得為什麼要反向連邊的原理!
就是給你一次反悔的機會,類似於我們搜尋時改變當前層的一個變數,後面又要把他改回來!!!
這裡轉載一下EK演算法和Dinic演算法的一些演算法大體實現思想
一般增廣路演算法(EdmondsKarp)
在一般的增廣路演算法中, 程式的實現過程與增廣路求最大流的過程基本一致. 即每一次更新都進行一次找增廣路然後更新路徑上的流量的過程。但是我們可以從上圖中發現一個問題, 就是每次找到的增廣路曲曲折折非常長, 此時我們往往走了冤枉路(即:明明我們可以從源點離匯點越走越進的,可是中間的幾條邊卻向離匯點遠的方向走了), 此時更新增廣路的複雜度就會增加。EK 演算法為了規避這個問題使用了 bfs 來尋找增廣路, 然後在尋找增廣路的時候總是向離匯點越來越近的方向去尋找下一個結點。
借用一下程式碼:
鄰接矩陣實現:
#include <queue> #include <cstdio> #include <cstring> #include <iostream> using namespace std; const int MAXN = 300; const int MAX_INT = ((1 << 31) - 1); int n; // 圖中點的數目 int pre[MAXN]; // 從 s - t 中的一個可行流中, 節點 i 的前序節點為 Pre[i]; bool vis[MAXN]; // 標記一個點是否被訪問過 int mp[MAXN][MAXN]; // 記錄圖資訊 bool bfs(int s, int t){ queue <int> que; memset(vis, 0, sizeof(vis)); memset(pre, -1, sizeof(pre)); pre[s] = s; vis[s] = true; que.push(s); while(!que.empty()){ int u = que.front(); que.pop(); for(int i = 1; i <= n; i++){ if(mp[u][i] && !vis[i]){ pre[i] = u; vis[i] = true; if(i == t) return true; que.push(i); } } } return false; } int EK(int s, int t){ int ans = 0; while(bfs(s, t)){ int mi = MAX_INT; for(int i = t; i != s; i = pre[i]){ mi = min(mi, mp[pre[i]][i]); } for(int i = t; i != s; i = pre[i]){ mp[pre[i]][i] -= mi; mp[i][pre[i]] += mi; } ans += mi; } return ans; }
鄰接表實現:
const int MAXN = 430; const int MAX_INT = (1 << 30); struct Edge{ int v, nxt, w; }; struct Node{ int v, id; }; int n, m, ecnt; bool vis[MAXN]; int head[MAXN]; Node pre[MAXN]; Edge edge[MAXN]; void init(){ ecnt = 0; memset(edge, 0, sizeof(edge)); memset(head, -1, sizeof(head)); } void addEdge(int u, int v, int w){ edge[ecnt].v = v; edge[ecnt].w = w; edge[ecnt].nxt = head[u]; head[u] = ecnt++; } bool bfs(int s, int t){ queue <int> que; memset(vis, 0, sizeof(vis)); memset(pre, -1, sizeof(pre)); pre[s].v = s; vis[s] = true; que.push(s); while(!que.empty()){ int u = que.front(); que.pop(); for(int i = head[u]; i + 1; i = edge[i].nxt){ int v = edge[i].v; if(!vis[v] && edge[i].w){ pre[v].v = u; pre[v].id = i; vis[v] = true; if(v == t) return true; que.push(v); } } } return false; } int EK(int s, int t){ int ans = 0; while(bfs(s, t)){ int mi = MAX_INT; for(int i = t; i != s; i = pre[i].v){ mi = min(mi, edge[pre[i].id].w); } for(int i = t; i != s; i = pre[i].v){ edge[pre[i].id].w -= mi; edge[pre[i].id ^ 1].w += mi; } ans += mi; } return ans; } // 加邊 addEdge(u, v, w); addEdge(v, u, 0); // 呼叫 int ans = EK(s, t);
演算法複雜度
每進行一次增廣需要的時間複雜度為 bfs 的複雜度 + 更新殘餘網路的複雜度, 大約為 O(m)(m為圖中的邊的數目), 需要進行多少次增廣呢, 假設每次增廣只增加1, 則需要增廣 nW 次(n為圖中頂點的數目, W為圖中邊上的最大容量), .
敲黑板
Dinic 演算法
演算法思想
DINIC 在找增廣路的時候也是找的最短增廣路, 與 EK 演算法不同的是 DINIC 演算法並不是每次 bfs 只找一個增廣路, 他會首先通過一次 bfs 為所有點新增一個標號, 構成一個層次圖, 然後在層次圖中尋找增廣路進行更新。
演算法流程
-
利用 BFS 對原來的圖進行分層,即對每個結點進行標號, 這個標號的含義是當前結點距離源點的最短距離(假設每條邊的距離都為1),注意:構建層次圖的時候所走的邊的殘餘流量必須大於0
-
用 DFS 尋找一條從源點到匯點的增廣路, 注意: 此處尋找增廣路的時候要按照層次圖的順序, 即如果將邊(u, v)納入這條增廣路的話必須滿足, 其中 為結點 的編號。找到一條路後要根據這條增廣路徑上的所有邊的殘餘流量的最小值更新所有邊的殘餘流量(即正向弧 - l, 反向弧 + l).
-
重複步驟 2, 當找不到一條增廣路的時候, 重複步驟 1, 重新建立層次圖, 直到從源點不能到達匯點為止。
其實實現還是非常簡單的,下面放一篇本人AC的程式碼,然後我再來講一講裡面要注意的一些細節。(注意上面是BFS找1條,只有1條!!!)
#include<bits/stdc++.h>
using namespace std;
const int M=100005;
const int inf=99999999;
struct sd
{
int to;
int cap;
};
sd edge[M*2];
vector <int> next[M];
int cnt;
int layer[M];
void addedge(const int &from,const int &to,const int &flow)
{
edge[cnt]=(sd){to,flow};
next[from].push_back(cnt++);
edge[cnt]=(sd){from,0};
next[to].push_back(cnt++);
}
bool BFS(int start,int end)
{
queue <int> q;
memset(layer,0,sizeof(layer));
q.push(start);
layer[start]=1;
int now,nextt,tar;
while(!q.empty())
{
now=q.front();
q.pop();
for(register int i=next[now].size()-1;i>=0;i--)
{
tar=next[now][i];
nextt=edge[tar].to;
if(!edge[tar].cap||layer[nextt])continue;
layer[nextt]=layer[now]+1;
q.push(nextt);
}
}
return layer[end];
}
int DFS(int now,int end,int value)
{
if(now==end || value == 0)return value;
int ret=0;
int nextt,tar,flow;
for(register int i=next[now].size()-1;i>=0;i--)
{
tar=next[now][i];
nextt=edge[tar].to ;
flow=edge[tar].cap ;
if(!flow||layer[now]!=layer[nextt]-1)continue;
int tmp=DFS(nextt,end,min(value-ret,flow));
if(!tmp)continue;
edge[tar].cap -=tmp;
edge[tar^1].cap +=tmp;
ret+=tmp;
if(tmp == value) return ret;
}
return ret;
}
int Dinic(int start,int end)
{
int ans=0;
while(BFS(start,end))
{
ans+=DFS(start,end,inf);
}
return ans;
}
int main()
{
int dot,line,st,en,a,c,b;
scanf("%d%d%d%d",&dot,&line,&st,&en);
for(register int i=1;i<=line;i++)
{
scanf("%d%d%d",&a,&b,&c);
addedge(a,b,c);
}
printf("%d",Dinic(st,en));
return 0;
}
注意:
-
我們最容易寫錯的東西其實就是dfs(),首先要注意就是每次dfs下去找最小的邊的時候,一定要保證最小邊不是0,如果是0,但是不判斷的話,他就會一直把一個沒有用的標記進行上傳,非常的浪費時間。
-
最後如果增廣失敗不要忘記返回0(return 0),否則程式會出現奇怪的錯誤。
-
記住用鏈式前向星的與圖論的鏈式前向星的一些不同的地方!還有反向弧減邊的技巧。
下面給出一道板題和AC程式碼:大家僅供參考:
題目連結:
P2740 [USACO4.2]草地排水Drainage Ditches
AC code:
#include<bits/stdc++.h>
using namespace std;
const int inf =0x7fffffff;
struct sd{
int v,next,to;
}edge[2005];
int n,m,cnt,head[2005],dep[2005],cur[2005],ans=0;
bool vis[2005];
void add(int a,int b,int c)
{
edge[cnt].next=head[a];
edge[cnt].to=b;
edge[cnt].v=c;
head[a]=cnt++;
}
int BFS()
{
queue<int> q;
memset(dep,0,sizeof(dep));
dep[1]=1; q.push(1);
while(!q.empty())
{
int now=q.front();q.pop();
for(int i=head[now];i+1;i=edge[i].next)
{
if(!dep[edge[i].to]&&edge[i].v)
{
dep[edge[i].to]=dep[now]+1;
q.push(edge[i].to);
}
}
}
return dep[n];
}
int dfs(int u,int w)
{
if(u==n) return w;
else
{
for(int &i=cur[u];i+1;i=edge[i].next)
{
if(edge[i].v&&dep[edge[i].to]==dep[u]+1)
{
int d=dfs(edge[i].to,min(w,edge[i].v));
if(d)
{
edge[i].v-=d;
edge[i^1].v+=d;
return d;
}
}
}
return 0;//?
}
}
int Dinic()
{
while(BFS())
{
for(int i=1;i<=n;++i) cur[i]=head[i];
while(int d=dfs(1,inf)) ans+=d;
}
return ans;
}
int main()
{
memset(head,-1,sizeof(head));
int a,b,c;
scanf("%d%d",&m,&n);
for(int i=1;i<=m;++i)
{
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,0);
}
printf("%d",Dinic());
return 0;
}