1. 程式人生 > >HDU -3549 (最大網路流——Edmonds_Karp演算法+Dinic演算法)

HDU -3549 (最大網路流——Edmonds_Karp演算法+Dinic演算法)

HDU -3549 (最大網路流)

Problem Description

Network flow is a well-known difficult problem for ACMers. Given a graph, your task is to find out the maximum flow for the weighted directed graph.

Input

The first line of input contains an integer T, denoting the number of test cases.
For each test case, the first line contains two integers N and M, denoting the number of vertexes and edges in the graph. (2 <= N <= 15, 0 <= M <= 1000)
Next M lines, each line contains three integers X, Y and C, there is an edge from X to Y and the capacity of it is C. (1 <= X, Y <= N, 1 <= C <= 1000)

Output

For each test cases, you should output the maximum flow from source 1 to sink N.

Sample Input

2
3 2
1 2 1
2 3 1
3 3
1 2 1
2 3 1
1 3 1

Sample Output

Case 1: 1
Case 2: 2

  • 題目大意:
    裸的最大網路流
    這是我寫的第一個網路流的問題。在這裡詳細的介紹一下
  • 最大網路流:
    首先是個有向圖 G=(V,E),每一個邊都有一個權值,我們把它記為c(u,v),是這個邊上的最大流量限制。我們形象的解釋一波: 在這裡插入圖片描述

    如圖:從A —— D 之間有多條管道連著,我們的目的是從A向D來輸送油(液體黃金吶)。每條邊上的權值代表管道”粗度“,比如 A-B 這條管道,最多能通過5個單位粗度的油。我們要求的是從A到D最多能輸送多少油(單位時間內),我們要把這個過程想象成動態的,就是說管道里一直流動著油。這就是我們最大網路流的定義了。【菜鳥的拙見,如有錯誤的地方,歡迎指正】。
  • 基礎知識儲備:
    因為解決這個問題,要用到一些術語,我們先來解釋一波術語。
    1.源點
    就是往外輸送油的點,也就是隻出不進的點(大公無私),就是上圖的A點
    2.匯點
    只進不出的點(最自私的點),就是上圖的D點。
    3.邊流量
    我們用f(u,v)表示邊流量。意思是當前u–v這條當前管道里的流動的油
    4.容量

    我們用c(u,v)表示容量,就是u–v這條管道的“粗度”,也就是我們圖裡的邊權值
    –因為已經解釋了幾個名詞了,我在這個地方插入一下網路流的性質
    a.f(u,v)<=c(u,v) 這個還用解釋嘛?就是流過管子的流量肯定得小於它的“粗度”啊。
    b.f(u,v)=-f(v,u) 我們把它類比速度來看,正向和反向大小相等,方向相反
    c.除了源點和匯點其他的點,流入的總值等於流出的總值。 這個也好理解,這些中間節點只是一些中轉點不會有流量的積攢。
    5.流量
    源點流出了多少單位的油(等價於匯點接收到了多少油,因為中間的結點是不會積攢油的,進多少,出多少,中間結點只是一箇中轉站)
    6.殘留網路
    假設我們已經確定了一個流量f.(首先我們要明確,一定會存在一個流量的,至少存在一個f=0的流量吧。。雖然這個流量沒有意義,但是趨勢存在)。然後我們把每條邊的c(u,v)都減去這個流量f 形成的圖我們就c稱為殘留網路。比如f=1時 上圖的殘留網路就是下圖
    在這裡插入圖片描述
    7.增廣路
    殘留子圖找一條能從源點到匯點的路徑,然後我們定義cf§為這條增廣路的流量值,cf§=min(增廣路的路徑上最小的邊),這個很好理解把,還是用運輸油的管道來打比方!在這裡插入圖片描述
    這就比如時運輸油的那一條路,他能運輸的量肯定是由最細的那個來決定呀。
    這裡有一個定理 f時最大流 就等價於 殘留網路中找不到 增廣路 這也是我們解決這個問題的一個核心。


BB了這麼多,還沒有說到點子上,到底怎麼解決這個問題呢?
我們來模擬一波,希望能讓你深入瞭解一下
借用一下大佬的圖(在文末會註明出處)
在這裡插入圖片描述
這是初始的狀態 ,剩餘就是這條邊還能經過多少。這個圖就是先假想出f=0,然後,我們開始找殘留網路裡的增廣路了。怎麼找呢?用bfs找呀。我們可以找這麼一條 A–B--C 最多能增廣 2 個單位。安排上,圖就變成這樣了
在這裡插入圖片描述
然後繼續找增廣路 A-B-D-C 然後能增廣1個單位,再安排上
在這裡插入圖片描述
z再找A-D-C 增廣 3 個單位,安排上
在這裡插入圖片描述
然後我們發現,這時候找不到增廣路了。那就結束了,說明這個最大流就是 2+1+3=6.本來可以愉快的結束這個問題了,但是我們發現一個問題,如果我們先找了A−B−D−C 這條增廣路的話在這裡插入圖片描述
然後我們再增廣 A-D-C 這時候只能增廣 1 了。
在這裡插入圖片描述
然後想再找增廣路,發現找不到了~找不到了!! 也就是說這樣的話我們的f=3+1=4 結果明顯不對。這就說明我們的演算法還有些瑕疵,畢竟在bfs找增廣路的時候沒q確定哪條最優。

