一、网络流与最大流

网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。在一个有向图中,一个源点S到一个汇点T之间有连边,边的权值是该边的最大容量,网络流就是从S点到T点的一个可行流。
网络流最初的问题就是研究最大流,就是指所有可行流里面最大的流。

举个简单的例子,如下图:
在这里插入图片描述
若v1到v6六个点表示的是六个城市,每条边的权值表示的是城市之间路的长度,选择要运送一批物资从v1到v6,要走的路最短这就是最短路问题
但是如果v1到v6表示的是六个中转站,每条边表示的是水管,权值是水管的最大运输量,现在求从v1运输到v6的最大运输量,这就是最大流问题
上图的最短路是15(v1-v4-v5-v6),最大流是5。


二、网络流三个基本性质

我们先定义C是某条边的最大容量F是某条边的实际流量,即C(u,v)就是<u,v>这条边的最大容量,F(u,v)就是<u,v>这条边的实际流量。

1、第一个很明显的性质是F(u,v) ≤ C(u,v),水管的实际流量肯定不会超过最大容量(否则水管就会爆炸…)。
2、第二个性质是对于任意一个节点,其流入的总量一定等于流出的总量,流量守恒,即对于节点x:Σ F(v,x) = Σ F(x,u)。总不能在某个节点蓄水(爆炸危险…)或平白无故的多水(???)。当然,对于源点和汇点不用满足流量守恒,我们不关心水从哪来到哪去。
3、第三个不是很明显的性质,斜对称性:F(u,v) = -F(v,u)。但这是完善网络流理论不可缺少的。意思是u向v流了f流量的水,则v向u就流了-f流量的水,就好比a给b给了10块钱,就等于b给a给了-10块钱。

在有向图G中,对于任意时刻,G的网络流满足如下三个性质:
①容量限制:对任意(u,v)∈E,F(u,v) ≤ C(u,v)。
②流量守恒:对任意u,若u不为S或T,一定有ΣF(u,x) = 0,(u,x)∈E。即u到相邻所有节点的流量之和为0,因为流入u的流量和u点流出的流量相等,反对称性正负相互抵消了。
③反对称性:对任意u,v∈V,f(u,v) = -f(v,u)。从u到v的流量一定是从v到u的流量的相反值。


三、重要定义定理

首先我们要了解一些定义。
①流量: 一条边上流过的流量。
②容量: 一条边上可供流过的最大流量。
③残量: 容量 - 流量。
④增广路: 从S到T的一条路径,路径上的每条边的残量都为正。
⑤可行流: 每条边的流量任意,但必须满足上面三个基本定理,这样可行的流网络的就称为可行流。
⑥最大流: 最大可行流,顾名思义就是所有可行流中最大的。
⑦残留网络: 残留网络也是个网络,记作f’,其所有点和原网络一样,边是原来的2倍。残留网络包括了原来的所有边,其f’的值是该边的残量(原来就有的正向边),同时残留网络也包含所有边的反向边,f’值是该边正向边的流量。定理:原可行流f+其残留网络f’仍是一个可行流。可以理解残留网络的正向边就是该边还可以加多少流量,反向边就是该边可以减多少流量(一进一退,还可以进的流量就是该边的残量[容量限制],可以退的流量就是该正向边的流量[最多退到0])。
残留网络

我们知道了这些定义后,那么具体怎么求最大流呢?


四、最大流算法

<Ⅰ> Edmonds-Karp算法(EK算法)
1、EK算法

最大流最简单的算法是Edmonds-Karp算法,即最短路径增广算法,简称EK算法。EK算法基于一个基本的方法:Ford-Fulkerson方法,即增广路方法,简称FF方法。增广路算法是很多网络流算法的基础,一般都在残留网络中实现。其思路是不断调整流值和残留网络,直到没有增广路为止。FF方法的依据是增广路定理:网络达到最大流当且仅当残留网络中没有增广路。证明略,不过这个定理也不难理解。

那么如何找到一条增广路呢?很简单,我们把所有残量为正的边都看作可行边,然后跑一遍bfs就找到了一条最短的增广路,因为是bfs,所以这是所有增广路里面最小的。当我们找到这条增广路后,所能增广的流值是路径上最小的残留容量边所决定的。

2、算法思想:

EK的算法思想其实很简单,主要的定理都在前面,下面用伪代码表示一下EK算法的流程,就会很清楚了。

