求解最大流的四种算法介绍、利用最大流模型解题入门

上一篇中介绍了网络流的基础,最大流最小割定理的证明,下面来看如何求一个容量网络的最大流,这里介绍四种算法:EK算法、SAP算法、DINIC算法、HLPP算法。这四种算法中,前三种基于增广路,最后一种基于预流推进。

基于增广路的算法

Ford-Fulkerson算法
先来简单提一下Ford-Fulkerson算法。
在上一节中证明了,如果一个可行流中没有增广路,那么此时这个可行流的流量就是最大流,因此Ford-Fulkerson算法就是在一个可行流中不断地遍历寻找增广路,如果有增广路,那么就在这个增广路上做调整(前向弧流量增加,后向弧流量减少)来消除增广路,当在整个可行流中再也没法找到一条增广路时,就得到了最大流。这一思想非常简单,也很好理解,但问题的关键是:如何在一个可行流中高效地寻找增广路?Ford-Fulkerson用的是一种标号法,但是那种方式的时间复杂度可能会依赖于网络流中各边的容量,在最坏的情况下复杂度是O(Ef)(E为边数,f为所有边流量的最大值),例如下图中,如果在查找增广路时先选择A->B->C->D,再选择A->C->B->D,再走A->B->C->D……如此就需要走2000次,而实际上直接走A->B->D,A->C->D两次就完成了。而Furd-Fulkerson算法确实有可能会如前一种的方式进行,因此就不具体介绍了。
EK、SAP、DINIC算法都基于这样的消除增广路的思想,但它们给出了更好的查找增广路的方式,下面来分别来看这三种算法。
在这里插入图片描述

EK算法
最简单的算法莫过于暴力搜索,而EK算法正是如此。
在每次搜索增广路的时候,都采取BFS的策略,将所有的从源点到汇点的路径都找出来,那么如果有增广路,就一定可以将它找出来。因此采用BFS策略首先是正确的,来看一下它的代码实现:

//capacity:容量
//flow:流量
//parent:记录在一条增广路中每个节点的前一个节点
//alpha:记录在增广路中当每个节点所能调整的流量的最大值

int EK(int m)
{
	//初始化操作
	int result = 0;
	for (int i = 1; i <= m; i++)	parent[i] = alpha[i] = 0;
	queue<int> vertexQueue;
	while (true)
	{
		memset(alpha, 0, sizeof(alpha));
		alpha[1] = INF;
		vertexQueue.push(1);
		//BFS过程
		while (!vertexQueue.empty())
		{
			int vtop = vertexQueue.front();
			vertexQueue.pop();
			for (int i = 1 ;  i <= m ; i ++ )
			{
				//如果目标节点还未在增广路中出现并且可以调整流量
				if (!alpha[i] && flow[vtop][i] < capacity[vtop][i])
				{
					parent[i] = vtop;
					alpha[i] = min(capacity[vtop][i] - flow[vtop][i], alpha[vtop]);
					vertexQueue.push(i);
				}
			}
		}
		//汇点可调整流量为0,说明没有增广路了,算法结束
		if (alpha[m] == 0)
		{
			return result;
		}
		//汇点可调整流量不为0,那么找到了增广路,增广路上所有节点做流量调整
		for (int i = m; i != 1; i = parent[i])
		{
			flow[parent[i]][i] += alpha[m];//前向弧流量增加
			flow[i][parent[i]] -= alpha[m];//后向弧流量减少
		}
		//由于一开始流量都为0,调整多少能量就代表整个可行流的流量增加了多少
		result += alpha[m];
	}
}

