圖論-網絡流③-最大流②

圖論-網絡流③-最大流②

上一篇:圖論-網絡流②-最大流①

下一篇:圖論-網絡流④-最大流解題①

參考文獻:

  • https://www.cnblogs.com/DuskOB/p/11216861.html
  • https://blog.csdn.net/yjr3426619/article/details/82808303
  • https://blog.csdn.net/lym940928/article/details/90209172
  • https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%B5%81/2987528?fr=aladdin
  • https://www.cnblogs.com/pk28/p/8039645.html
  • https://blog.csdn.net/disgustinglemon/article/details/51296636

大綱

  • 什麼是網絡流
  • 最大流(最小割)
  • DinicDinic (常用)
  • EKEK Start\color{#33cc00}\texttt{Start}
  • SapSap
  • FordFulkersonFord-Fulkerson(不講)
  • HLPPHLPP (快) End\color{red}\texttt{End}
  • 最大流解題
  • 費用流
  • EKEK 費用流
  • DinicDinic 費用流
  • zkwzkw 費用流
  • 費用流解題

  • 有上下界的網絡流

  • 無源匯上下界可行流
  • 有源匯上下界可行流
  • 有源匯上下界最大流
  • 有源匯上下界最小流
  • 最大權閉合子圖
  • 有上下界的網絡流解題

上一篇中講了最大流定義、最小割定理以及DinicDinic算法,這篇中會講剩下三種最大流算法:EKEKSAPSAPHLPPHLPP

EK

EKEK 的全稱叫 EdmondsKarpEdmonds-Karp。是一個與 DinicDinic 相比代碼較短,跑得較的算法。

EKEK 就是簡單地暴力搜索整個網絡流圖。在每次搜索增廣路的時候,都採取 BfsBfs 的策略,將所有的從源點到匯點的路徑都找出來,那麼如果有增廣路,就一定可以將它找出來。因此採用 BfsBfs 策略首先是正確的,代碼:

#include<bits/stdc++.h>
using namespace std;
const int N=210;
const int inf=0x3f3f3f3f;
int n,m,s,t;
int fw[N][N],pre[N]; //殘留網絡,初始化爲原圖
bool vis[N];
queue<int> q;
bool bfs(int s,int t){//尋找一條從s到t的增廣路,若找到返回true
	memset(pre,0,sizeof pre);
	memset(vis,0,sizeof vis);
	while(q.size()) q.pop();
	q.push(s),pre[s]=s,vis[s]=1;
	while(q.size()){
		int x=q.front();q.pop();
		for(int i=1;i<=n;i++)
			if(fw[x][i]>0&&!vis[i]){
				pre[i]=x,vis[i]=1;
				if(i==t) return 1;
				q.push(i);
			}
	}
	return 0;
}
int EdmondsKarp(int s,int t){
	int flow=0,f;
	while(bfs(s,t)){
		f=inf;
		for(int i=t;i!=s;i=pre[i])
			f=min(f,fw[pre[i]][i]);
		for(int i=t;i!=s;i=pre[i])
			fw[pre[i]][i]-=f,fw[i][pre[i]]+=f;
		flow+=f;
   }
   return flow;
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1,x,y,f;i<=m;i++){
		scanf("%d%d%d",&x,&y,&f);
		fw[x][y]+=f;
	}
	printf("%d\n",EdmondsKarp(s,t));
	return 0;
}

EKEK 比較 DinicDinic 已經不抽象很多了,但爲了方便理解,舉以下圖例:
在這裏插入圖片描述
可以看出EKEK算法有很多多餘的增廣與遍歷。《算法導論》中證明了在每次 BfsBfs 查找增廣路之後,最短增廣路的長度一定是非減的,也即對於每一個節點,它到源點的最短距離是非減的。 同時根據EKEK的增廣過程,我們可以推導出EKEK算法中所能找到的增廣路的數量爲 Θ(VE)\Theta(VE)。由於 BfsBfs 找增廣路的時間複雜度爲 Θ(E)\Theta(E),而至多進行 Θ(VE)\Theta(VE) 次查找,因此就可以得出EKEK算法的時間複雜度爲 Θ(VE2)\Theta(VE^2)