while(在残留网络中能找到一条增广路)
{
	当前可行流加上增广路上所有边中最小的流值
	修改残留网络(正向边减这个最小值,反向边加这个最小值)
}
/*
*知道找不到增广路就退出
*找不到增广路时的可行流就是最大流
*找增广路用bfs走所有流值为正的边,若走到了T说明找到了一条增广路
*建残留网络时用链式前向星建,将同一条边的正反边连续建在一块(这样可以将正向边和反向边的编号建成一对配偶数)
*/

对于正反向边的变换不懂的可以看这(异或运算求配偶数

3、代码模板
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 1010,M = 20010,INF = 0x3f3f3f3f;

int n,m,s,t;
int e[M],ne[M],w[M],h[M],idx;	//链式前向星建图
int pre[N],minw[N];	//pre是存增广路径的,后面要更新这条路上边的流值,minw是存该路径上边的最小值
bool vis[N];	//bfs判断点是否走过

void add(int a,int b,int c)	//建边函数
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()	//bfs搜索增广路
{
	queue<int> q;	//bfs的队列
	memset(vis,0,sizeof vis);	//初始化vis数组
	q.push(s);	//源点s入队
	vis[s] = 1;	//标记s入队
	minw[s] = INF;	//初始化s点的最小流值,视为无穷大
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(!vis[v] && w[i])	//当v点没访问过且该边流值为正时,走到v点
			{
				vis[v] = 1;	//标记v点
				minw[v] = min(minw[u],w[i]);	//更新边的最小流值,取到u点的最小流值和u到v边的w[i],二者的最小值
				pre[v] = i;	//存路径,注意此处存的是v点的前驱边(通过哪条边到达的v点,因为我们后面要修改边)
				if(v == t)	//如果走到了汇点t
					return 1;	//就返回1
				q.push(v);	//v点入队
			}
		}
	}
	return 0;	//如果走完bfs都没走到t点,说明再没有增广路了,返回0
}

int EK()
{
	int ans = 0;	//记录当前可行流的流量,从0开始不断增加
	while(bfs())	//如果可以bfs到
	{
		ans += minw[t];	//当前的可行流加上到汇点t的最小流值
		for(int i = t;i != s;i = e[pre[i]^1])	//更新每条边,注意这里i是从t点往前更新,直到s点,pre[i]是到走i点的那条边的序号,序号^1得到的是他的反向边的序号,再通过此序号访问e数组得到的就是i点的上一个点
		{
			w[pre[i]] -= minw[t];	//pre[i]是到i的正向边,w值减去最小流值
			w[pre[i]^1] += minw[t];	//pre[i]^1是到i的反向边,w值加上流值,如上就更新完了残留网络
		}
	}
	return ans;	//直到上面找不到增广路后退出while循环,此时的可行流就是最大流,返回ans
}

int main()
{
	int a,b,c;
	cin >> n >> m >> s >> t;
	memset(h,-1,sizeof h);	//注意h数组的初始化
	while(m--)
	{
		cin >> a >> b >> c;
		add(a,b,c);	//建残留网络时,正向边的流值是容量c
		add(b,a,0);	//反向边的流值是0
	}
	
	cout << EK() << endl;

	return 0;
}

一次bfs加残留网络的更新大概是E,一个图最多增广VE次,因此最坏情况下EK的时间复杂度是O(VE2)。

4、模板题

acwing2171:EK求最大流

<Ⅱ> Dinic算法
1、算法核心思想

很明显,EK算法要多次找增广路径,最多能找VE次,很麻烦,时间复杂度很高,因此就可以用dinic算法优化一下。dinic算法在增广的时候也是找的最短增广路径,但与EK不同的是dinic不是每次bfs只找一条增广路,他是先通过一次bfs为每个点添加一个编号,构成一个层次图,然后在层次图中找增广路进行更新。所以dinic是一种高效的增广方法,可以在一次增广中找到多条增广路径。

dinic
bfs得到一个分层图后,在这个图上跑一遍dfs,找到当前所有的增广路后,再根据当前的残留网络分层,再找增广路,直到无法分层,说明走不到汇点t了,意味着再没有增广路了,就结束。

limit是流入该点的流量,flow是该点分给其他点的总流量,limit-flow是能给当前点的最大流量,和其边的容量取最小值分给下一个点。dfs找到一条增广路后就会更新残留网络。没理解也没关系,具体可以在下面代码中看,很快就会理解了。

2、代码模板
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 10010,M = 200010,INF = 0x3f3f3f3f;	//N是最大点数,M 是最大边数,INF是无穷大

