網絡流之最大流算法總結(FF, EK, Dinic)

目錄

FF算法:最基礎的最大流算法

EK算法:每次BFS尋找増廣路

Dinic算法:EK算法的優化

Dinic+鏈式前向星


這裏以POJ1273這道題爲例,題目鏈接:http://poj.org/problem?id=1273

FF算法:最基礎的最大流算法

通過DFS増廣,直到不能増廣爲止。

記最大流的流量爲F,FF算法最多進行F次DFS,所以其複雜度爲O(F|E|),每一次DFS的複雜度不確定,但是最壞的情況幾乎是不存在的,所以還是比較快的。

最大流算法的精髓就是加了一條反向邊,給了程序有一個後悔的機會,在一次DFS結束之後,每條正向邊減去流向匯點的流量,每條反向邊加上流向匯點的流量。

以下面這個圖爲例,第一次尋找的増廣路是1->2->3->4,流量是5,如果沒有反向邊的話,就已經不能再増廣了,但是很明顯這不是最佳策略。

在引入反向邊後可以發現,有一條新的増廣路1->3->2->4,流量爲5,最後發現沒有増廣了,求得最大流爲10。

在FF算法中邊是用鄰接表來存儲的,但是每次正向邊和反向邊的加減是要同時進行,所以在知道正向邊的同時,要知道反向邊的位置,所以結構體中有一個rev來存儲反向邊的位置。

#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <vector>
#define N 10020
using namespace std;

int n, m, inf=0x7f7f7f;
bool book[N];
struct edge {
	int v;
	int w;
	int rev;                   //在反向邊中存儲的位置
};
vector<edge>e[N];
void add(int u, int v, int w)      //加邊
{
	e[u].push_back(edge{ v, w, e[v].size() });
	e[v].push_back(edge{ u, 0, e[u].size() - 1 });
}
int dfs(int s, int t, int f)            
{
	if (s == t)
		return f;                                   //找到終點
	book[s] = true;
	for (int i = 0; i < e[s].size(); i++)
	{
		edge &G = e[s][i];
		if (G.w > 0 && book[G.v] == false)
		{
			int d = dfs(G.v, t, min(f, G.w));      //兩者之間流量較小的一個
			if (d > 0)
			{
				G.w -= d;                       //改變正向邊和反向邊
				e[G.v][G.rev].w += d;
				return d;
			}
		}
	}
	return 0;
}

int FF(int s, int t)
{
	int ans = 0;
	while (1)
	{
		memset(book, false, sizeof(book)); //每次找増廣路
		int d = dfs(s, t, inf);
		if (d == 0)                       //找不到增廣路返回總流量
			return ans;
		ans += d;
	}
}

int main()
{
	int u, v, w;
	while (scanf("%d%d", &m, &n) != EOF)
	{
		for (int i = 1; i <= n; i++)
			e[i].clear();
		for (int i = 0; i < m; i++)
		{
			scanf("%d%d%d", &u, &v, &w);
			add(u, v, w);
		}
		printf("%d\n", FF(1, n));
	}
	return 0;
}

 

EK算法:每次BFS尋找増廣路

EK算法是基於FF算法的,只是邊的存儲變爲鄰接矩陣存儲,求増廣路的過程變爲BFS,在正向邊和反向邊的改變上也有變化,但是總體上的思路是一樣的,或者說FF, EK, Dinic這三者的思想是相同的。

EK算法的時間複雜度是O(V*E^2)適合邊較少的稀疏圖。

因爲EK算法圖的存儲是用鄰接矩陣存儲,所以每次在輸入流量的時候應該加上原有的流量,在這道題上也有體現。

因爲EK算法不能像FF算法中遞歸改變邊的流量,所以在EK算法中記錄的每一個點的匹配點,通過匹配點來改變邊的流量,這一點只要仔細想想應該能明白。

#include <stdio.h>
#include <queue>
#include <string.h>
#include <algorithm>
#define N 220
using namespace std;
int n, m, e[N][N], pre[N], flow[N], inf=0x7f7f7f;
int bfs(int s, int t)
{
	memset(pre, -1, sizeof(pre));
	queue<int>q;
	q.push(s);
	flow[s] = inf;                       //最開始流量爲無窮 
	while (!q.empty())
	{
		int u = q.front();
		q.pop();
		for (int v = 1; v <= n; v++)         //求増廣路 
		{
			if (e[u][v] > 0 && v != s && pre[v] == -1)   //注意v!=s 
			{
				pre[v] = u;
				q.push(v);
				flow[v] = min(flow[u], e[u][v]);         //現在的流量是流過來的流量和可以流走的量的最小值 
			}
		}
	}
	if (pre[t] == -1)                   //如果沒有到達終點 
		return -1;
	return flow[t];                      //返回流量 
}

int EK(int s, int t)
{
	int ans = 0;
	while (1)
	{
		int d = bfs(s, t);
		if (d == -1)                    //無法在找増廣路 
			break;
		ans += d;
		int p = t; 
		while (p != s)                  //邊的流量發生改變 
		{
			e[pre[p]][p] -= d;
			e[p][pre[p]] += d;
			p = pre[p];
		}
	}
	return ans;
}
int main()
{
	int u, v, w;
	while (scanf("%d%d", &m, &n) != EOF)
	{
		memset(e, 0, sizeof(e));
		while (m--)
		{
			scanf("%d%d%d", &u, &v, &w);
			e[u][v] += w;                     //這裏要加上原有的流量
		}
		printf("%d\n", EK(1, n));
	}
	return 0;
}

Dinic算法:EK算法的優化