雖說 EKEK 算法是“時間換碼量”,但當整個圖是稀疏圖的時候,使用EKEK算法不失爲一種簡便可行的方法,但是如果圖的邊數非常多,這個算法的性能也就顯得不是那麼優秀。

SAP

SAPSAP算法是對DinicDinic算法一個小的優化。在DinicDinic算法中,每次增廣都要進行一次BfsBfs來更新層次網絡,這是一種浪費,因爲有些點的層次實際上是不需要更新的。SAPSAP算法就採取一邊找增廣路,一邊更新層次網絡的策略。代碼:

#include <bits/stdc++.h>
using namespace std;

//&Start
#define lng long long
const int inf=0x3f3f3f3f;

//&Debug

//&SAP
const int N=2e5+10,M=4e6;
int p,s,t;
int E=1,g[N],to[M],nex[M],fw[M];
void add(int x,int y,int f){
	nex[++E]=g[x],to[E]=y,fw[E]=f,g[x]=E;
	nex[++E]=g[y],to[E]=x,fw[E]=0,g[y]=E;
}
int dep[N],gap[N],pv[N],pe[N];
int SAP(){
	int flow=0,x=s,f;
	gap[0]=p,pv[s]=s;
	while(dep[s]<p){
		int e;
		for(e=g[x];e;e=nex[e])
			if(fw[e]&&dep[x]==dep[to[e]]+1) break;
		if(e){
			pe[to[e]]=e,pv[to[e]]=x,x=to[e];
			if(x==t){
				f=inf;
				for(int i=x;i!=s;i=pv[i])
					f=min(f,fw[pe[i]]);
				flow+=f;
				for(int i=x;i!=s;i=pv[i])
					fw[pe[i]]-=f,fw[pe[i]^1]+=f;
				x=s;
			}
		} else {
			f=p;
			for(int e=g[x];e;e=nex[e])
				if(fw[e]) f=min(f,dep[to[e]]);
			gap[dep[x]]--;
			if(!gap[dep[x]]) break;
			dep[x]=f+1,gap[dep[x]]++,x=pv[x];
		}
	}
	return flow;
}

//&Main
int n,m;
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t),p=n;
	for(int i=1,x,y,f;i<=m;i++){
		scanf("%d%d%d",&x,&y,&f);
		add(x,y,f);
	}
	printf("%d\n",SAP());
	return 0;
}

SAPSAP算法中源點的層次是最高的。一定要有GAPGAP優化,這個算法的時間複雜度優越性也得不到良好的表現。SAPSAP算法的複雜度上界和DinicDinic一樣也是 Θ(V2E)\Theta(V^2E)

HLPP

HLPPHLPP算法即最高標號預流推進算法。與前面三種算法不同的是,它並不採取找增廣路的思想,而是不斷地在可行流中找到那些仍舊有盈餘的節點,將其盈餘的流量推到周圍可接納流量的節點中。

對於一個最大流而言,除了源點和匯點以外所有的其他節點都應該滿足流入的總流量等於流出的總流量,如果首先讓源點的流量都儘可能都流到其相鄰的節點中,這個時候相鄰的節點就有了盈餘,即它流入的流量比流出的流量多,所以要想辦法將這些流量流出去。這種想法其實很自然,如果不知道最大流求解的任何一種算法,要手算最大流的時候,採取的策略肯定會是這樣,將能流的先流出去,遇到容量不足的邊就將流量減少,直到所有流量都流到了匯點。

但是這樣做肯定會遇到一個問題,可能會有流量從一個節點流出去然後又流回到這個節點。如果這個節點是源點的話這麼做是沒問題的,因爲有的時候通過某些節點是到達不了匯點的,這個時候要將流量流回到源點,但是其他情況就可能會造成循環流動,因此需要用到層次網絡,只在相鄰層次間流動。