int e[M],ne[M],w[M],h[M],idx;	//链式前向星建图
int n,m,s,t,dep[N];	//dep是层数标号

void add(int a,int b,int c)	//加边函数
{	
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()	//bfs分层

{
	memset(dep,-1,sizeof dep);	//初始化层数标记数组
	queue<int> q;
	q.push(s);	//源点s入队
	dep[s] = 0;	//s的层数记为0
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dep[v] == -1 && w[i])	//如果v点还没有访问过,并且u到v的边残量为正
			{
				dep[v] = dep[u] + 1;	//v的层数是u的层数加一
				if(v == t)	//如果bfs到终点了
					return 1;	//说明能找到增广路,返回1
				q.push(v);	//v入队
			}
		}
	}
	return 0;	//若找不到,返回0
}

int dfs(int u,int limit)	//dfs,u是当前点,limit是u点可给的最大流量
{
	if(u == t)	//如果dfs到汇点t了,说明找到一条增广路径了
		return limit;	//返回增加的流量limit
	int flow = 0;	//flow是已经给其他边分配了的流量大小
	for(int i = h[u];~i;i = ne[i])
	{
		int v = e[i];
		if(dep[v] == dep[u]+1 && w[i])	//如果v是u的下一层,且u到v的残量为正,走u-v这条路
		{
			int temp = min(limit-flow,w[i]);	//temp是能给v分到的最大流量。就是流入u的流量limit减去u分给其他点的流量flow 和 u到v容量 二者的最小值
			int minf = dfs(v,temp);	//minf是递归返回的流量,就是这条增广路上最大能加的流量大小
			w[i] -= minf;	//更新残留网络,正向边减去这个能增加的最大流量minf
			w[i^1] += minf;	//反向边加上minf
			flow += minf;	//分出去的流量加上minf
			if(flow == limit)	//如果分出去的flow已经等于流入的limit了,就不用再往下dfs了,直接返回flow,因为已经达到最大流量了,没有流量可给后面分了
				return flow;
		}
	}
	return flow;	//最后返回能增加的流量flow
}

int dinic()
{	
	int ans = 0;
	while(bfs())	//当能bfs分层时,等价于能找到增广路
		ans += dfs(s,INF);	//ans加上dfs能增加的流量,ans不断增加,直到没有增广路了,ans就是最大流了
	return ans;
}

int main()
{
	cin >> n >> m >> s >> t;
	memset(h,-1,sizeof h);	//注意初始化h数组
	while(m--)
	{
		int a,b,c;
		cin >> a >> b >> c;
		add(a,b,c);	//建残留网络
		add(b,a,0);
	}

	cout << dinic() << endl;

	return 0;
}

Dinic的时间复杂度是O(V2E)

3、dinic算法的优化

传送门:dinic+当前弧优化

4、模板题

acwing2172:Dinic/ISAP求最大流


五、费用流

1、最小费用最大流

假如我们现在有一个流网络,每条边不仅有流量,还有一个单位费用,流过该条边的费用就等于该边的流量乘以单位费用。现在我们要使流量最大的同时也要费用最小。这就是最小费用最大流问题。
在一个流网络中,最大流只有一个,但“流法”有很多种,每种流法经过的边不同,因此费用也不同。所以需要最短路算法,总费用就是最短增广路乘以总流量

2、主要思想

因为最大流是一样的,所以当我们将每条边的单位费用看作路径长度时,每次增广都找最短路,就可以使总长度 × \times ×最大流量的积最小,就达到了最小费用的意图,因此只需要将Dinic算法中的bfs换成spfa跑最短路即可。(注意不能乱用dijkstra,因为边权值有可能为负的)若要用dij,可以加上势优化
注意因为这里用的是spfa算法,所以原图中不能有负权回路,如果有的话就得用另一个方法——消圈法。

最大流流量总值一定,为使费用最小,我们贪心的去让更多的流量去流路径上总费用最小的那条路,因此就有了spfa算法,将费用看作路径长度找最短路。

3、代码模板
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 50010,M = 100010,INF = 0x3f3f3f3f;

int n,m,s,t,flow,cost;	//flow是总流量,cost是总花费
int e[M],w[M],f[M],ne[M],h[N],idx;	//链式前向星建图
int dis[N],pre[N];	//dis是spfa的到点的距离,pre是EK算法存最短路的路径,pre[i]存的是i点的前驱边的编号
bool vis[N];	//vis数组是判断i点是否在队列中

