1. 程式人生 > 其它 >網路流詳解+題目

網路流詳解+題目

介紹

首先先了解網路和流

再瞭解一下網路流的相關定義:

  • 源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點。
  • 匯點:另一個點也很特殊,只進不出,叫做匯點。
  • 容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[i,j]表示,流量則通常是f[i,j].
  • 殘量:就是當前容量減去流量

網路流的常見問題
網路流問題中常見的有以下三種:最大流,最小割,費用流。

最大流
1、我們有一張圖,要求從源點流向匯點的最大流量(可以有很多條路到達匯點),就是我們的最大流問題。

最小費用最大流
2、最小費用最大流問題是這樣的:每條邊都有一個費用,代表單位流量流過這條邊的開銷。我們要在求出最大流的同時,要求花費的費用最小。

最小割
3、割其實就是刪邊的意思,當然最小割就是割掉 \(X\) 條邊來讓 \(S\)\(T\) 兩個集合(可以理解為 源點\(S\)匯點\(T\) 不連通)不互通。我們要求 \(X\) 條邊加起來的流量總和最小。這就是最小割問題。

1. 求最大流

首先,明確最大流是幹什麼的

給定指定的一個有向圖,其中有兩個特殊的點源S(Sources)和匯T(Sinks),每條邊有指定的容量(Capacity),求滿足條件的從S到T的最大流(MaxFlow).

通俗一點,

就好比你家是匯 自來水廠是源

然後自來水廠和你家之間修了很多條水管子接在一起 水管子規格不一 有的容量大 有的容量小

然後問自來水廠開閘放水 你家收到水的最大流量是多少

如果自來水廠停水了 你家那的流量就是0 當然不是最大的流量

但是你給自來水廠交了100w美金 自來水廠拼命水管裡通水 但是你家的流量也就那麼多不變了 這時就達到了最大流

理解起來還好吧,也就是上文說的那樣

1.1Edmond-Karp演算法

1.1.1 演算法解析

首先引入增廣路的概念:

  • 增廣路

增廣路指從s到t的一條路,水流流過這條路,使得當前可以到達t的流量可以增加。

通俗一點

在原圖 \(G\) 中若一條從源點到匯點的路徑上所有邊的 剩餘容量都大於 \(0\),這條路被稱為增廣路(Augmenting Path)。

通過定義,我們顯然可以看出求最大流其實就是不斷尋找增廣路的過程。

可以用BFS也可以用DFS,只是BFS快一點

詳細一點:就是用 BFS 找增廣路,然後對其進行增廣。你可能會問,怎麼找?怎麼增廣?

1、找?我們就從源點一直 BFS 走來走去,碰到匯點就停,然後增廣(每一條路都要增廣)。我們在 BFS 的時候就注意一下流量合不合法就可以了。

2、增廣?其實就是按照我們找的增廣路在重新走一遍。走的時候把這條路的能夠成的最大流量減一減,然後給答案加上最小流量就可以了。

再講一下 反向邊。增廣的時候要注意建造反向邊,原因是這條路不一定是最優的,這樣子程式可以進行反悔(就相當於藍色和橙色的線抵消了。)。假如我們對這條路進行增廣了,那麼其中的每一條邊的正向邊就減去流量,反向邊的流量就加上它的流量。

1.1.2 複雜度

EK 演算法的時間複雜度為 \(O(nm^2)\) (其中 \(n\) 為點數, \(m\) 為邊數)。效率還有很大提升空間。

1.1.3 模板

P3376 【模板】網路最大流為例

#include <bits/stdc++.h> 
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
struct o{
	int pre,edge;
}p[10005];

struct node{
	int next,to,w;
}e[10005];
void add(int x,int y,int w){
	e[++cnt].w=w;
	e[cnt].to=y;
	e[cnt].next=head[x];
	head[x]=cnt;
}

bool dfs(){
	queue<int>q;
	memset(vis,0,sizeof(vis));
	q.push(s);
	vis[s]=1;
	while (!q.empty()){
		int top=q.front();
		q.pop();
		for (int i=head[top];i;i=e[i].next){
			if (!vis[e[i].to]&&e[i].w){//若沒去過且容量大於0
				vis[e[i].to]=1;//標記去過
				p[e[i].to].edge=i;//記錄路徑,edge為邊
				p[e[i].to].pre=top;//記錄路徑,pre為上一個點
				if (e[i].to==t){
					return 1;
				}q.push(e[i].to);
			}
		}
	}return 0;//若全部搜完還沒有出現增廣路,返回0
}

