淺談網路流(最大流,最小割,mcmf,最大匹配)
前言:
對於網路流的基礎知識,網上許多大佬解釋得很透徹了,我在這裡也不去挑戰大佬權威了!
這篇部落格記錄我一週學習網路流的學習筆記!以後還會逐漸完善!
一、最大流
最大流定理:
如果殘留網路上找不到增廣路徑,則當前流為最大 流;反之,如果當前流不為最大流,則一定有增廣路徑。
用最大流的增廣路經求二分圖匹配:
求二分圖匹配的過程就是求最大曾廣路的問題,而最大流定理就是將兩者之間聯絡起來,所以,二分圖最大匹配問題用最大流做也是一個好方法。所以,用網路流在求二分圖匹配時候,我們首先要做的就是求出最大流。
說到最大流,最大流求法呢也是有很多的!對比一下哈~
FF(Ford-Fulkerson)演算法 :
步驟:
(1)如果存在增廣路徑,就找出一條增廣路徑 DFS(EK演算法為BFS)
(2)然後沿該條增廣路徑進行更新流量 (增加流量)
While 有增廣路徑 :
do 更新該路徑的流量
時間複雜度:
O(cn^2),c(邊的容量和C,頂點數N)
程式碼:
缺點:如果運氣不好 這種圖會讓你的程式執行200次dfs 雖然實際上最少只要2次我們就能得到最大流
EK(Edmonds-Karp)演算法:
步驟:
(1)如果存在增廣路徑,就找出一條增廣路徑 BFS
(2)然後沿該條增廣路徑進行更新流量 (增加流量)
時間複雜度:
O(nm^2)(頂點數N,邊數M)
程式碼:
以hdu3549為例的ac程式碼
//O(m^2*n) #include<bits/stdc++.h> using namespace std; const int maxm=1e4+5; const int maxn=15+5; const int inf=0x7f7f7f7f; struct edge { int next,v,w; }edge[maxm]; int head[maxn]; int vis[maxn];//儲存是否被訪問 int pre[maxn];//記錄前驅 int last[maxn];//記錄前驅頂點和當前頂點的邊的編號 int cnt; void init() { cnt=0; memset(head,-1,sizeof(head)); } void add_edge(int u,int v,int w) { //正向邊 edge[cnt].v=v; edge[cnt].w=w; edge[cnt].next=head[u],head[u]=cnt++; //反向邊 edge[cnt].v=u,edge[cnt].w=0; edge[cnt].next=head[v],head[v]=cnt++; } bool bfs(int s,int e) { queue<int > q; memset(vis,0,sizeof(vis)); //memset(pre,-1,sizeof(pre)); vis[s]=1; q.push(s); while(!q.empty()) { int u=q.front(); q.pop(); for(int i=head[u];i!=-1;i=edge[i].next) { int v=edge[i].v,w=edge[i].w; if(!vis[v] && w) { last[v]=i; pre[v]=u; vis[v]=1; if(v==e) return true; q.push(v); } } } return false; } int EK(int s,int e) { int flow=0; while(bfs(s,e)) { int d=inf; /*for(int i=e;i!=s;i=pre[i]) cout<<i<<" "; for(int i=e;i!=s;i=pre[i]) cout<<last[i]<<" "; */ for(int i=e;i!=s;i=pre[i]) { d = min(d, edge[ last[i] ].w ); } for(int i=e;i!=s;i=pre[i]) { edge[ last[i] ].w-=d; edge[ last[i]^1 ].w+=d; } flow+=d; } return flow; } int main() { int n,m; int t,flag=1; cin>>t; while(t--) { cin>>n>>m; init(); for(int i=0;i<m;i++){ int u,v,w; cin>>u>>v>>w; add_edge(u,v,w); } printf("Case %d: %d\n",flag++,EK(1,n) ); } return 0; }
dinic演算法:
這個演算法是基於FF與EK的聯合提出來的演算法,同時用到BFS與DFS。
步驟:
(1):建造原網路G的一個分層網路L。
(2):用增廣路演算法計算L的最大流F,若在L中找不 到增廣路,演算法結束。
(3):根據F更新G中的流f,轉STEP1。
分層網路的構造演算法:
STEP1:標號源節點s,M[s]=0。
STEP2:呼叫廣度優先遍歷演算法,執行一步遍歷操作, 當前遍歷的弧e=v1v2,令r=G.u(e)-G.f(e)。
若r>0,則
(1) 若M[v2]還沒有遍歷,則M[v2]=M[v1]+1,且將 弧e加入到L中,容量L.u(e)=r。
(2) 若M[v2]已經遍歷且M[v2]=M[v1]+1,則將邊e 加入到L中,容量L.u(e)=r。
(3) 否則L.u(e)=0。 否則L.u(e)=0。 重複本步直至G遍歷完。其中的G.u(e)、G.f(e)、L.u(e) 分別表示圖G中弧e的容量上界和當前流量,圖L中弧e 的容量上界。
時間複雜度:O(mn^2)鄰接表表示圖,空間複雜度為O(n+m)。
又是hdu3549哈,以下是未優化以及優化過的dinic:
未優化程式碼:
//鏈式前向星dinic
//O(mn^2)
#include<bits/stdc++.h>
using namespace std;
const int maxm=1e4+5;
const int maxn=15+5;
const int inf=0x7f7f7f7f;
struct edge
{
int next;
int v,w;
}edge[maxm];
int m,n;
int cnt,head[maxn];
int dis[maxn];
void init()
{
cnt=0;
memset(head,-1,sizeof(head));
}
void add_edge(int u,int v,int w)
{
edge[cnt].v=v,edge[cnt].w=w;
edge[cnt].next=head[u],head[u]=cnt++;
edge[cnt].v=u,edge[cnt].w=0;
edge[cnt].next=head[v],head[v]=cnt++;
}
//分層
bool bfs()
{
memset(dis,-1,sizeof(dis));
queue<int> q;
dis[1]=0;
q.push(1);
while(!q.empty())
{
int u=q.front();
q.pop();
if(u==n)
return true;
for(int i=head[u];~i;i=edge[i].next)
{
int v=edge[i].v,w=edge[i].w;
if(dis[v]==-1 && w)
{
dis[v]=dis[u]+1;
q.push(v);
}
}
}
return false;
}
//求增廣路
int dfs(int s,int t,int flow)
{
if(s==t)
return flow;
int pre=0;
for(int i=head[s];~i;i=edge[i].next)
{
int v=edge[i].v,w=edge[i].w;
if(dis[s]+1==dis[v] && w>0)
{
int tmp=min(flow-pre,w);
int tf=dfs(v,t,tmp);
edge[i].w-=tf;
edge[i^1].w+=tf;
pre+=tf;
if(pre==flow)
return pre;
}
}
return pre;
}
int dinic()
{
int ret = 0;
while(bfs())
ret+=dfs(1,n,inf);
return ret;
}
int main()
{
int t;
cin>>t;
int flag=1;
while(t--)
{
cin>>n>>m;
init();
for(int i=0;i<m;i++){
int u,v,w;
cin>>u>>v>>w;
add_edge(u,v,w);
}
cout<<"Case "<<flag++<<": ";
cout<<dinic()<<endl;
}
return 0;
}
優化:
/*
HDU3549
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include <string.h>
using namespace std;
const int MAXN = 10000 + 5;
const int MAXM = 100000 + 5;
const int INF = 1e9;
int n,m;
int s,t;//源點 匯點
int maxflow;//答案
struct Edge {
int next;
int to,flow;
} l[MAXM << 1];
int head[MAXN],cnt = 1;
int deep[MAXN],cur[MAXN];//deep記錄bfs分層圖每個點到源點的距離
queue <int> q;
inline void add(int x,int y,int z) {
cnt++;
l[cnt].next = head[x];
l[cnt].to = y;
l[cnt].flow = z;
head[x] = cnt;
return;
}
int min(int x,int y) {
return x < y ? x : y;
}
int dfs(int now,int t,int lim) {//分別是當前點,匯點,當前邊上最小的流量
if(!lim || now == t) return lim;//終止條件
// cout<<"DEBUG: DFS HAS BEEN RUN!"<<endl;
int flow = 0;
int f;
for(int i = cur[now]; i; i = l[i].next) {//注意!當前弧優化
cur[now] = i;//記錄一下榨取到哪裡了,這裡也有人這麼寫,for(int &i = cur[now]; i; i = l[i].next),並且不要cur[now] = i;
if(deep[l[i].to] == deep[now] + 1 //誰叫你是分層圖
&& (f = dfs(l[i].to,t,min(lim,l[i].flow)))) {//如果還能找到增廣路
flow += f;
lim -= f;
l[i].flow -= f;
l[i ^ 1].flow += f;//記得處理反向邊
if(!lim) break;//沒有殘量就意味著不存在增廣路
}
}
return flow;
}
bool bfs(int s,int t) {
for(int i = 1; i <= n; i++) {
cur[i] = head[i];//拷貝一份head,畢竟我們還要用head
deep[i] = 0x7f7f7f7f;
}
while(!q.empty()) q.pop();//清空佇列 其實沒有必要了
deep[s] = 0;
q.push(s);
while(!q.empty()) {
int tmp = q.front();
q.pop();
for(int i = head[tmp]; i; i = l[i].next) {
if(deep[l[i].to] > INF && l[i].flow) {//有流量就增廣
//deep我賦的初值是0x7f7f7f7f 大於 INF = 1e9)
deep[l[i].to] = deep[tmp] + 1;
q.push(l[i].to);
}
}
}
if(deep[t] < INF) return true;
else return false;
}
void dinic(int s,int t) {
while(bfs(s,t)) {
maxflow += dfs(s,t,INF);
// cout<<"DEBUG: BFS HAS BEEN RUN!"<<endl;
}
}
void init()
{
cnt=1;
memset(head,0,sizeof(head));
maxflow=0;
}
int main() {
int t,flag=1;
cin>>t;
while(t--)
{
init();
cin>>n>>m;//點數邊數
int x,y,z;
for(int i = 1; i <= m; i++) {
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,0);
}
// cout<<"DEBUG: ADD FININSHED!"<<endl;
dinic(1,n);
printf("Case %d: %d\n",flag++,maxflow);
}
return 0;
}
話說這到底優化了什麼呢,我看到一篇部落格說的很好哈:
每次增廣一條路後可以看做“榨乾”了這條路,既然榨乾了就沒有再增廣的可能了。但如果每次都掃描這些“枯萎的”邊是很浪費時間的。那我們就記錄一下“榨取”到那條邊了,然後下一次直接從這條邊開始增廣,就可以節省大量的時間。這就是 當前弧優化 。
在DFS中用cur[x]表示當前應該從x的編號為cur[x]的邊開始訪問,也就是說從0到cur[x]-1的這些邊都不用再訪問了,相當於刪掉了(上面說的榨乾),達到了滿流。DFS(x,a)表示當前在x節點,有流量a,到終點t的最大流。當前弧優化在DFS裡的關鍵點在if(a==0) break;也就是說對於結點x,如果x連線的前面一些弧已經能把a這麼多的流量都送到終點,就不需要再去訪問後面的一些弧了,當前未滿的弧和後面未訪問的弧等到下次再訪問結點x的時候再去增廣。
以上就是計算最大流的一些簡單演算法,再回到最開始的那個問題,怎麼求最大匹配?
最大匹配:
匈牙利演算法和em演算法也是處理最大匹配的一個好演算法,非常好用,但是呢,網路流更全面,除了最大匹配,還可以用在其他許許多多方面,比較萬能吧!下面就最大流來求最大匹配:
超級原點與超級匯點:
想想就知道哈,二分圖就是兩個集合之間有聯絡,然而,就知道,他們有無數個源點與無數個匯點,所以呢,就要把它變換成只有一個源點與一個匯點的網路,這就需要超級原點與超級匯點!之後呢,就把每條邊的流量改成1就迎刃而解了。
求最大匹配步驟:
(1)增加一個源點s和一個匯點t;
(2)從s向集合X的每一個頂點引一條有向邊,從集合Y的每一個頂點向t引一條有向邊;
(3)將原圖的每條邊改為從集合X向集合Y的有向邊;
(4)置每條邊的容量為1;
程式碼以後附上吧。。。
MCMF(最小費用最大流):
顧名思義,就是求最大流花費的最小費用,對於費用(就是最短路啊)可以用SPFA來解(不能用dij,因為反向邊是負值),所以,就是在以上求最大流的基礎上加上SPFA及妥妥的!
程式碼:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=100010;
bool vis[maxn];
int n,m,s,t,x,y,z,f,dis[maxn],pre[maxn],last[maxn],flow[maxn],maxflow,mincost;
struct Edge{
int to,next,flow,dis;
}edge[maxn];
int head[maxn],num_edge;
queue <int> q;
void add_edge(int from,int to,int flow,int dis)
{
edge[++num_edge].next=head[from];
edge[num_edge].to=to;
edge[num_edge].flow=flow;
edge[num_edge].dis=dis;
head[from]=num_edge;
}
bool spfa(int s,int t)
{
memset(dis,0x7f,sizeof(dis));
memset(flow,0x7f,sizeof(flow));
memset(vis,0,sizeof(vis));
q.push(s); vis[s]=1; dis[s]=0; pre[t]=-1;
while (!q.empty())
{
int now=q.front();
q.pop();
vis[now]=0;
for (int i=head[now]; i!=-1; i=edge[i].next)
{
if (edge[i].flow>0 && dis[edge[i].to]>dis[now]+edge[i].dis)
{
dis[edge[i].to]=dis[now]+edge[i].dis;
pre[edge[i].to]=now;
last[edge[i].to]=i;
flow[edge[i].to]=min(flow[now],edge[i].flow);
if (!vis[edge[i].to])
{
vis[edge[i].to]=1;
q.push(edge[i].to);
}
}
}
}
return pre[t]!=-1;
}
void MCMF()
{
while (spfa(s,t))
{
int now=t;
maxflow+=flow[t];
mincost+=flow[t]*dis[t];
while (now!=s)
{
edge[last[now]].flow-=flow[t];
edge[last[now]^1].flow+=flow[t];
now=pre[now];
}
}
}
int main()
{
memset(head,-1,sizeof(head)); num_edge=-1;
scanf("%d%d%d%d",&n,&m,&s,&t);
for (int i=1; i<=m; i++)
{
scanf("%d%d%d%d",&x,&y,&z,&f);
add_edge(x,y,z,f); add_edge(y,x,0,-f);
}
MCMF();
printf("%d %d",maxflow,mincost);
return 0;
}
上下限有待更新。。。