void add(int a,int b,int c,int d)	//加边函数,c是流量,d是费用
{
	e[idx] = b;
	w[idx] = d;		//w存费用
	f[idx] = c;		//f是流量
	ne[idx] = h[a];
	h[a] = idx++;
	e[idx] = a;
	w[idx] = -d;	//反向边费用是-d,退流退钱
	f[idx] = 0;		//反向边流量为0
	ne[idx] = h[b];
	h[b] = idx++;
}

bool spfa()		//spfa找s到t的最短路
{
	queue<int> q;
	memset(dis,INF,sizeof dis);
	memset(pre,-1,sizeof pre);
	memset(vis,0,sizeof vis);
	q.push(s);
	dis[s] = 0;
	vis[s] = 1;
	while(q.size())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] > dis[u]+w[i] && f[i])	//如果有更短的边并且还可以增广,就更新
			{
				dis[v] = dis[u] + w[i];
				pre[v] = i;		//v点的前驱边是i
				if(!vis[v])		//如果v点不在队列中(重复入队没有用)
				{
					q.push(v);	//就入队
					vis[v] = 1;	//入队标记
				}
			}
		}
	}
	if(pre[t] != -1)	//t有前驱边,说明找到t了,返回1
		return 1;
	else				//如果没找到t,说明没有s到t的增广路了,返回0
		return 0;
}

void EK()		//EK算法一条一条的找增广路
{
	while(spfa())
	{
		int minf = INF;
		for(int i = pre[t];~i;i = pre[e[i^1]])	//从t往前遍历整条路径
			minf = min(minf,f[i]);		//找到限制的最小流量
		for(int i = pre[t];~i;i = pre[e[i^1]])	//更新这条路上的流量
		{
			f[i] -= minf;
			f[i^1] += minf;
			cost += minf * w[i];	//花费加上总费用*增广的流量
		}
		flow += minf;	//流量加上
	}
}

int main()
{
	cin >> n >> m >> s >> t;
	memset(h,-1,sizeof h);
	while(m--)
	{
		int a,b,c,d;
		cin >> a >> b >> c >> d;
		add(a,b,c,d);
	}
	
	EK();
	
	cout << flow << " " << cost << endl;	//输出最大流和最小费用
	
	return 0;
}
4、模板题

acwing2174:费用流

5、最大费用最大流

如果这里是求最大费用最大流的话就直接将所有边的花费取反,然后求最小费用最大流得到的答案再取反即可。这里不能用spfa求最大路来求最大费用,因为很可能出现正环使得spfa出不来或错误。

6、代码板子
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 160,M = 10310,INF = 0x3f3f3f3f;

int n,m,s,t;
int e[M],ne[M],w[M],f[M],h[N],idx;
int pre[N],dis[N];
bool vis[N];

void add(int a,int b,int c,int d)
{
	e[idx] = b;
	w[idx] = d;
	f[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
	e[idx] = a;
	w[idx] = -d;
	f[idx] = 0;
	ne[idx] = h[b];
	h[b] = idx++;
}

bool spfa()
{
	queue<int> q;
	memset(dis,INF,sizeof dis);
	memset(pre,-1,sizeof pre);
	memset(vis,0,sizeof vis);
	q.push(s);
	dis[s] = 0;
	vis[s] = 1;
	while(q.size())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] > dis[u]+w[i] && f[i])
			{
				dis[v] = dis[u] + w[i];
				pre[v] = i;
				if(!vis[v])
				{
					q.push(v);
					vis[v] = 1;
				}
			}
		}
	}
	if(pre[t] != -1)
		return 1;
	else
		return 0;
}

int EK()
{
	int cost = 0;
	while(spfa())
	{
		int minf = INF;
		for(int i = pre[t];~i;i = pre[e[i^1]])
		minf = min(minf,f[i]);
		for(int i = pre[t];~i;i = pre[e[i^1]])
		{
			f[i] -= minf;
			f[i^1] += minf;
			cost += minf * w[i];
		}
	}
	return cost;
}

int main()
{
	cin >> m >> n;
	s = 0,t = m+n+1;
	memset(h,-1,sizeof h);
	for(int i = 1;i <= m;i++)
	{
		int a;
		cin >> a;
		add(s,i,a,0);
	}
	for(int i = 1;i <= n;i++)
	{
		int a;
		cin >> a;
		add(m+i,t,a,0);
	}
	for(int i = 1;i <= m;i++)
	{
		for(int j = 1;j <= n;j++)
		{
			int a;
			cin >> a;
			add(i,m+j,INF,a);
		}
	}
	 
	cout << EK() << endl;
 
	for(int i = 0;i < idx;i += 2)
	{
		f[i] += f[i^1];
		f[i^1] = 0;
		w[i] = -w[i];
		w[i^1] = -w[i^1];
	}
	cout << -EK() << endl;
 
	return 0;
}