那么如何评估它的性能呢?首先可以确定的是,它不会出现像Ford-Fulkerson那样的问题,考虑上面那张图所示的情形,如果采用EK算法,是肯定不会走A->B->C->D的,为什么?因为采用BFS获取的路径一定是最短距离的路径,很明显上图中从源点到汇点的最短距离为3,因此EK算法能够避免Ford-Fulkerson遇到的问题。
但是仅仅如此还是不能对EK算法的性能有一个清晰的认识,需要知道其准确的时间复杂度。不妨先来看一个EK算法的运行实例:(图来自wiki)
在这里插入图片描述
红色的路径就是每次BFS所找到的增广路。从这张图中可以观察到一个事实:在每次BFS查找增广路之后,最短增广路的长度一定是非减的,也即对于每一个节点,它到源点的最短距离是非减的。这个性质可以有严格的证明,见《算法导论》(引理26.7 P426),直观上想象一下,如果对于一个给定的图确定了从源点到某一点的最短距离为 d d d,现在在这个图中去掉一些边,那么从源点到这一点要么变得不连通,要么距离会不变,要么距离会增大,绝对不可能减少。因为如果它减少为 d ′ &lt; d d&#x27;&lt;d d<d,那么把去掉的这些边加回来,得到原图,这一过程并没有影响到 d ′ d&#x27; d的这条路径,因此原图中还肯定存在距离为 d ′ d&#x27; d的路径,这与 d d d为最短距离矛盾。增广路调整的过程,就相当于在原图中去掉了一些边,因为某些前向弧变成了满流,后向弧变成了零流,没办法再经过这些边了。
基于这一点,可以证明一个引理:
EK算法中所能找到的增广路的数量为O(VE)
证明
每次调整增广路的时候,所调整的流量为所有边可调整流量的最小值,那么就定义具有最小值的那条边为 关键边,显然每条增广路都必须至少有一条关键边
设流f的源点为s,汇点为t,假设边(u,v)成为某次BFS搜索得到的增广路中的关键边,此时 d i s t ( s , v ) = d i s t ( s , u ) + 1 dist(s,v) = dist(s,u) + 1 dist(s,v)=dist(s,u)+1,在这次增广路流量调整后,这条边的可调整流量将变为0,也就是说(u,v)会从残存网络中消失。如果边(u,v)想要再度成为关键边,那么(u,v)的流量必须要减少,也就是说当(u,v)再度成为关键边时一定有 d i s t ′ ( s , u ) = d i s t ′ ( s , v ) + 1 dist&#x27;(s,u) = dist&#x27;(s,v)+1 dist(s,u)=dist(s,v)+1,而又有 d i s t ′ ( s , u ) &gt; = d i s t ( s , u ) , d i s t ′ ( s , v ) &gt; = d i s t ( s , v ) dist&#x27;(s,u) &gt;= dist(s,u),dist&#x27;(s,v) &gt;= dist(s,v) dist(s,u)>=dist(s,u),dist(s,v)>=dist(s,v)因此 d i s t ′ ( s , u ) &gt; = d i s t ( s , u ) + 2 dist&#x27;(s,u) &gt;= dist(s,u) + 2 dist(s,u)>=dist(s,u)+2,也就是说当边 ( u , v ) (u,v) (u,v)两次成为关键边时, d i s t ( s , u ) dist(s,u) dist(s,u)至少增加2。而 d i s t ( s , u ) &lt; = ∣ V ∣ − 2 dist(s,u) &lt;= |V| - 2 dist(s,u)<=V2( ∣ V ∣ |V| V为顶点总数),因此边 ( u , v ) (u,v) (u,v)成为关键边的次数至多为 O ( ∣ V ∣ − 2 2 ) O(\frac{|V|-2}{2}) O(2V2) O ( ∣ V ∣ ) O(|V|) O(V),整个图中所有的边理论上都有可能成为关键边,因此所有边成为关键边的次数至多为 O ( V E ) O(VE) O(VE),每条增广路至少有一条关键边,因此增广路的数量至多为 O ( V E ) O(VE) O(VE),引理得证。

由于BFS找增广路的时间复杂度为 O ( E ) O(E) O(E),而至多进行 O ( V E ) O(VE) O(VE)次查找,因此就可以得出EK算法的时间复杂度为 O ( V E 2 ) O(VE^2) O(VE2)

EK算法的代码实现很简单,当整个图是稀疏图的时候,使用EK算法不失为一种简便可行的方法,但是如果图的边数非常多,这个算法的性能也就显得不是那么优秀。可以看到EK算法的处理在每次找到增广路后,就从源点开始重新再BFS遍历查找,这一过程中有很多的遍历都是没必要的。

Dinic算法
在后面这三种算法的介绍中,它们都用到了一个共同的结构:分层网络。
在这里插入图片描述
其实也就是做一次BFS,先对源点标号为1,然后遍历源点的相邻节点,标号为2,再遍历这些节点的相邻节点……
下面来介绍Dinic算法:
1.将源点的层次设为1,其余层次初始化为0。
2.沿着非饱和前向弧和非零流后向弧构造层次网络,如果发现汇点不在层次网络中则算法终止。
3.在层次网络中,沿着相邻层搜索所有的增广路,并做相应的流量调整,回退到1。
来看一个具体的求解实例:(图来自wiki)
在这里插入图片描述
第一次建立层次网络,找到了蓝线表示的三条增广路,做流量调整
在这里插入图片描述
第二次建立层次网络,边(s,1) (1,3) (1,2)均不在建立层次网络中起作用,找到一条增广路。
在这里插入图片描述
第三次建立层次网络,汇点不在层次网络中,算法终止。