int main(){
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (int i=1;i<=m;i++){
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w);
        add(v,u,0);
    }
    long long ans=0;
    while (dfs()){
    	int minn=999999999;
    	for (int i=t;i!=s;i=p[i].pre) //找最小容量
    		minn=min(minn,e[p[i].edge].w);
    	for (int i=t;i!=s;i=p[i].pre){//更改
    		e[p[i].edge].w-=minn;
    		e[p[i].edge^1].w+=minn;
		}
		ans+=minn;
    }
    printf("%lld",ans);
    return 0;
}

1.2 Dinic演算法

1.2.1 演算法思路

Dinic演算法的思想也是分階段地在層次網路中增廣。它與最短增廣路演算法不同之處是:最短增廣路每個階段執行完一次BFS增廣後,要重新啟動BFS從源點Vs開始尋找另一條增廣路;而在Dinic演算法中,只需一次DFS過程就可以實現多次增廣,這是Dinic演算法的巧妙之處。Dinic演算法具體步驟如下:

(1)初始化容量網路和網路流。

(2)構造殘留網路和層次網路,若匯點不再層次網路中,則演算法結束。

(3)在層次網路中用一次DFS過程進行增廣,DFS執行完畢,該階段的增廣也執行完畢。

(4)轉步驟(2)。

在Dinic的演算法步驟中,只有第(3)步與最短增廣路相同。在下面例項中,將會發現DFS過程將會使演算法的效率有非常大的提高。

構造層次網路就是給圖分層,使得增廣路最短

1.2.2 複雜度

因為一次DFS的複雜度為 \(O(nm)\) ,所以,Dinic演算法的總複雜度即\(O(n^2m)\)

1.2.3 模板

還是以模板題為例

#include <bits/stdc++.h>
#define LL long long
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
int d[205];
struct node{
	int next,to,w;
}e[10005];

LL ans=0;

inline int read(){   
	int x=0,f=1;  
	char ch=getchar();  
	while(ch<'0'||ch>'9'){  
		if(ch=='-')  
		f=-1;  
		ch=getchar();  
	}  
	while(ch>='0'&&ch<='9'){  
		x=x*10+ch-'0';  
		ch=getchar();  
	}  
	return x*f;  
}


void add(int x,int y,int w){
	e[++cnt].w=w;
	e[cnt].to=y;
	e[cnt].next=head[x];
	head[x]=cnt;
}

bool bfs(){
	memset(d,0x3f,sizeof(d));
	memset(vis,0,sizeof(vis));
	queue<int>q;
	vis[s]=1;
	d[s]=0;
	q.push(s);
	while (!q.empty()){
		int top=q.front();
		q.pop();
		for (int i=head[top];i;i=e[i].next){
			if (d[e[i].to]>d[top]+1&&e[i].w){
				d[e[i].to]=d[top]+1;
				if (!vis[e[i].to]){
					vis[e[i].to]=1;
					q.push(e[i].to);
				}
			}
		}
	}
	if (d[t]==d[0]){
		return 0;
	}
	return 1;
}
LL dfs(int x,int minn){
	int use=0;
	if (x==t){
		ans+=minn;
		return minn;
	}
	for (int i=head[x];i;i=e[i].next){
		if (e[i].w&&d[e[i].to]==d[x]+1){
			int nex=dfs(e[i].to,min(minn-use,e[i].w));
			if (nex>0){
				use+=nex;
				e[i].w-=nex;
				e[i^1].w+=nex;
				if (use==minn)
					break;
			}
		}
	}
	return use;
}


int main(){
	n=read();m=read();s=read();t=read();
    for (int i=1;i<=m;i++){
    	u=read();v=read();w=read();
        add(u,v,w);
        add(v,u,0);
    }
    while (bfs()){
    	dfs(s,999999999);
    }
    printf("%lld",ans);
    return 0;
}

然後我們就會發現,我們TLE了一個點,這是就需要優化,可以想到至高無上的弧優化。

1.3 Dinic演算法+當前弧優化

1.3.1 演算法優化

當前弧優化實際上只是增加了一個數組 \(cur\),用 \(cur_i\) 代替鄰接表中的 \(head_i\)
原理是:當我們在dfs時,從u點出發訪問到了第i條邊,就說明前i-1條邊都已經被榨乾了,所以才到了第i條邊,因此我們直接改cur[u] = i,下次訪問從u點出發的邊時,就會直接訪問第i條邊,省略了前面i-1條邊。

不懂的話看看下面程式碼模板,就會理解了(或者可以去了解一下鏈式前向星)。

1.3.2 完整程式碼