#include <bits/stdc++.h>
using namespace std;
#define lng long long
#define fo(i,a,b,c) for(int i=a;i<=b;i+=c)
#define al(i,g,x) for(int i=g[x];i;i=e[i].nex)
const int V=2e3;
const int M=4e5;
const int inf=0x3f3f3f3f;
int n,m,s,t,p;
namespace graph{
	class edge{
	public:
		int adj,nex,fw;
	}e[M];
	int g[V],top=1;
	void add(int x,int y,int w){
		e[++top]=edge{y,g[x],w},g[x]=top;
	}
	void Add(int x,int y,int w){
		add(x,y,w),add(y,x,0);
	}
}using namespace graph;
namespace HLPP{
	int fl[V],dep[V],ct[V<<1]; //節點盈餘、層次和gap優化
	bool vis[V]; //訪問
	queue<int> Q;
	class cmp{public:
		bool operator()(int x,int y){return dep[x]<dep[y];}
	};
	priority_queue<int,vector<int>,cmp> q; //優先推進層次高的節點
	bool bfs(){ //和Dinic差不多的bfs
		fo(i,1,p,1) dep[i]=inf,vis[i]=0;
		Q.push(t),dep[t]=0,vis[t]=1;
		while(Q.size()){
			int x=Q.front();Q.pop(),vis[x]=0;
			al(i,g,x){ int to=e[i].adj;
				if(e[i^1].fw&&dep[to]>dep[x]+1){
					dep[to]=dep[x]+1;
					if(!vis[to]) Q.push(to),vis[to]=1;
				}
			}
		}
		return dep[s]<inf;
	}
	void Push(int x){ //推x節點盈餘的流
		al(i,g,x){ int to=e[i].adj;
			if(e[i].fw&&dep[to]+1==dep[x]){
				int f=min(fl[x],e[i].fw);
				e[i].fw-=f,e[i^1].fw+=f;
				fl[x]-=f,fl[to]+=f;
				if(!vis[to]&&to!=t&&to!=s)
					q.push(to),vis[to]=1;
				if(!fl[x]) break;
			}
		}
	}
	void Low(int x){ //gap優化,離散化層次
		dep[x]=inf;
		al(i,g,x){ int to=e[i].adj;
			if(e[i].fw&&dep[x]>dep[to]+1)
				dep[x]=dep[to]+1;
		}
	}
	int hlpp(){
		if(!bfs()) return 0;
		dep[s]=p; //源點層次最高
		fo(i,1,p,1)if(dep[i]<inf)
			ct[dep[i]]++;
		al(i,g,s){int to=e[i].adj,f; //先將源點推流
			if((f=e[i].fw)>0){
				e[i].fw-=f,e[i^1].fw+=f;
				fl[s]-=f,fl[to]+=f;
				if(to!=t&&to!=s&&!vis[to])
					q.push(to),vis[to]=1;
			}
		}
		while(q.size()){ //取層次大的節點預流推進
			int x=q.top();q.pop(),vis[x]=0;
			Push(x);
			if(fl[x]){
				if(!--ct[dep[x]]) //Gap優化
					fo(to,1,p,1) if(to!=s&&to!=t
					&&dep[to]>dep[x]&&dep[to]<=p)
						dep[to]=p+1;
				Low(x),ct[dep[x]]++;
				q.push(x),vis[x]=1;
			}
		}
		return fl[t];
	}
}using namespace HLPP;
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t),p=n;
	fo(i,1,m,1){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		Add(x,y,z);
	}
	printf("%d\n",hlpp());
	return 0;
}

推進都是從高層次節點推到低層次節點中,源點的層次始終爲節點總數。我們注意到預流推進算法的程序實現中有個優先隊列,這使得程序會先取層次較高的節點推進。因爲層次較低的節點是有可能接受到層次高節點流出的流量的,如果先推層次低的節點的流量,之後它有可能又接受到了高層次節點的流量,那麼又要對其作推進處理。而如果每次都先將高層次節點取出,就可以將所有的高層次的節點的流量都先推入對應的低層次的節點中,在低層次的節點中先累積流量,最後再一起推進,提升效率。

特別的,HLPPHLPP算法的時間複雜度上限爲 Θ(V2E)\Theta(V^2\sqrt E),所以有時HLPPHLPP過得了別的算法過不了的題。

下一篇會講最大流解題技巧、方法。

祝大家學習愉快!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章