Dinic算法的代码实现为:

//构建层次网络
void bfs()
{
	memset(visit, false, sizeof(visit));
	memset(dist, 0, sizeof(dist));
	queue<int> vertQue;
	vertQue.push(1);
	dist[1] = 0;
	visit[1] = true;
	while (!vertQue.empty())
	{
		int vTop = vertQue.front();
		vertQue.pop();
		for (int i = head[vTop] ; i != -1 ; i = Edges[i].next)
		{
			//目标节点还未被确立层次,并且可以到达目标节点,就进行层次更新
			if (Edges[i].capacity && !visit[Edges[i].to])
			{
				dist[Edges[i].to] = dist[vTop] + 1;
				visit[Edges[i].to] = true;
				vertQue.push(Edges[i].to);
			}
		}
	}
}
//dfs查找所有增广路并做流量调整
int dfs(int end , int u , int delta)
{
	if (u == end) return delta;
	int res = 0;
	for (int i = head[u] ; i != -1  ; i = Edges[i].next)
	{
		if (dist[Edges[i].to] == dist[u] + 1)
		{
			int dd = dfs(end, Edges[i].to, min(Edges[i].capacity, delta));
			Edges[i].capacity -= dd;	//前向弧流量增加(capacity为可调整流量)
			Edges[i ^ 1].capacity += dd; //反向弧流量减少
			delta -= dd;
			res += dd;
		}
	}
	return res;
}

int dinic(int end)
{
	int ret = 0;
	while (true)
	{
		bfs();
		if (!visit[end])
		{
			return ret;
		}
		ret += dfs(end, 1, 1e8);
	}
	return ret;
}

Dinic算法的复杂度分析:
层次网络中,汇点的层次就代表着源点到汇点的最短距离,在每次构建层次网络时,汇点的层次必然会比上一次至少多1,因为在每次的dfs中,所有的最短路径都被找了出来,并且经过流量调整后当前所有的最短路径均被阻塞,因此在下一次构建层次网络时,没有办法再找到这么短的路径了,也就是说源点到汇点的最短距离至少增加1,也就是汇点的层次至少加1。而层次的上界显然为顶点的个数,因此最外层的while循环至多遍历 O ( V ) O(V) O(V)次。内层的bfs需要 O ( E ) O(E) O(E)的时间,dfs为找所有的增广路,由对EK算法的分析不难得出,找到所有的增广路不会超过 O ( V E ) O(VE) O(VE)的时间,因此Dinic算法的复杂度上界为 O ( V 2 E ) O(V^2E) O(V2E),这个性能要优于EK算法,而且Dinic算法的实现也很简便,我认为是解题的首选算法。

Sap算法
Sap算法是对Dinic算法一个小的优化,在Dinic算法中,每次都要进行一次bfs来更新层次网络,这未免有些过于浪费,因为有些点的层次实际上是不需要更新的。Sap算法就采取一边找增广路,一边更新层次网络的策略。直接看代码实现吧:

int gap[maxN]; //层次网络中某一层包含节点的个数
int map[maxN][maxN];//邻接矩阵
int level[maxN]; //层次
int pre[maxN]; //增广路中节点的前一个节点

//m为节点总个数
int sap(int m)
{
	//一开始所有节点的层次设为0
	int result = 0;
	gap[0] = m;
	pre[1] = 1;
	int u = 1 , v;
	while (level[1] < m)
	{
		//找可行弧
		for (v = 1; v <= m; v++)
		{
			if (map[u][v] && level[u] == level[v] + 1) break;
		}
		//找到了可行弧
		if (v <= m)
		{
			pre[v] = u;
			u = v;
			//找到了一条增广路,做流量调整
			if (v == m)
			{
				int min = 1e8;
				for (int i = v; i != 1; i = pre[i])
					if (min > map[pre[i]][i])
						min = map[pre[i]][i];
				result += min;
				for (int i = v; i != 1; i = pre[i])
				{
					map[pre[i]][i] -= min;
					map[i][pre[i]] += min;
				}
				u = 1;
			}
		}
		else {
			//未找到可行弧,调节层次网络,将当前节点的层次设为周围所有节点层次最小值+1,
			//以确保下一次能找到可行弧
			int minlevel = 1e5;
			for (int i = 1; i <= m; i++)
				if (map[u][i] && minlevel > level[i])
					minlevel = level[i];
			//gap优化 如果当前这个节点的层次中只包含这个节点,在这个节点的层次做调整后,
			//当前网络就不再包含具有这个层次的节点了,这个时候是一定没办法找到可行流的,
			//因此算法可以终止了。
			gap[level[u]]--;
			if (gap[level[u]] == 0) break;
			level[u] = minlevel + 1;
			gap[minlevel + 1]++;
			u = pre[u];
		}
	}
	return result;
}