六、多源汇最大流

1、定义

顾名思义,就是有多个源点和汇点的网络里求整张网络的最大流。

2、算法思路

思路其实不难想,就是建一个超级源点S和超级汇点T,给超级源点S到所有源点s建一条容量为正无穷的边,给所有汇点t到超级汇点T建一条容量为正无穷的边。

这样我们就从原图G构建了一个新图G’,我们可以发现,G中的每一个可行流都和G’中的一个可行流相互对应,这样就将原图G的最大流转换到了新图G’的最大流,我们在新图中求一个S到T的最大流就是原图整个网络的最大流。

3、代码模板
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 10010,M = (100010+N)*2,INF = 0x3f3f3f3f;

int n,m,s,t,S,T,dis[N],cur[N];
int e[M],w[M],ne[M],h[M],idx;

void add(int a,int b,int c)	//链式前向星建图
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()	//dinic板子
{
	queue<int> q;
	memset(dis,-1,sizeof dis);
	q.push(S);
	dis[S] = 0;
	cur[S] = h[S];
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] == -1 && w[i])
			{
				dis[v] = dis[u] + 1;
				cur[v] = h[v];
				if(v == T)
					return 1;
				q.push(v);
			}
		}
	}
	return 0;
}

int dfs(int u,int limit)
{
	if(u == T)
		return limit;
	int flow = 0;
	for(int i = cur[u];~i;i = ne[i])
	{	
		cur[u] = i;
		int v = e[i];
		if(dis[v] == dis[u]+1 && w[i])
		{
			int minf = dfs(v,min(w[i],limit-flow));
			w[i] -= minf;
			w[i^1] += minf;
			flow += minf;
			if(flow == limit)
				return flow;
		}
	}
	return flow;
}

int dinic()
{
	int ans = 0;
	while(bfs())
		ans += dfs(S,INF);
	return ans;
}

int main()
{
	cin >> n >> m >> s >> t;	//s个源点,t和汇点
	memset(h,-1,sizeof h);
	S = 0,T = n+1;	//自己建超级源点S和超级汇点T
	while(s--)
	{
		int c;
		cin >> c;	//输入源点
		add(S,c,INF);	//从S到源点建一条容量为正无穷的边
		add(c,S,0);	//建反向边
	}
	while(t--)
	{
		int c;
		cin >> c;	//输入汇点
		add(c,T,INF);	//从汇点到T建一条容量为正无穷的边
		add(T,c,0);	//建反向边
	}
	while(m--)
	{
		int a,b,c;
		cin >> a >> b >> c;	//输入m条边
		add(a,b,c);
		add(b,a,0);
	}

	cout << dinic() << endl;	//从S到T跑一遍dinic

	return 0;
}
4、模板题

acwing2234:多源汇最大流


七、无源汇上下界可行流

1、定义

顾名思义,外面可以将这个名词拆成三个词:
①无源汇:无源汇指没有源点和汇点(自己建一个就行)。
②上下界:上下界指每条边的流量有一个上界同时也有一个下界,流量不能超过上界值也不能小于下界值。
③可行流:问是否有可行流方案。

2、算法思路

这个流网络很特殊,每条边多了一个下界,而我们只知道普通的流网络做法,因此我们需要将这个特殊的流网络转换成普通的流网络。我们看这个增加的特殊条件:Cl(u,v) ≤ f(u,v) ≤ Cu(u,v),每条边的流量f要大于等于下界Cl(u,v),同时要小于等于上界Cu(u,v)。那么普通的流网络都是0 ≤ f(u,v) ≤ C(u,v),很明显,我们可以给上面不等式的每一项都减去下界Cl(u,v),就变成了普通的流网络:0 ≤ f(u,v)-Cl(u,v) ≤ Cu(u,v)-Cl(u,v)。我们将f(u,v)-Cl(u,v)看作新网络G’的流量,将Cu(u,v)-Cl(u,v)看作新网络G’的容量。就变成了一个普通的流网络了。

