【學習筆記】網路流

linyihdfj發表於2022-07-11

發現自己對網路流的理解更深了,所以就寫一篇學習筆記了。

網路流基礎:

最大流的概念:

流網路:一個有向圖(\(G = (V,E)\)),包含一個源點和一個匯點,每條邊都有一個權值代表容量(\(c\))。

可行流:給每一條邊指定一個流量(\(f\)),當任意一條邊都滿足以下條件時,這個圖就叫做一個可行流:

  1. 容量限制:任意一條邊的流量小於等於它的容量,
  2. 流量守恆:任意一個點的流出的流量等於流入的流量,

最大流:可行流中流量最大的可行流就叫做最大流,也叫做最大可行流

最小割的概念:

割:將所有的點分為兩個集合,滿足一個集合(\(S\))中含有源點(\(s\))另一個集合(\(T\))中含有匯點(\(t\)),連線這兩個集合的邊的容量之和就叫做這個割的大小。

最小割:割裡面大小最小的一個割

殘餘網路的概念:

殘餘網路的邊與原網路中的邊基本一致,但是多了原圖中的邊的反向邊,殘餘網路是定義在可行流上的,也就是說不同的可行流有不同的殘餘網路。
若在 \(G\) 中有一條可行流 \(f\),那麼這種情況下的殘餘網路的邊的容量就為:

  1. 原圖中有的邊: \(f`_{u,v} = c_{u,v} - f_{u,v}\)
  2. 原圖中的邊的反向邊: \(f`_{v,u} = f_{u,v}\)

對於正向的邊也就是可以理解為可以再從這條邊流下去多少,對於反向邊也就是可以理解為能將流量退回來多少

幾個定理:

(1)若有原圖上的一條可行流 \(f\),以及一條殘量網路上的可行流 \(f`\),則 \(f + f`\) 依舊是原圖的一條可行流,這種加是指對應邊的權值相加。

  1. 容量限制:考慮 \(0 \le f_{u,v} \le c_{u,v}\),而 \(0 \le f`_{u,v} \le c_{u,v} - f_{u,v}\),所以 \(0 \le f_{u,v} + f`_{u,v} \le c_{u,v}\),即滿足容量限制
  2. 流量守恆:\(\sum_{(u,x) \in E} \ f_{u,x} = \sum_{(x,u) \in E} \ f_{x,u}\)\(\sum_{(u,x) \in E} \ f`_{u,x} = \sum_{(x,u) \in E} \ f`_{x,u}\),兩者相加之後仍相等

(2)若殘量網路中沒有可行流,則此時的原圖中的可行流一定是最大流
若是殘量網路中有可行流,則將這個可行流加到原圖中的可行流中顯然更優,如果沒有則顯然沒有辦法更優

(3)任意一個割的容量都一定大於等於任意一個可行流的流量

考慮一個任意的割,一條可行流一定是橫跨了這兩個點集,也就是一定是經過了這幾條紅邊,也就是任意一個可行流的大小一定不會超過這個割的大小

(4)最大流最小割定理:最大流等於最小割
我們記 \(|f|\)\(f\) 這個可行流的流量,\(C_{S,T}\)\(S,T\) 這個割的容量。

  1. 可以證明一定會存在一個可行流的流量等於某一個割的容量,不妨記這個可行流為 \(f\),由上文可以知:\(|f_{max}| \le C_{S,T}\),因為 \(\exists \ |f| = C_{S,T}\),所以 \(|f_{max}| \le |f|\),因為 $|f| \le f_{max} $,所以可行流 \(f\) 就是最大流。
  2. 因為 \(C_{min} \le C_{S,T}\),而 \(C_{S,T} = |f_{max}|\),所以 \(C_{min} \le |f_{max}|\),因為 \(|f_{max}| \le C_{S,T}\),所以 \(|f_{max}| = C_{min}\)

求解最大流的演算法:

\(dinic\)

基本原理:

\(dinic\) 求解最大流就是使用:殘餘網路中的可行流加原網路中的可行流一定是原網路的一條可行流,若殘餘網路中沒有可行流則原網路中的可行流就是最大流
基本做法就是,每次儘可能多地找到殘餘網路中的可行流,然後將殘餘網路中的可行流合併到原網路的可行流中。