注意在Sap算法中源点的层次应该是最高的,一定要有Gap优化,不然这个算法的性能就不尽如人意了。Sap算法的复杂度上界和Dinic一样也是 O ( V 2 E ) O(V^2E) O(V2E)

HLPP算法
HLPP算法即最高标号预流推进算法,与前面三种算法不同的是,它并不采取找增广路的思想,而是不断地在可行流中找到那些仍旧有盈余的节点,将其盈余的流量推到周围可接纳流量的节点中,具体什么意思呢?对于一个最大流而言,除了源点和汇点以外所有的其他节点都应该满足流入的总流量等于流出的总流量,如果首先让源点的流量都尽可能都流到其相邻的节点中,这个时候相邻的节点就有了盈余,即它流入的流量比流出的流量多,所以要想办法将这些流量流出去。这种想法其实很自然,如果不知道最大流求解的任何一种算法,要手算最大流的时候,采取的策略肯定会是这样,将能流的先流出去,遇到容量不足的边就将流量减少,直到所有流量都流到了汇点。
但是这样做肯定会遇到一个问题,会不会有流量从一个节点流出去然后又流回到这个节点?如果这个节点是源点的话这么做是没问题的,因为有的时候通过某些节点是到达不了汇点的,这个时候要将流量流回到源点,但是其他情况就可能会造成循环流动,因此需要用到层次网络,只在相邻层次间流动。
还是先来看具体的实现代码吧:

struct Vert
{
	int ef;//盈余
	int x;//节点编号
	int dist;//节点层次
	Vert() {}; 
	inline bool operator < (const Vert & v)const
	{
		return this->dist < v.dist;
	}
}Verts[maxN];

//出现断层时,将所有高于该层的节点的层次都设为 源点层次+1,好让它们流回到源点
//因为由于断层这些节点再也没办法将流量流到汇点了
void Ga(int m , int d)
{
	for (int i = 1; i <= m; i++)
		if (i != 1 && i != m && Verts[i].dist > d && Verts[i].dist <= m) Verts[i].dist = m + 1;
}

//推进流量 如果推进了0流量 就返回false 表示实际上并不可以推进流量
bool pushFlow(int from ,int to , int edge)
{
	int w = min(Verts[from].ef, Edges[edge].capacity);
	Edges[edge].capacity -= w;
	Edges[edge ^ 1].capacity += w;
	Verts[from].ef -= w;
	Verts[to].ef += w;
	return w;
}

int preMaxFlow(int t)
{
	memset(Verts, 0, sizeof(Verts));
	for (int i = 1; i <= t; i++)Verts[i].x = i;
	priority_queue<Vert> vertQue;
	Verts[1].dist = t;
	Verts[1].x = 1;
	Verts[1].ef = 1e8;
	vertQue.push(Verts[1]);
	Gap[Verts[1].dist] = 1;
	Gap[0] = t - 1;
	while (!vertQue.empty())
	{
		Vert topV = vertQue.top();
		vertQue.pop();
		if (!Verts[topV.x].ef) continue;
		for (int i = head[topV.x] ; i != -1 ; i = Edges[i].next)
		{
			int v = Edges[i].to;
			//源点直接推流量出去  其余情况只在相邻层推流量
			if ( (topV.x == 1 || (Verts[topV.x].dist == Verts[v].dist+1) )  && 
			pushFlow(topV.x,v,i) &&  v != 1  && v!=t)
			{
				vertQue.push(Verts[v]);
			}
		}
		//仍有流量但没推出去,就将当前节点的层次抬高,以让其尽可能流出
		if (Verts[topV.x].ef && topV.x != 1 && topV.x != t)
		{
			if (!--Gap[Verts[topV.x].dist]) Ga(t, Verts[topV.x].dist);
			++Gap[++Verts[topV.x].dist];
			vertQue.push(Verts[topV.x]);
		}
	}
	return Verts[t].ef;
}