Dinic算法是EK算法的優化,實際上和FF算法也是很像的, Dinic通過BFS分層,在用DFS求増廣路,可以達到多路増廣的效果,基本上Dinic算法是比較優秀的算法了。

衆所周知,網絡流題目會卡FF和EK,但是不會卡Dinic[笑]。

可以看到加邊操作是和FF算法是一樣的,分層也是一個比較常規的操作。

Dinic算法引入了一個當前弧優化:在一次BFS分層中已經搜索過的邊不用再搜。

#include <stdio.h>
#include <string.h>
#include <queue>
#include <vector>
#include <algorithm>
#define N 220
using namespace std;

struct edge {
	int v;
	int w;
	int rev;
};
vector<edge>e[N];
int n, m, inf=99999999, dis[N], iter[N];
bool book[N]; 
void add(int u, int v, int w)                  //加邊 
{
	e[u].push_back(edge{ v, w, e[v].size() });
	e[v].push_back(edge{ u, 0, e[u].size() - 1 });
}
 
void bfs(int s)                              //分層 
{
	queue <int>q;
	memset(dis, -1, sizeof(dis));
	dis[s] = 0;
	q.push(s);
	while (!q.empty())
	{
		int u = q.front();
		q.pop();
		for (int v = 0; v < e[u].size(); v++)
		{
			edge G = e[u][v];
			if (dis[G.v] == -1 && G.w)          //有流量時才加入 
			{
				dis[G.v] = dis[u] + 1;
				q.push(G.v);
			}
		}
	}
}

int dfs(int s, int t, int f)
{
	if (s == t)
		return f;
	for (int &v = iter[s]; v < e[s].size(); v++)        //當前弧優化 
	{
		edge &G = e[s][v];
		if (dis[G.v] == dis[s] + 1 && G.w)         //只有層數是+1時才増廣 
		{
			int d = dfs(G.v, t, min(G.w, f));
			if (d > 0)
			{
				G.w -= d;
				e[G.v][G.rev].w += d;
				return d;
			}
		}
	}
	return 0;
}

int Dinic(int s, int t)
{
	int ans = 0;
	while (1)
	{
		bfs(s);                
		if (dis[t] == -1)
			return ans;		
		int d;
		memset(iter, 0, sizeof(iter));          
		while ((d = dfs(s, t, inf)) > 0)       //多次dfs 
			ans += d;
	}
	return ans;
}
int main()
{
	int u, v, w;
	while (scanf("%d%d", &m, &n) != EOF)
	{
		for (int i = 1; i <= n; i++)         //初始化 
			e[i].clear();
		while (m--)
		{
			scanf("%d%d%d", &u, &v, &w);
			add(u, v, w);
		}
		printf("%d\n", Dinic(1, n));
	}
	return 0;
}

下面是把  if (dis[G.v] == dis[s] + 1 && G.w)

改爲         if (dis[G.v] > dis[s]  && G.w) 的結果

Dinic+鏈式前向星

與之前存邊的方式有所不同, 這種存邊是用鏈式前向星來存圖的。

據說這是當前網絡流的主流寫法。

有時候不用前向星的話,可能會被卡掉,在hdu4292上有所體現。

這種存圖的巧妙之處是把正向邊和反向邊一起存儲,這樣正向邊=反向邊^1(異或1),反向邊=正向邊^1。

可以看到除了存邊不同之外,其他的代碼幾乎一致。

#include <stdio.h>
#include <string.h>
#include <queue>
#include <algorithm>
#define N 220
using namespace std;
struct date {
	int v;
	int w;
	int next;
}edge[N*N];
int n, m, cnt, inf=0x3f3f3f, head[N], dis[N], iter[N];
int add(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++;
}
void bfs(int s)
{
	memset(dis, -1, sizeof(dis));
	queue<int>q;
	q.push(s);
	dis[s] = 0;
	while (!q.empty())
	{
		int u = q.front();
		q.pop();
		for (int v = head[u]; v != -1; v = edge[v].next)
		{
			date G = edge[v];
			if (dis[G.v] == -1 && G.w && G.v != s)
			{
				dis[G.v] = dis[u] + 1;
				q.push(G.v);
			}
		}
	}
}

int dfs(int s, int t, int f)
{
	if (s == t)
		return f;
	for (int &v = iter[s]; v != -1; v = edge[v].next)
	{
		date &G = edge[v];
		if (G.w && dis[G.v] == dis[s] + 1)
		{
			int d = dfs(G.v, t, min(G.w, f));
			if (d > 0)
			{
				edge[v].w -= d;
				edge[v ^ 1].w += d;
				return d;
			}
		}
	}
	return 0;
}

int Dinic(int s, int t)
{
	int ans = 0;
	while (1)
	{
		bfs(s);
		if (dis[t] == -1)
			return ans;
		
		for (int i = 1; i <= n; i++)
			iter[i] = head[i];
		int d;
		while ((d = dfs(s, t, inf)) > 0)
			ans += d;
	}
}
int main()
{
	int u, v, w;
	while (scanf("%d%d", &m, &n) != EOF)
	{
		cnt = 0;
		memset(head, -1, sizeof(head));
		while (m--)
		{
			scanf("%d%d%d", &u, &v, &w);
			add(u, v, w);
		}
		printf("%d\n", Dinic(1, n));
	}
	return 0;
}

 以上就是幾種常見的最大流算法,這幾種算法都屬於増廣路算法,也就是不斷求增廣路來更新最大流,關於最大流的算法還有很多,可以根據圖的不同選取想要的算法。

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