時間複雜度:

時間複雜度 \(O(n^2m)\),但是因為 \(dinic\) 有非常多的優化所以可以跑的飛快,可以過掉點數邊數在 \(10^4 - 10^5\) 的資料

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4+5;
const int MAXM = 2e5+5; 
const int INF = 1e18+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[2 * MAXM];
int n,m,s,t,cnt = 1,head[MAXN],dis[MAXN],cur[MAXN];  
//s 源點,t 匯點,cnt 從 1 開始
void add_edge(int from,int to,int val){   //只維護殘量網路 
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0);  //殘量網路建雙向邊 
	head[to] = cnt;
}
bool bfs(){  //判斷是否有可行流 
	memset(dis,-1,sizeof(dis));  //分層圖 
	queue<int> q;
	q.push(s);dis[s] = 1;cur[s] = head[s];   //cur 即當前弧優化 
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i=head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] == -1 && e[i].val){  //找到可行流,必須流量大於 0 
				dis[to] = dis[now] + 1;
				cur[to] = head[to];
				if(to == t)	return true;  //能到達匯點所以就有 
				q.push(to);
			}
		}
	}
	return false;
}
int dfs(int now,int limit){   //找到可行流的流量。
//limit 即走過的這一條路徑的限制,或者理解為流到這裡的流量 
	if(now == t)	return limit;   //流到了匯點所以就找到了 limit 大小的可行流 
	int flow = 0;
	for(int i=cur[now]; i && flow < limit; i = e[i].nxt){  //flow < limit 判斷條件的優化 
 		cur[now] = i;  //當前弧優化 
		int to = e[i].to;
		if(e[i].val && dis[to] == dis[now] + 1){  //滿足可行流 + 分層圖 
			int h = dfs(to,min(e[i].val,limit - flow));
			if(!h)	dis[to] = -1;  //-1 優化 
			e[i].val -= h;e[i^1].val += h;flow += h;
		}
	}
	return flow;
}
int dinic(){
	int ans = 0,flow = 0;
	while(bfs())	while(flow = dfs(s,INF)) ans += flow;
	return ans;
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1; i<=m; i++){
		int from,to,val;
		scanf("%d%d%d",&from,&to,&val);
		add_edge(from,to,val); 
	}
	printf("%d\n",dinic());
	return 0;
}

集中解釋一下幾個優化:

  1. 分層圖優化:因為原圖中可以有環,所以就將圖分層,即上一層只能到達下一層,這樣就能保證不會一直在某一個環上轉圈
  2. 當前弧優化:對於這一個點的前幾條邊,因為我們已經將它流完了,所以下一次即使再訪問到當前節點也沒有必要訪問那些邊了
  3. flow < limit :顯然我們的流出去的流量必須小於等於流入的流量,而等於顯然意味著不能流了
  4. -1 優化:我們從某一個點流不到匯點一點流量,那麼我們下一次也沒有必要再訪問這個節點了,因為這個點已經滿了

上下界網路流:

無源匯上下界可行流:

問題描述:

給定一個流網路,沒有源點與匯點,每一條邊有最小的流量限制以及最大的流量限制,請判斷是否有可行流,並輸出任意一組方案。

問題分析:

看到這個題我們最顯然的一種想法就是:將最小限制通過減法變成 \(0\),那麼就可能可以在這種圖上求一遍網路流得到一些新的東西了。
我們考慮建一下兩個流網路:下界網路、差網路。這兩個網路中連邊與原網路一致,只是邊的容量不一致。
下界網路:在下界網路中邊 \((u,v)\) 的容量為原網路中這條邊的最小流量限制 \((low_{u,v})\)
差網路:在差網路中邊 \((u,v)\) 的容量為原網路中這條邊的最大流量限制 \((high_{u,v})\) 減去最小流量限制
可以發現一點:下界網路中我們必須流滿,這樣下界網路中的流量加差網路中的可行流的流量如果能形成可行流,那麼必然是原網路的一條可行流。
考慮是不是可行流即是否滿足容量限制和流量守恆:

  1. 容量限制:顯然對於邊 \((u,v)\) 它的流量大小即:\(low_{u,v} \le f_{u,v} \le high_{u,v}\),符合上下界的要求
  2. 流量守恆:對於差網路中一個點的流入流出流量一定守恆,但是對於下界網路卻不一定,所以不一定滿足。