注意所有的推进都是从高层次节点推到低层次节点中,源点的层次始终为节点总数。
可以看到这里采用了一个优先级队列,每次都优先取出层次较高的节点做推进,因为层次较低的节点是有可能接受到层次高节点流出的流量的,如果先推层次低的节点的流量,之后它有可能又接受到了高层次节点的流量,那么又要对其作推进处理,而如果每次都先将高层次节点取出,就可以将所有的高层次的节点的流量都先推入对应的低层次的节点中,在低层次的节点中先累积流量,最后再一起推进,提升效率。
HLPP算法的复杂度为 O ( V 2 E ) O(V^2\sqrt{E}) O(V2E ),证明较为繁琐,可见wiki相关资料。

最大流模型解题入门

将题目描述的问题转化为最大流模型是一个非常有意思的过程,而有些转换确实很难想到,这里对做过的几个题进行一下整理。
在建模的时候,要考虑四个问题:
1.源点是什么,题目中什么可以作为源点,什么可以与源点相连?
2.汇点是什么,题目中什么可以作为汇点,什么可以与汇点相连?
(大多数情况下源点和汇点都需要自己建立)
3.源点汇点以外的节点该有哪些,它们该怎样连接?
4.最大流的实际意义是什么?

要求的问题与最大流紧密相关
POJ 1087
在这里插入图片描述
题目中有三类东西:插头、设备、转换器,很显然这三类东西都不止一种,因此它们都没法作为源点或者汇点,源点与汇点要自己建立。
那么什么与源点相连?插头是已经给出的,数量、种类已知,因此它们插头要与源点相连,每种插头与源点相连的容量为该种插头的个数。什么与汇点相连?最后要求的问题与设备有关,因此让设备与汇点相连,每个设备与汇点之间的容量为1。转换器可以让插头转换为另一种插头,因此转换器就与两个插头连接,容量为转换器的数量。设备要插在插头上,因此要让设备与对应的插头相连,容量为1。
在这样的一个图上做最大流,得到的是什么?不难想到应该是最多能够插入对应插座的设备总数,那么就很容易求得最少不能插入对应插座的设备总数了。
AC代码:(DINIC算法)

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <algorithm>
#include <iostream>	
#include <queue> 
#include <set>
#include <vector>
#include <map>
using namespace std;

const int maxN = 315;
const int INF = 1e8;
bool visit[maxN];
int dist[maxN];
int head[maxN];
map<string, int> hashTable;
int currentEdge;

struct edge {
	int to;
	int capacity;
	int next;
}Edges[maxN*maxN];

void AddEdge(int from , int to , int capacity )
{
	edge e;

	e.to = to;
	e.capacity = capacity;
	e.next = head[from];
	Edges[currentEdge] = e;
	head[from] = currentEdge;
	currentEdge++;

	e.to = from;
	e.capacity = 0;
	e.next = head[to];
	Edges[currentEdge] = e;
	head[to] = currentEdge;
	currentEdge++;

}

void bfs()
{
	memset(visit, false, sizeof(visit));
	memset(dist, 0, sizeof(dist));
	queue<int> vertQue;
	vertQue.push(1);
	dist[1] = 0;
	visit[1] = true;
	while (!vertQue.empty())
	{
		int vTop = vertQue.front();
		vertQue.pop();
		for (int i = head[vTop] ; i != -1 ; i = Edges[i].next)
		{
			if (Edges[i].capacity && !visit[Edges[i].to])
			{
				dist[Edges[i].to] = dist[vTop] + 1;
				visit[Edges[i].to] = true;
				vertQue.push(Edges[i].to);
			}
		}
	}
}

int dfs(int end , int u , int delta)
{
	if (u == end) return delta;
	int res = 0;
	for (int i = head[u] ; i != -1  ; i = Edges[i].next)
	{
		if (dist[Edges[i].to] == dist[u] + 1)
		{
			int dd = dfs(end, Edges[i].to, min(Edges[i].capacity, delta));
			Edges[i].capacity -= dd;
			Edges[i ^ 1].capacity += dd;
			delta -= dd;
			res += dd;
		}
	}
	return res;
}

int dinic(int end)
{
	int ret = 0;
	while (true)
	{
		bfs();
		if (!visit[end])
		{
			return ret;
		}
		ret += dfs(end, 1, 1e8);
	}
	return ret;
}

