网络流之最大流算法总结(FF, 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;
}

 以上就是几种常见的最大流算法,这几种算法都属于増广路算法,也就是不断求增广路来更新最大流,关于最大流的算法还有很多,可以根据图的不同选取想要的算法。

posted @ 2019-08-01 18:05  宿星  阅读(2400)  评论(0编辑  收藏  举报