#include <bits/stdc++.h>
#define LL long long
using namespace std;
int cnt=1;
int n,m,s,t;
int u,v,w;
bool vis[205];
int head[205];
int d[205];
int cur[205];
struct node{
	int next,to,w;
}e[10005];

LL ans=0;

inline int read(){   
	int x=0,f=1;  
	char ch=getchar();  
	while(ch<'0'||ch>'9'){  
		if(ch=='-')  
		f=-1;  
		ch=getchar();  
	}  
	while(ch>='0'&&ch<='9'){  
		x=x*10+ch-'0';  
		ch=getchar();  
	}  
	return x*f;  
}


void add(int x,int y,int w){
	e[++cnt].w=w;
	e[cnt].to=y;
	e[cnt].next=head[x];
	head[x]=cnt;
}

bool bfs(){
	for (int i=0;i<=n;i++){
		d[i]=0x3ffffff;
		vis[i]=0;
		cur[i]=head[i];
	}
	queue<int>q;
	vis[s]=1;
	d[s]=0;
	q.push(s);
	while (!q.empty()){
		int top=q.front();
		q.pop();
		for (int i=head[top];i;i=e[i].next){
			if (d[e[i].to]>d[top]+1&&e[i].w){
				d[e[i].to]=d[top]+1;
				if (!vis[e[i].to]){
					vis[e[i].to]=1;
					q.push(e[i].to);
				}
			}
		}
	}
	if (d[t]==d[0]){
		return 0;
	}
	return 1;
}
LL dfs(int x,int minn){
	int use=0;
	if (x==t){
		ans+=minn;
		return minn;
	}
	for (int i=head[x];i;i=e[i].next){
		cur[x]=i;
		if (e[i].w&&d[e[i].to]==d[x]+1){
			int nex=dfs(e[i].to,min(minn-use,e[i].w));
			if (nex>0){
				use+=nex;
				e[i].w-=nex;
				e[i^1].w+=nex;
				if (use==minn)
					break;
			}
		}
	}
	return use;
}


int main(){
	n=read();m=read();s=read();t=read();
    for (int i=1;i<=m;i++){
    	u=read();v=read();w=read();
        add(u,v,w);
        add(v,u,0);
    }
    while (bfs()){
    	dfs(s,0x3ffffff);
    }
    printf("%lld",ans);
    return 0;
}

2.求最小割

\((CUT)\)
割是網路中頂點的劃分,它把對於一個網路流圖 \(G=(V,E)\)的所有頂點劃分成兩個集合 \(S\)\(T\),且\(S+T=V\) ,其中源點\(s∈S\),匯點\(t∈T\)。記為\(c(S,T)\)

定義割\((S,T)\)的容量\(c(S,T)\)表示所有從\(S\)\(T\)的邊的容量之和(注意是有方向的),即\(c(S,T)=\sum\limits_{u\in S, v \in T}c(u,v)\)

就是一個恐怖分子想要,砍斷水管使你家斷水,至少要砍斷那些水管使水管容量和最小

顯然上圖的最小流是8,但是一想,最大流也是8,這之間有什麼關係嗎?

2.1 最大流最小割定理

在任何的網路中,最大流的值等於最小割的容量

具體的證明分三部分

1.任意一個流都小於等於任意一個割
這個很好理解 自來水公司隨便給你家通點水,構成一個流
恐怖分子隨便砍幾刀 砍出一個割
由於容量限制,每一根的被砍的水管子流出的水流量都小於管子的容量
每一根被砍的水管的水本來都要到你家的,現在流到外面 加起來得到的流量還是等於原來的流
管子的容量加起來就是割,所以流小於等於割
由於上面的流和割都是任意構造的,所以任意一個流小於任意一個割

2.構造出一個流等於一個割
當達到最大流時,根據增廣路定理
殘留網路中s到t已經沒有通路了,否則還能繼續增廣
我們把s能到的的點集設為S,不能到的點集為T
構造出一個割集C(S,T),S到T的邊必然滿流 否則就能繼續增廣
這些滿流邊的流量和就是當前的流即最大流
把這些滿流邊作為割,就構造出了一個和最大流相等的割

相當於在殘量網路中,源點能到達的結點的各個邊的容量和為最大流

所以如果我們要求一個最小割的邊集,我們只要跑一編最大流,然後在殘量網路中找正向邊殘量為0的邊,那麼這條邊肯定在最小割裡面,
這樣就可以得到一組最小割的邊集

3.最大流等於最小割
設相等的流和割分別為F_m和C_m
則因為任意一個流小於等於任意一個割

  任意F≤F_m=C_m≤任意C 

費用流

給定一個網路