那么我们还需要证一下这个新网络G’满不满足作为一个网络的定理:容量限制流量守恒呢。首先根据上面那个不等式很容易看出这个网络满足容量限制,但流量守恒却不一定成立。举个例子:
在这里插入图片描述
首先原网络G是一定满足流量守恒的,但新网络的流量发生了变化,新的F’v入就不一定等于F’v出了,所以在这里我们要判断F’v入和F’v出的大小了,如果F’v出 > F’v入,我们就要建一条源点s到v点的边,边容量是F’v出 - F’v入;如果F’v出 < F’v入,我们就建一条v到汇点t的边,容量是F’v入 - F’v出

由此我们的新网络G就建好了,现在要解决的就是求原图G的可行流。由上面建图过程可知,我们建的源点s到点v的边必须流满才能满足流量守恒,因为这样流量相加才有F’v入 = F’v出。因此我们只需要判断s出来的所有边是否都流满了(判断最大流是否等于s所有出边的容量和),就可以知道原网络是否有可行流了。我们就实现了将求原网络的可行流转化成了求新网络的最大流

3、代码模板
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 210,M = (10200+210)*2,INF = 0x3f3f3f3f;

int n,m,s,t,dis[N],cur[M],A[N];		//dis是存层数的数组,cur是当前弧优化,A数组是存每个点少了多少流量(用来控制流量守恒)
int e[M],w[M],l[M],ne[M],h[M],idx;	//链式前向星建图

void add(int a,int b,int c,int d)	//a点到b点,下界是c,上界是d
{
	e[idx] = b;
	w[idx] = d-c;	//ab边的容量是上界减下界
	l[idx] = c;	//ab边下界存在l数组里
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()	//dinic板子
{
	queue<int> q;
	memset(dis,-1,sizeof dis);
	q.push(s);
	dis[s] = 0;
	cur[s] = h[s];
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] == -1 && w[i])
			{
				dis[v] = dis[u] + 1;
				cur[v] = h[v];
				if(v == t)
					return 1;
				q.push(v);
			}
		}
	}
	return 0;
}

int dfs(int u,int limit)
{
	if(u == t)
		return limit;
	int flow = 0;
	for(int i = cur[u];~i;i = ne[i])
	{
		cur[u] = i;
		int v = e[i];
		if(dis[v] == dis[u]+1 && w[i])
		{
			int minf = dfs(v,min(w[i],limit-flow));
			w[i] -= minf;
			w[i^1] += minf;
			flow += minf;
			if(flow == limit)
				return flow;
		}
	}
	return flow;
}

int dinic()
{
	int ans = 0;
	while(bfs())
		ans += dfs(s,INF);
	return ans;
}

int main()
{
	cin >> n >> m;
	memset(h,-1,sizeof h);
	s = 0,t = n+1;		//自己建s和t
	for(int i = 1;i <= m;i++)
	{
		int a,b,c,d;
		cin >> a >> b >> c >> d;
		add(a,b,c,d);		//正向边的流量初始是上界减下界(d-c)
		add(b,a,0,0);		//反向边的流量初始是0(建残留网络)
		A[a] -= c;		//为使流量守恒,A数组计算每个点入边总共减了多少流量,出边总共减了多少流量,来判断和s连还是和t连,这里A是f出-f入的值。如果没导出来可以自己推一下
		A[b] += c;
	}
	int tot = 0;		//tot累加s出边的总容量,用来判断和最大流是否相等,从而判断原图有没有可行流
	for(int i = 1;i <= n;i++)
	{
		if(A[i] > 0)	//如果A[i]大于0说明f出-f入为正,说明流入的小于流出的,就要将i点和s建边来补充流量
		
		{
			add(s,i,0,A[i]);	//从s到i建一条容量为A[i]的边
			add(i,s,0,0);		//反向边
			tot += A[i];
		}
		else if(A[i] < 0)		//如果A[i]小于0说明流入的大于流出的就要就要将i点和t点建边来分担流量
		{
			add(i,t,0,-A[i]);	//正向边的容量就是差的流量的绝对值,即A的相反数
			add(t,i,0,0);
		}
	}
	if(dinic() != tot)	//如果最大流和s出边的容量和不相等	
		cout << "NO" << endl;	//说明原图不存在可行流
	else
	{
		cout << "YES" << endl;
		for(int i = 0;i < m*2;i += 2)	//循环前m条边,因为是正反建两条,所以只循环i是偶数的情况
			cout << w[i^1] + l[i] << endl;	//可行流的流量是残留网络的反向边的大小加上减去的下界l,注意这是残留网络,反向边的流量大小是原图的流量
	}

	return 0;
}
4、模板题

acwing2188:无源汇上下界可行流


八、有源汇上下界可行流