int main()
{
	int n,m,k;
	int start = 1, end;
	cin >> n;
	std::string str;

	memset(head, -1, sizeof(head));

	for (int i = 1 ; i <= n ;  i ++ )
	{
		cin >> str;
		if (hashTable[str])
		{
			Edges[(hashTable[str] - 2) * 2].capacity++;
		}
		else {
			hashTable[str] = i + 1;
			AddEdge(1, i + 1, 1);
		}
	}
	cin >> m;
	end = 2 + m + n;
	int tmp = end;
	for (int i = 1 ; i <= m ; i ++ )
	{
		std::string str2;
		cin >> str >> str2;
		if (!hashTable[str2])
		{
			hashTable[str2] = ++tmp;
		}
		AddEdge(n + i + 1, end, 1);
		AddEdge(hashTable[str2], n + i + 1, 1);
	}
	cin >> k;
	while(k--)
	{
		std::string str2;
		cin >> str >> str2;
		AddEdge(hashTable[str2], hashTable[str], INF);
	}
	cout << m - dinic(end) << endl;
	return 0;
}

要求的问题作为网络流建图的限制条件,并利用求得的最大流二分答案
POJ 2391
在这里插入图片描述
本题要求解的是所有奶牛都到避雨点所用的最小时间,首先这是一个最小时间,很难用一个最大流来表示,其次这个时间影响到了奶牛能够到达的避雨点的范围,因此求解的这个问题是会影响到如何建图的。那么对于这样一类问题应该采用二分答案的策略,既然答案会影响到求解的过程,而这个答案又具有明显的单调性,那么就先定一个很大的范围来做二分处理。令l=1,r=INF,不断的取mid作为时间来建图:
建立一个源点,与所有的草地相连,容量为当前每个草地牛的个数,建立一个汇点,与所有的避雨点相连,容量为避雨点所接纳牛的最大个数。如果从草地到避雨点所需要的时间小于给的时间mid,就将草地与避雨点相连,容量为无穷大。最终求得的最大流就是能够到达避雨点的牛的个数,如果这个个数等于牛的总数,就说明当前这个mid是可行的,那么就找有没有更小的mid能够满足,让r=mid-1;如果小于牛的总数,就说明这个时间太小了,没办法让所有的牛都到避雨点,因此要抬高时间,让l=mid+1。
但是需要考虑两个问题:
1.如何知道从某个草地到某个避雨点所需要的最小时间,来与给定时间比较?
可以采用Floyd算法预处理。
2.草地和避雨点一定要区分开吗?
确实根据题意,草地本身就是避雨点,似乎可以让草地直接与草地相连,但是要注意到,如果本题中只设置草地节点,假设在某个时间mid下,草地A的牛能到草地B避雨,草地B的牛能到草地C避雨,但是草地A的牛不能到草地C避雨,现在将A与B连接,B与C连接,那么在做最大流的时候,A的牛就可以通过B到达C,而事实上A是不能到C的,因此草地和避雨点不能合在一起。应该新建一类避雨点和草地节点个数相同,让草地A与避雨点B相连,草地B与避雨点C相连,这样草地A的牛就不能到达C了。
这种方式叫做拆点,在建图的时候一定要考虑到是否要拆点的问题。

AC代码(预流推进法)

#define _CRT_SECURE_NO_WARNINGS

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <algorithm>
#include <iostream>
#include <queue> 
#include <set>
#include <vector>
using namespace std;

const int maxN = 205*2;
const int maxM = 100010;

struct edge
{
	int to;
	int next;
	int capacity;
}Edges[maxM];
int Gap[maxN];

struct Vert
{
	int ef;
	int x;
	int dist;
	Vert() {}; 
	inline bool operator < (const Vert & v)const
	{
		return this->dist < v.dist;
	}
}Verts[maxN];

int head[maxN];
long long map[maxN][maxN];
int vertData[maxN][2];
int currentEdge;

void AddEdge(int from , int to , int capacity)
{
	edge e;
	e.to = to;
	e.capacity = capacity;
	e.next = head[from];
	Edges[currentEdge] = e;
	head[from] = currentEdge++;

	e.to = from;
	e.capacity = 0;
	e.next = head[to];
	Edges[currentEdge] = e;
	head[to] = currentEdge++;

}

void floyd(int n)
{
	for (int k = 1 ; k <= n ; k ++  )
	{
		for (int i = 1 ; i <= n ; i ++ )
		{
			for (int j = 1 ; j <= n ; j ++ )
			{
				map[i][j] = min(map[i][j],map[i][k]+map[k][j]);
			}
		}
	}
}

void Ga(int m , int d)
{
	for (int i = 1; i <= m; i++)
		if (i != 1 && i != m && Verts[i].dist > d && Verts[i].dist <= m) Verts[i].dist = m + 1;
}