所以這時候我們,引入一波反向弧的概念
就是每次你找到增廣路,就把這條路上所有的邊都加一條反向邊,權值是你增廣的值。至於我們為什麼這麼做呢,簡單的來說,我們就是給我們的程式提供一個反悔的機會,具體咋反悔呢,這樣說太抽象,沒法理解,我們還是就之前的例子繼續說。
如果剛開始選了A-B-D-C
增廣的值是3 然後我們把路徑上的都加一條反向邊 q權值為3.
在這裡插入圖片描述
然後我們再找 增廣路的時候就能找 A-D-B-C 增廣的值是2.因為 D-B之間加了一條邊
然後再增廣 A-D-C 增廣的值是1
(就不畫圖了 太麻煩了)
這時候結果還是 f=6 完美的解決了之前的問題。
接下來就是程式碼實現了

  • 程式碼實現細節——1.Edmonds_Karp:
    1.存圖我們用鄰接矩陣存mp[][]
    2.為了實現反向弧操作,我們為每一個結點都給一個pre[i],表示i的前一個是啥,pre陣列初始化為-1,然後我們找到增廣路後,往前回溯,加反向邊。當然還要把之前邊的權值更新一下 就是-flow
void update(int u,int flow)///找到增廣路之後將在增廣路上的邊-flow 反向+flow
{
	while(pre[u]!=-1)//回溯加反向邊
	{
		mp[pre[u]][u]-=flow;
		mp[u][pre[u]]+=flow;
		u=pre[u];
	}
}

3.我們怎樣找增廣路呢?
我們用BFS找增廣路
用個佇列q 來一層一層的裝經過的結點,然後如果能找到終點了就是找到增廣路了,如果找了一遍pre[t]=-1就是沒有找到與終點連線的邊,就自然沒找到增廣路啦。就return false

bool find_fath_bfs(int s,int t)//用BFS找增廣路的流量
{
	memset(vis,0,sizeof(vis));
	memset(pre,-1,sizeof(pre));
	vis[s]=1;
    minn=INF;
	queue<int>q;
	q.push(s);
	while(!q.empty())
	{
		int cur=q.front();
		q.pop();
		if(cur==t)//找到增廣路
			break;

		for(int i=1;i<=n;i++)
		{
			if(vis[i]==0&&mp[cur][i]!=0)
			{
				q.push(i);
				minn=min(minn,mp[cur][i]);

				pre[i]=cur;
				vis[i]=1;
			}
		}			
	}
	
	if(pre[t]==-1)//與匯點沒有相連的點??那還搞個毛
	{
		return false; 
	 } 
	return true;
}

4.接下來就是我們Edmonds_Karp演算法的核心部分了。我們不停的找增廣路,不停的更新我們的new_flow. 這個值就是我們找的增廣路增廣的值,然後加到我們的max_flow裡,一直找增廣路,找呀找呀找呀,一直到找不到增廣路為止,當然找到後還要回溯更新邊,並加上那個反向的邊。

int Edmonds_Karp(int s,int t)
{
	int new_flow=0;
	int max_flow=0;
	while(find_fath_bfs(s,t))
	{
		new_flow=minn;
		max_flow+=new_flow;
		update(t,new_flow);
	}
	return max_flow;
}

因為我太菜了…………………………
這個破東西我看了一晚上,那個進化版的最大流我還沒搞定,等搞定了再回來更新部落格。
注:我借鑑的大佬的部落格
https://blog.csdn.net/vonmax007/article/details/64921089#commentBox
https://blog.csdn.net/LiRewriter/article/details/78759337

  • AC程式碼:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=1e3+10;
int mp[maxn][maxn];///用來存圖
int vis[maxn];//標記陣列,在bfs的時候標記是否經過這個點
int pre[maxn];///用來存一個結點的前一個結點,為了後來方便回溯的
int n,m;
int minn=INF;
void update(int u,int flow)///找到增廣路之後將在增廣路上的邊-flow 反向+flow
{
	while(pre[u]!=-1)//回溯加反向邊
	{
		mp[pre[u]][u]-=flow;
		mp[u][pre[u]]+=flow;
		u=pre[u];
	}
}