有源汇上下界可行流和上面无源汇上下界可行流做法类似,只有细微的不同。有源汇的上下界可行流分为求有源汇上下界最大流和有源汇上下界最小流。

<1>.有源汇上下界最大流
1、定义

同理,顾名思义,我们可以将这个名词拆成三个词语:
①:有源汇:就是给定了源点和汇点
②:上下界:每条边的流量限制有上界和下界,流量不能小于下界,也不难大于上界
③:最大流:求给定源点到汇点之间的最大流

2、算法思路

这个思路和上面的无源汇很像,就多了一个源点汇点。我们还是要将这个有上下界的特殊图转换成一个普通图,即将每条边的流量都减去他的下界,但在这个图中源点和汇点并不满足流量守恒,容量限制当然在各边依旧是满足的。所以为了解决源汇点流量守恒的问题,我们就建一条从汇点t到源点s容量为正无穷的边,这样从s到t流了多少流量,t就可以给s流多少,就满足了流量守恒,就可以加一个超级源点S超级汇点T(得到新图G’)跑dinic了,就和上面无源汇一样。当我们在新图G’上找到最大流后,判断S的出边是不是都满流,若满流说明原图存在可行流,我们再找s到t的最大流;若不满流。说明原图就不存在可行流,当然也不存在最大流。

若S出边都满流,我们就在这个满流的残留网络f’里求s点到t点的最大流,删除自己建的t到s的边后再用dinic跑一下s到t点的最大流,将该最大流量加上新图G’中s到t的流量就是原图s到t的最大流了。(在G’上s到t的流量就是建的那条t到s的边的反向边的流量,因为流量守恒,s到t流了多少t到s就流了多少,再因为残留网络中,反向边就是该边的流量)注意一定要删除自己建的t到s的边,不然跑出来就是错的。

做法就是这么简单,但如何证明呢?简单证一下(详细证明可以后面讨论),不想理解的可以跳过,直接按上面做法套板子就可以。
本来求s到t的最大流必须在s到t的可行流的残留网络里dinic,但我们是在新图G’中求得s到t的最大流,再加上G’中s到t的流量得到了原图s到t的最大流…比较玄学…其实是因为这个图比较特殊----所有S的出边和T的入边都是满流的。因此任取原图G的两个可行流f1和f2对应了两个新图G’中的两个满流f1’和f2’,我们可以将f1’减去f2’得到一个s和t的可行流f0’(S和T的边都是满流,所以一减就没了,就剩下内部点和s、t构成的网络了,且这个流网络一定合法----满足流量守恒和容量限制),即f0’ = f1’ - f2’。因为是任取,所以是通解。原图的一个可行流f1对应的满流f1’ = f0’ + f2’。因此我们将G’中s到t的流量加上s到t的最大流就是原图中s到t的最大流。

3、代码模板
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 210,M = (10000+N)*2,INF = 0x3f3f3f3f;

int n,m,s,t,S,T,dis[N],cur[N],A[N];	//和上面无源汇上下界可行流一样
int e[M],w[M],ne[M],h[N],idx;

void add(int a,int b,int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()	//dinic板子
{
	queue<int> q;
	memset(dis,-1,sizeof dis);
	q.push(S);
	dis[S] = 0;
	cur[S] = h[S];
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] == -1 && w[i])
			{	
				dis[v] = dis[u] + 1;
				cur[v] = h[v];
				if(v == T)
					return 1;
				q.push(v);
			}
		}
	}
	return 0;
}

int dfs(int u,int limit)
{
	if(u == T)
		return limit;
	int flow = 0;
	for(int i = cur[u];~i;i = ne[i])
	{
		cur[u] = i;
		int v = e[i];
		if(dis[v] == dis[u]+1 && w[i])
		{
			int minf = dfs(v,min(w[i],limit-flow));
			w[i] -= minf;
			w[i^1] += minf;
			flow += minf;
			if(flow == limit)
				return flow;
		}
	}
	return flow;
}

int dinic()
{
	int ans = 0;
	while(bfs())
		ans += dfs(S,INF);
	return ans;
}