void Init(int n , long long time)
{
	currentEdge = 0;
	memset(head, -1, sizeof(head));
	for (int i = 1 ; i <= n ; i ++ )
	{
		AddEdge(1,i+1,vertData[i][0]);
		AddEdge(i+1, i +n+ 1, 1e8);

	}
	for (int i = 1 ; i <= n ;  i ++ )
	{
		for (int j = 1 ; j <= n ; j ++ )
		{
			if (map[i][j] <= time && vertData[j][1] != 0)
			{
				AddEdge(i + 1, j + n + 1,1e8);
			}
		}
	}
	for (int i = 1 ; i <= n ; i ++ )
	{
		AddEdge(1 + i + n, 2 * n + 2 , vertData[i][1]);
	}
}

bool pushFlow(int from ,int to , int edge)
{
	int w = min(Verts[from].ef, Edges[edge].capacity);
	Edges[edge].capacity -= w;
	Edges[edge ^ 1].capacity += w;
	Verts[from].ef -= w;
	Verts[to].ef += w;
	return w;
}

int preMaxFlow(int t)
{
	memset(Verts, 0, sizeof(Verts));
	for (int i = 1; i <= t; i++)Verts[i].x = i;
	priority_queue<Vert> vertQue;
	Verts[1].dist = t;
	Verts[1].x = 1;
	Verts[1].ef = 1e8;
	vertQue.push(Verts[1]);
	Gap[Verts[1].dist] = 1;
	Gap[0] = t - 1;
	while (!vertQue.empty())
	{
		Vert topV = vertQue.top();
		vertQue.pop();
		if (!Verts[topV.x].ef) continue;
		for (int i = head[topV.x] ; i != -1 ; i = Edges[i].next)
		{
			int v = Edges[i].to;
			if ( (topV.x == 1 || (Verts[topV.x].dist == Verts[v].dist+1) )  && pushFlow(topV.x,v,i) &&  v != 1  && v!=t)
			{
				vertQue.push(Verts[v]);
			}
		}
		if (Verts[topV.x].ef && topV.x != 1 && topV.x != t)
		{
			if (!--Gap[Verts[topV.x].dist]) Ga(t, Verts[topV.x].dist);
			++Gap[++Verts[topV.x].dist];
			vertQue.push(Verts[topV.x]);
		}
	}
	return Verts[t].ef;
}

int main()
{
	int totalCow = 0;
	int n, m;
	scanf("%d%d", &n, &m);
	long long maxDist = 1e17;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n ; j ++)
		{
			map[i][j] = maxDist;
		}
	}
	for (int i = 1 ; i <= n ; i ++ )
	{
		int current, capacity;
		scanf("%d%d", &current, &capacity);
		vertData[i][0] = current;
		totalCow += current;
		vertData[i][1] = capacity;
	}
	while (m--)
	{
		int from, to;
		long long time;
		scanf("%d%d%lld", &from, &to,&time);
		if (map[from][to]  >= time)
		{
			map[from][to] = map[to][from] = time;
		}
	}
	floyd(n);
	long long l = 0, r = 1e12;
	long long ans = -1;
	while ( l <= r )
	{
		long long mid = (l + r) >> 1;
		Init(n, mid);
		int result = preMaxFlow(2 * n + 2);
		if (result >= totalCow)
		{
			ans = mid;
			r = mid - 1;
		}
		else {
			l = mid + 1;
		}
	}
	printf("%I64d\n", ans);
	return 0;
}

不容易想到的巧妙转换
在这里插入图片描述
这是一个混合图的欧拉回路问题,在有向图中欧拉回路要求所有点的入度等于出度,在一个混合图中走的话,无向图实际上也是有方向的,它的方向就是走的时候的方向。因此可以先将所有的无向边都假定一个方向,之后再根据需要调整无向边的方向,看是否能调整出具有欧拉回路的有向图出来。统计所有点的入度和出度,计算入度和出度之差:如果这个差是一个奇数,原图一定没有欧拉通路。因为能够进行的操作只能是改变某条无向边的方向,这样就会使入度增加1出度减少1 或者 入度减少1出度增加1,这样是永远不可能让入度与出度相等的。
在不存在奇数的情况下,就可以将问题转化为:在原图中是否能改变某些无向边的方向,来让所有点的入度出度相等?
建立一个网络流,源点与入度<出度的点相连,容量为(出度-入度)/2,表示这个点需要调节这么多条无向边。让入度>出度的点与汇点相连,容量为(入度-出度)/2,让原图中本来的无向边的两个点按照假定无向边的方向连接,有向边忽略(因为有向边无法改变方向了)。跑最大流相当于什么?改变与源点相连的点的出边的方向以减少出度增大入度,改变与汇点相连的点的入边的方向以减少入度增大出度,中间的点由于平衡条件入度出度之差不变,始终为0。这样判定最终这个最大流是不是满流就可以得出是否有欧拉回路的结论了。