我們為了使得下界網路加上差網路的可行流之後可以形成一條原網路的可行流,我們就要對差網路進行一些操作。
對於下界網路的一個點,我們記它的流入流量與流出流量的差為 \(A_{x}\),即 \(A_{x} = \sum f_{in} - \sum f_{out}\)
那麼為了使得流量守恆也就意味著在差網路中這個點流入流量與流出流量的差就要為 \(-A_{x}\)

  1. \(-A_{x} > 0\) 那麼就意味著流入流量要多一些,那麼就從該點向匯點連邊就好了。
  2. \(-A_{x} < 0\) 那麼就意味著流出流量要多一些,那麼就從源點向該點連邊就好了。

注意這裡指的流入與流出流量都是指的原差網路上的邊有的流量,因為只有這些邊才與下界網路對應邊相加,所以我們從源點連入也就意味著增加流出流量,連向匯點就意味著增加流入流量。因為我們在差網路上要滿足是一條可行流即滿足流量守恆,所以連入的邊流量多了,即流出流量多了,連出的邊流量多了,即流入流量多了。
下圖即為一個例子:

(來源自知乎
連完之後的差網路是這樣的:

(來源自知乎
通過分析我們也能發現,我們在差網路上新連的邊必須流滿,因為只有他們流滿才能使得差網路加下屆網路是原網路的一個可行流,而如果流不滿即無解。

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 210;
const int MAXM = 1e5+5;
const int INF = 1e9+7;
struct edge{
	int nxt,to,val,low;
	edge(){}
	edge(int _nxt,int _to,int _val,int _low){
		nxt = _nxt,to = _to,val = _val,low = _low;
	}
}e[MAXM];
int n,m,s,t,cnt = 1,c[MAXN],cur[MAXN],head[MAXN],dis[MAXN];
void add_edge(int from,int to,int val,int low){
	e[++cnt] = edge(head[from],to,val,low);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0,low);
	head[to] = cnt; 
}
bool bfs(){
	memset(dis,-1,sizeof(dis));
	queue<int> q;
	q.push(s);dis[s] = 1;cur[s] = head[s];
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] == -1 && e[i].val){
				dis[to] = dis[now] + 1;
				cur[to] = head[to];
				if(to == t)	return true;
				q.push(to); 
			}
		}
	}
	return false;
}
int dfs(int now,int limit){
	if(now == t)	return limit;
	int flow = 0;
	for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
		int to = e[i].to;
		cur[now] = i;
		if(dis[to] == dis[now] + 1 && e[i].val){
			int h = dfs(to,min(e[i].val,limit - flow));
			if(!h)	dis[to] = -1;
			e[i].val-=h;e[i^1].val+=h;flow+=h; 
		}
	}
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(s,INF))
			ans += flow;
	}
	return ans;
}
int main(){
	cin>>n>>m;
	s = n + 1,t = n + 2;
	for(int i=1; i<=m; i++){
		int from,to,low,high;
		cin>>from>>to>>low>>high;
		add_edge(from,to,high - low,low);
		c[from] -= low,c[to] += low; 
	}
	int res = 0;
	for(int i=1; i<=n; i++){
		if(c[i] < 0){
			add_edge(i,t,-c[i],0);
		}
		else if(c[i] > 0){
			add_edge(s,i,c[i],0);
			res += c[i];
		}
	}
	int ans = dinic();
	if(ans != res){
		printf("NO");
	}
	else{
		printf("YES\n");
		for(int i=2; i<=m * 2; i+=2){
			printf("%d\n",e[i].low + e[i^1].val);
			//一條邊的流量就是其反向邊的 val 
		}
	}
	return 0;
}
因為我們是加入一條正向邊立刻加入反向邊,所以列舉正向邊就從第一條開始每次加二就好了

相關文章