int main()
{
	cin >> n >> m >> s >> t;
	memset(h,-1,sizeof h);
	S = 0,T = n+1;		//自己建一个超级源点S和超级汇点T
	while(m--)
	{
		int a,b,c,d;
		cin >> a >> b >> c >> d;
		add(a,b,d-c);	//和无源汇一样
		add(b,a,0);
		A[a] -= c;
		A[b] += c;
	}
	int tot = 0;
	for(int i = 1;i <= n;i++)
	{
		if(A[i] > 0)
		{
			add(S,i,A[i]);
			add(i,S,0);
			tot += A[i];
		}
		else if(A[i] < 0)
		{
			add(i,T,-A[i]);
			add(T,i,0);
		}
	}
	add(t,s,INF),add(s,t,0);	//加一条t到s容量为无穷大的边,注意反向边也得加
	if(dinic() < tot)	//如果最大流不是满流,就说明原图不存在可行流
		cout << "No Solution" << endl;
	else
	{
		int ans = w[idx-1];	//记录下新图G'中s到t的流量大小
		w[idx-1] = w[idx-2] = 0;	//删去自己建的t到s的边
		S = s,T = t;		//更新源汇点,从s到t跑一遍dinic
		ans += dinic();		//ans加上最大流就是原图的s到t的最大流
		cout << ans << endl;
	}
	
	return 0;
}

模板题:acwing2189

<2>有源汇上下界最小流
1、定义

这里和上面一样,但是是求的s到t的最小流。

2、思路

在上面的基础上,我们求得式子f1’ = f0’ + f2’后,为使f1’最大,我们使f0’最大,即在G’中求s到t的最大流,加上f2’就是原图的最大流,但是现在我们要使f1’最小,因此我们就要使f0’最小。就是求s到t的最小流,如何加上f2’就是原图的最小流。

根据定理,我们可以知道s到t的可行流就等于负的t到s的可行流,因此s到t的最小流,就等于t到s的最大流取相反数。所以我们在这求个t到s的最大流,取个相反数,再加上f2’就是原图G中s到t的最小流。

3、代码模板
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 50010,M = (125010+N)*2,INF = 0x3f3f3f3f;

int n,m,s,t,S,T,dis[N],cur[N],A[N];
int e[M],w[M],ne[M],h[N],idx;


void add(int a,int b,int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()
{
	queue<int> q;
	memset(dis,-1,sizeof dis);
	q.push(S);
	dis[S] = 0;
	cur[S] = h[S];
	while(q.size())
	{
		int u = q.front();
		q.pop();
		for(int i = h[u];~i;i = ne[i])
		{
			int v = e[i];
			if(dis[v] == -1 && w[i])
			{
				dis[v] = dis[u] + 1;
				cur[v] = h[v];
				if(v == T)
					return 1;
				q.push(v);
			}
		}
	}
	return 0;
}

int dfs(int u,int limit)
{
	if(u == T)
		return limit;
	int flow = 0;
	for(int i = cur[u];~i;i = ne[i])
	{
		cur[u] = i;
		int v = e[i];
		if(dis[v] == dis[u]+1 && w[i])
		{
			int minf = dfs(v,min(w[i],limit-flow));
			w[i] -= minf;
			w[i^1] += minf;
			flow += minf;
			if(flow == limit)
				return flow;
		}
	}
	return flow;
}

int dinic()
{
	int ans = 0;
	while(bfs())
		ans += dfs(S,INF);
	return ans;
}

int main()
{
	cin >> n >> m >> s >> t;
	memset(h,-1,sizeof h);
	S = 0,T = n+1;
	while(m--)
	{
		int a,b,c,d;
		cin >> a >> b >> c >> d;
		add(a,b,d-c);
		add(b,a,0);
		A[a] -= c;
		A[b] += c;
	}
	int tot = 0;
	for(int i = 1;i <= n;i++)
	{
		if(A[i] > 0)
		{
			add(S,i,A[i]);
			add(i,S,0);
			tot += A[i];
		}
		if(A[i] < 0)
		{
			add(i,T,-A[i]);
			add(T,i,0);
		}
	}
	add(t,s,INF),add(s,t,0);
	if(dinic() < tot)
		cout << "No Solution" << endl;
	else
	{
		int ans = w[idx-1];
		S = t,T = s;	//和有源汇上下界最大流的唯一区别,这里从t到s跑dinic
		w[idx-1] = w[idx-2] = 0;
		ans -= dinic();		//最后ans+最大流的相反数就等于ans减去这个值
		cout << ans << endl;
	}

	return 0;
}
4、模板题

acwing2190:有源汇上下界最小流


做题时,最好找到流对应的是什么实际的东西,把这个东西当作一个流来想,更好的去理解网络流。


九、最大权闭合子图

传送门:最大权闭合子图问题

十、最大密度子图

传送门:最大密度子图问题

十一、最小权点覆盖集 与 最大权独立集

传送门:最小权点覆盖集 & 最大权独立集

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