AC代码(Dinic算法)


#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <algorithm>
#include <iostream>	
#include <queue> 
#include <set>
#include <vector>
using namespace std;
const int maxN = 505;

int currentEdge;
int head[maxN*maxN*2];
int map[maxN][maxN];
int indegree[maxN];
int adjustEdge[maxN];
int outdegree[maxN];
int totalFlow;
bool visit[maxN];
int dist[maxN];

struct edge
{
	int to;
	int capacity;
	int next;
}Edges[maxN*maxN*2];

void AddEdge(int from , int to ,int capacity)
{
	edge e;
	e.to = to;
	e.capacity = capacity;
	e.next = head[from];
	Edges[currentEdge] = e;
	head[from] = currentEdge++;

	e.to = from;
	e.capacity = 0;
	e.next = head[to];
	Edges[currentEdge] = e;
	head[to] = currentEdge++;
}

void bfs()
{
	memset(visit, false, sizeof(visit));
	memset(dist, 0, sizeof(dist));
	queue<int> vertQue;
	vertQue.push(1);
	dist[1] = 0;
	visit[1] = true;
	while (!vertQue.empty())
	{
		int vTop = vertQue.front();
		vertQue.pop();
		for (int i = head[vTop] ; i != -1 ; i = Edges[i].next)
		{
			if (!visit[Edges[i].to] && Edges[i].capacity)
			{
				dist[Edges[i].to] = dist[vTop] + 1;
				visit[Edges[i].to] = true;
				vertQue.push(Edges[i].to);
			}
		}
	}
}

int dfs(int u , int n  , int delta)
{
	if (u == n) return delta;
	int res = 0;
	for (int i = head[u] ; i != -1 ; i = Edges[i].next)
	{
		int v = Edges[i].to;
		if (dist[v] == dist[u] + 1)
		{
			int dd = dfs(v, n, min(Edges[i].capacity, delta));
			Edges[i].capacity -= dd;
			Edges[i ^ 1].capacity += dd;
			res += dd;
			delta -= dd;
		}
	}
	return res;
}

int Dinic(int n)
{
	int res = 0;
	while (true)
	{
		bfs();
		if (!visit[n])
		{
			return res;
		}
		res += dfs(1, n, 1e8);
	}
	return res;
}

void Init(int n)
{
	totalFlow = 0;
	for (int i = 1 ; i <= n ; i ++)
	{
		if (adjustEdge[i] < 0)
		{
			AddEdge(1, i + 1, -adjustEdge[i] / 2);
		}
		else if(adjustEdge[i] > 0)
		{ 
			AddEdge(i + 1, n + 2, adjustEdge[i] / 2);
			totalFlow += adjustEdge[i] / 2;
		}
		for (int j = 1; j <= n ; j ++ )
		{
			if (map[i][j] == 0 )
			{
				AddEdge(i+1,j+1,1);
			}

		}
	}
}

int main()
{
	int testNum;
	cin >> testNum;
	while (testNum--)
	{
		memset(map, -1, sizeof(map));
		memset(indegree, 0, sizeof(indegree));
		memset(outdegree, 0, sizeof(outdegree));
		memset(head, -1, sizeof(head));
		int n, m;
		cin >> n >> m;
		currentEdge = 0;
		while (m--)
		{
			int from, to, direction;
			cin >> from >> to >> direction;
			indegree[to]++;
			outdegree[from]++;
			map[from][to] = direction;
		}
		bool possible = true;
		for (int i = 1 ; i <=  n ; i ++ )
		{
			int delta = indegree[i] - outdegree[i];
			if (abs(delta) % 2 != 0)
			{
				possible = false;
			}
			adjustEdge[i] = delta;
		}
		if (possible)
		{
			Init(n);
			int maxFlow = Dinic(n + 2);
			if (maxFlow == totalFlow) printf("possible\n");
			else printf("impossible\n");
		}
		else {
			printf("impossible\n");
		}
	}
	return 0;
}

这些就是对最大流算法的一些总结整理,感觉除了建立模型以外最难的是如何识别这是一个网络流问题,可能还要多练练吧。

  • 39
    点赞
  • 155
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值