bool find_fath_bfs(int s,int t)//用BFS找增廣路的流量
{
	memset(vis,0,sizeof(vis));
	memset(pre,-1,sizeof(pre));
	vis[s]=1;
    minn=INF;
	queue<int>q;
	q.push(s);
	while(!q.empty())
	{
		int cur=q.front();
		q.pop();
		if(cur==t)//找到增廣路
			break;

		for(int i=1;i<=n;i++)
		{
			if(vis[i]==0&&mp[cur][i]!=0)
			{
				q.push(i);
				minn=min(minn,mp[cur][i]);

				pre[i]=cur;
				vis[i]=1;
			}
		}			
	}
	
	if(pre[t]==-1)//與匯點沒有相連的點??那還搞個毛
	{
		return false; 
	 } 
	return true;
}

int Edmonds_Karp(int s,int t)
{
	int new_flow=0;
	int max_flow=0;
	while(find_fath_bfs(s,t))
	{
		new_flow=minn;
		max_flow+=new_flow;
		update(t,new_flow);
	}
	return max_flow;
}
int main()
{
	int T;
	cin>>T;
	int cas=1;
	while(T--)
	{
		cin>>n>>m;
		memset(mp,0,sizeof(mp));
		for(int i=1;i<=m;i++)
		{
			int a,b,w;
			cin>>a>>b>>w;
			mp[a][b]+=w;
		}
//		for(int i=1;i<=n;i++)
//		{
//			for(int j=1;j<=n;j++)
//				cout<<mp[i][j]<<" ";
//			cout<<endl;
//		}
		int ans=Edmonds_Karp(1,n);
		cout<<"Case "<<cas++<<": " <<ans<<endl;
	}
	return 0;
}

Edmonds_Karp演算法有個明顯得缺陷,請看這個圖
在這裡插入圖片描述
我們找一條增廣路,並加上反向邊
在這裡插入圖片描述
然後我們再繼續增廣 找到 S-U-V-T 並加上反向邊
V在這裡插入圖片描述
然後我們發現我們又能增廣 S-V-U-T ,然後又能增廣S-U-V-T
本來很簡單得兩條路徑解決的問題,我們在這裡需要多進行了好多次,浪費了大量的時間,所以說這個Edmonds_Karp演算法並不高效。
所以我們在Edmonds_Karpd的基礎上引入新的演算法—Dinic演算法.

  • Dinic演算法
    大致思路就是把所有的點按距離源點的遠近分層,然後找增廣路的時候每一步都找下一層的點。這樣就完美的避開了Edmonds_Karpd所造成的時間浪費問題。
    大佬部落格
    他介紹的Dinic演算法非常詳細也非常清晰。
    AC程式碼:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=1e4+10;
class Graph
{
private:
	int s,t;//源點 匯點
	int cnt;
	int head[maxn];
	int next[maxn];//連線到下一個點
	int V[maxn];///每一條邊指向的點
	int W[maxn];///每一條邊的殘量
	int depth[maxn];//分層的深度
public:
	int n;
	void init(int nn,int ss,int tt)
	{
		n=nn;
		s=ss;
		t=tt;
		cnt=0;
		memset(head,-1,sizeof(head));
		memset(next,-1,sizeof(next));
		return ;
	}
	void _Add(int u,int v,int w )
	{
		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)///表示當前的點,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)//前面這個判斷條件是確保找的是下一層,後面這個是判斷它殘留量大於零
			{
				int di=dfs(V[i],min(dist,W[i]));//往下dfs
				if(di>0)
				{
					W[i]-=di;
					W[i^1]+=di;//反向邊
					return di;
				}
			}
		}
		return 0;//找不到增廣路
	}
	bool bfs()//分層
	{
		queue<int>q;
		while(!q.empty())//清空佇列
			q.pop();
		memset(depth,0,sizeof(depth));
		depth[s]=1;
		q.push(s);
		while(q.size())
		{
			int u=q.front();
			q.pop();
			for(int i=head[u];i!=-1;i=next[i])
			{
				if(depth[V[i]]==0 &&W[i]>0)///這個點還沒分層且殘量大於0
				{
					depth[V[i]]=depth[u]+1;
					q.push(V[i]);
				}
			}
		}
		if(depth[t]==0)//匯點沒深度說明 沒有分層圖
			return false;
		return true;
	}
	int Dinic()
	{
		int ans=0;
		while(bfs())//能分層
		{
			while(int d=dfs(s,inf))//有增廣路
				ans+=d;
		}
		return ans;
	}

};
int main()
{
	int t;
	cin>>t;
	int n,m;
	Graph G;
	int cas=1;
	while(t--)
	{

		cin>>n>>m;
		G.init(n,1,n);
		while(m--)
		{
			int a,b,c;
			cin>>a>>b>>c;
			G.Add_Edge(a,b,c);
		}
		cout<<"Case "<<cas++<<": "<<G.Dinic()<<endl;
	}
	return 0;
}