图匹配 | 最大流 | Ford-Fulkerson

注:本文为 “图的匹配 | 最大流 | Ford-Fulkerson” 相关文章合辑。

如有内容异常请看原文。


图的匹配与最大流 (一)

谢潇雨 于 2013-07-08 21:06:16 发布

关于图的匹配,最大流,线性规划等这些图论中的重要而且有着千丝万缕连续的问题,顺便介绍求图的最大匹配问题的著名的匈牙利算法。算是对前段时间学习的一个小结吧。Ps:本人自认很水,多多见谅。

首先这次就先从基本的概念介绍起,顺便说说这之间的一些联系,接下来再开始一一上算法。

一、图的匹配问题

我们日常中听过最多的图的匹配问题可能就是著名的结婚问题:在一个集合中,有 m m m 个女生, s s s 个男生,其中 1 ≤ r ≤ s 1 \leq r \leq s 1rs,设集合 S 1 , S 2 , … , S i , … , S m S_1, S_2, \ldots, S_i, \ldots, S_m S1,S2,,Si,,Sm 分别代表第 i i i 个女生喜欢的男生的集合(当然一个女生可以喜欢多个男生),问有没有可能最终令这 m m m 个女生都能够和自己心仪的男生结婚?

这个问题我们最直观的想法是,首先必须保证,任意 r r r ( ≤ m \leq m m) 个女生喜欢的男生数量必须大于等于 r r r。这是很直观的,比如 3 个女生如果同时只喜欢上 2 个男生,则无论如何也无法分配。

而正是这一直观的想法,解决了这一问题。从上面可知,最终能够令 m m m 个女生都能和自己心仪的男生结婚的必要条件就是任意 r r r 个女生喜欢的男生的数量必须大于等于 r r r。而通过证明,我们知道这一条件也是充分的。这一证明并不是本文的重点,所以这里略过,有兴趣,可以继续讨论。

这一问题就是图的最大匹配问题。不过更一般的我们常说的图的最大匹配问题是这样描述的:对于一个二部图,其最大匹配是多少?如图,所示,最大匹配为 4。

img

二、最大流问题

最大流问题是一个经典问题,许多人对其很熟悉,它能够等同于一个线性规划问题。下面给出最大流问题的基本描述:

如下图所示, s s s 是源点, t t t 为汇点,每条边上数字的含义是边能够允许流过的最大流量。可以将边看成管道, 0 / 3 0/3 0/3 代表该管道每秒最多能通过 3 3 3个单位的流量, 0 0 0 代表当前流量。最大流问题即是说,从 s s s 点到 t t t 点,最大允许流量是多少?

img

那它跟线性规划又是如何产生联系的呢?这里我们先简要介绍下。

c u v c_{uv} cuv 代表边 u u u v v v 最大允许流量(Capacity), f u v f_{uv} fuv 代表 u u u v v v​ 当前流量,那么有以下两个结论:

  1. ( u , v ) ∈ E (u,v)\in E (u,v)E,则 f u v ⩽ c u v f_{uv}\leqslant c_{uv} fuvcuv,对于所有的边,当前流量不允许超过其 Capacity;
  2. 除了 s s s t t t 之外, ∑ ( w , u ) ∈ E f w u = ∑ ( u , v ) ∈ E f u v \sum_{(w,u)\in E}f_{wu}=\sum_{(u,v)\in E}f_{uv} (w,u)Efwu=(u,v)Efuv,即代表,对于任何一个节点,流入该点。

的流量等于流出该点的流量。这个可以理解为流量守恒,就如同电流一样,流入一个电阻的电流必然等于流出的电流。

那么最大流就可以表示为:

max ⁡ ∑ u : ( s , u ) ∈ E f s u \Large \max \sum\limits_{u:(s,u)\in E}{\mathop{f}_{su}} maxu:(s,u)Efsu

s.t. \Large \text{s.t.} s.t. 0 ≤ f u v ≤ c u v , ∀ ( u , v ) ∈ E \Large 0\leq f_{uv}\leq c_{uv},\quad\forall(u,v)\in E 0fuvcuv,(u,v)E

∑ w : ( w , u ) ∈ E f w u − ∑ v : ( u , v ) ∈ E f u v = 0 , ∀ u ∈ V \ { s , t } \Large \sum\limits_{w:(w,u)\in E}f_{wu}-\sum\limits_{v:(u,v)\in E}f_{uv}=0,\quad\forall u\in V\backslash\{s,t\} w:(w,u)Efwuv:(u,v)Efuv=0,uV\{s,t}

三、联系

1. 最大流等于最大二分匹配

如图所示,对于一个二分图,令已有的边的容量(Capacity)为无穷大,增加一个源点 s s s 和一个汇点 t t t,令 s s s t t t 分别连接二部图中的一个分步,并设置其容量为 1 1 1 。这时,计算得到的最大流就等于最大二分匹配。这里的证明也会比较复杂,需要用到最大流等于最小割定理,我们以后再详细说。

img

2. 匈牙利算法

用最大流问题的复杂度为 O ( V E ∗ E ) O(VE*E) O(VEE),而匈牙利算法是专门用来解决图的最大匹配问题的算法,其复杂度可以达到 O ( V E ) O(VE) O(VE)。其中 V 代表点的数量,E 代表边的数量。这是一种充分挖掘了最大匹配问题内在规律后的得到的算法,非常聪明。

3. 线性规划

上面说到了,求最大流问题可以转换为线性规划问题。而线性规划问题最著名的解法自然就是 单纯形法。这种算法非常奇特,其复杂度为在最坏情况下是指数级的,但其在实践中绝大多数的情况下表现出的效率非常令人满意。而后来提出的能够在多项式时间解决线性规划问题的算法实践中表现反而没有单纯形法好。而正因为这,后来有人提出了著名的算法的平滑分析法。

4. 最大流问题其他妙用

最大流问题除了能够解决最大匹配问题外,还有其他的许多妙用。这里我们会介绍的是用最大流来解决图的点连通度和边连通度问题。点连通度是指对于一个连通图,最少删去多少点才能够使其变为非连通图。边连通度是指对于一个连通图,最少删去多少条边才能够使其变为非连通图。


图的匹配与最大流(二)—— 最大流问题 Ford-Fulkerson 方法

谢潇雨 于 2013-07-10 18:22:31 发布

本篇承接上一篇文章,主要讲解最大流问题的 Ford-Fulkerson 解法。可以说这是一种方法,而不是算法,因为它包含具有不同运行时间的几种实现。该方法依赖于三种重要思想:残留网络,增广路径和割。本文将会详细介绍这些内容,下一篇文章我们提供一种该方法的 Java 实现。

在介绍这三种概念之前,我们先简单介绍下 Ford-Fulkerson 方法的基本思想。首先需要了解的是 Ford-Fulkerson 是一种迭代的方法。开始时,对所有的 u , v ∈ V u, v \in V u,vV f ( u , v ) = 0 f(u, v) = 0 f(u,v)=0(这里 f ( u , v ) f(u, v) f(u,v) 代表 u u u v v v 的边当前流量),即初始状态时流的值为 0。在每次迭代中,可以通过寻找一个“增广路径”来增加流值。增广路径可以看做是从源点 s s s 到汇点 t t t 之间的一条路径,沿该路径可以压入更多的流,从而增加流的值。反复进行这一过程,直到增广路径都被找出为止。

举个例子来说明下,如图所示,每条红线就代表了一条增广路径,当前 s s s t t t 的流量为 3。

img

当然这并不是该网络的最大流,根据寻找增广路径的算法我们其实还可以继续寻找增广路径,最终的最大流网络如下图所示,最大流为 4。

img

接下来我们就介绍如何寻找增广路径。在介绍增广路径之前,我们首先需要介绍残留网络的概念。

一、残留网络

顾名思义,残留网络是指给定网络和一个流,其对应还可以容纳的流组成的网络。具体说来,就是假定一个网络 G = ( V , E ) G = (V, E) G=(V,E),其源点 s s s,汇点 t t t。设 f f f G G G 中的一个流,对应顶点 u u u 到顶点 v v v 的流。在不超过 C ( u , v ) C(u, v) C(u,v) 的条件下( C C C 代表边容量),从 u u u v v v 之间可以压入的额外网络流量,就是边 ( u , v ) (u, v) (u,v) 的残余容量(residual capacity),定义如下:

r ( u , v ) = c ( u , v ) − f ( u , v ) r(u, v) = c(u, v) - f(u, v) r(u,v)=c(u,v)f(u,v)

举个例子,假设 ( u , v ) (u, v) (u,v) 当前流量为 3 / 4 3/4 3/4,那么就是说 c ( u , v ) = 4 c(u, v) = 4 c(u,v)=4 f ( u , v ) = 3 f(u, v) = 3 f(u,v)=3,那么 r ( u , v ) = 1 r(u, v) = 1 r(u,v)=1

我们知道,在网络流中还有这么一条规律。从 u u u v v v 已经有了 3 3 3 个单位流量,那么从反方向上看,也就是从 v v v u u u 就有了 3 3 3 个单位的残留网络,这时 r ( v , u ) = 3 r(v, u) = 3 r(v,u)=3。可以这样理解,从 u u u v v v 3 3 3 个单位流量,那么从 v v v u u u 就有了将这 3 3 3 个单位流量的压回去的能力。

我们来具体看一个例子,如下图所示一个流网络

img

其对应的残留网络为:

img

二、增广路径

在了解了残留网络后,我们来介绍增广路径。已知一个流网络 G G G 和流 f f f,增广路径 p p p 是其残留网络 G f G_f Gf 中从 s s s t t t 的一条简单路径。形象的理解为从 s s s t t t 存在一条不违反边容量的路径,向这条路径压入流量,可以增加整个网络的流值。

上面的残留网络中,存在这样一条增广路径:

img

其可以压入 4 个单位的流量,压入后,我们得到一个新的流网络,其流量比原来的流网络要多 4。这时我们继续在新的流网络上用同样的方法寻找增广路径,直到找不到为止。这时我们就得到了一个最大的网络流。

三、流网络的割

上面仅仅是介绍了方法,可是怎么证明当无法再寻找到增广路径时,就证明当前网络是最大流网络呢?这就需要用到最大流最小割定理。

首先介绍下,割的概念。流网络 G ( V , E ) G(V, E) G(V,E) 的割 ( S , T ) (S, T) (S,T) V V V 划分为 S S S T = V − S T = V - S T=VS 两部分,使得 s ∈ S s \in S sS t ∈ T t \in T tT。割 ( S , T ) (S, T) (S,T) 的容量是指从集合 S S S 到集合 T T T 的所有边(有方向)的容量之和(不算反方向的,必须是 S → T S \rightarrow T ST)。如果 f f f 是一个流,则穿过割 ( S , T ) (S, T) (S,T) 的净流量被定义为 f ( S , T ) f(S, T) f(S,T)(包括反向的, S → T S \rightarrow T ST 的为正值, T → S T \rightarrow S TS​ 的为负值)。

将上面举的例子继续拿来,随便画一个割,如下图所示:

img

割的容量就是 c ( u , w ) + c ( v , x ) = 26 c(u, w) + c(v, x) = 26 c(u,w)+c(v,x)=26

当前流网络的穿过割的净流量为 f ( u , w ) + f ( v , x ) − f ( w , v ) = 12 + 11 − 4 = 19 f(u, w) + f(v, x) - f(w, v) = 12 + 11 - 4 = 19 f(u,w)+f(v,x)f(w,v)=12+114=19

显然,我们有对任意一个割,穿过该割的净流量上界就是该割的容量,即不可能超过割的容量。所以网络的最大流必然无法超过网络的最小割。

可是,这跟残留网络上的增广路径有什么关系呢?

首先,我们必须了解一个特性,根据上一篇文章中讲到的最大流问题的线性规划表示时,提到,流网络的流量守恒的原则,根据这个原则我们可以知道,对网络的任意割,其净流量的都是相等的。具体证明是不难的,可以通过下图形象的理解下,

img

和上面的割相比,集合 S S S 中少了 u u u v v v,从源点 s s s 到集合 T T T 的净流量都流向了 u u u v v v,而在上一个割图中,集合 S S S 到集合 T T T 的流量是等于 u u u v v v 到集合 T T T 的净流量的。其中 w w w 也有流流向了 u u u v v v,而这部分流无法流向源点 s s s,因为没有路径,所以最后这部分流量加上 s s s u u u v v v 的流量,在 u u u v v v 之间无论如何互相传递流,最终都要流向集合 T T T,所以这个流量值是等于 s s s 流向 u u u v v v 的值的。将 s s s 比喻成一个水龙头, u u u v v v 流向别处的水流,都是来自 s s s 的,其自身不可能创造水流。所以任意割的净流量都是相等的。

万事俱备,现在来证明当残留网络 G f G_f Gf 中不包含增广路径时, f f f G G G 的最大流。

假设 G f G_f Gf 中不包含增广路径,即 G f G_f Gf 不包含从 s s s v v v 的路径,定义 S = { v : G f  中从  s  到  v  存在一条通路 } S = \{v : G_f \text{ 中从 } s \text{ 到 } v \text{ 存在一条通路}\} S={v:Gf 中从 s  v 存在一条通路},也就是 G f G_f Gf s s s 能够有通路到达的点的集合,显然这个集合不包括 t t t,因为 s s s t t t 没有通路。这时,我们令 T = V − S T = V - S T=VS。那么 ( S , T ) (S, T) (S,T) 就是一个割。

如下图所示:

img

那么,对于顶点 u ∈ S u \in S uS v ∈ T v \in T vT,有 f ( u , v ) = c ( u , v ) f(u, v) = c(u, v) f(u,v)=c(u,v)。否则,如果 ( u , v ) (u, v) (u,v) 存在残余流量,那么 s s s u u u 加上 u u u v v v 就构成了一条 s s s v v v 的通路,所以 v v v 就必须属于 S S S,这与 v ∈ T v \in T vT 矛盾。因此,这时就表明当前流 f f f 等于当前割的容量,因此 f f f 就是最大流。

参考:《算法导论》《组合数学》


一篇网络流 基本模型超全总结(最大流 费用流 多源汇最大流 上下界可行流) 思路+代码模板

harry1213812138 于 2020-12-06 09:53:27 发布

一、网络流与最大流

网络流(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 算法的优化(当前弧优化)思路+代码模板

harry1213812138 于 2020-11-29 23:39:56 发布

Dinic算法

普通的dinic算法在有些时候会被卡掉,因为每次dfs太多了,所以对此就有了当前弧优化来解决这个问题。

什么是当前弧优化

当前弧优化就是说我们在每次dfs找的时候,把已经榨干的点删掉,我们直接从可以增加流量的边开始。

当前弧优化

假设我们的残留网络已经更新成现在这个网络了(如上图),假设源点s出发的有编号1、2、3号边,1和2号边都无法再增加流量了,没有用了(相当于这两条边被榨干了),那么我们在下次bfs得到的分层图再dfs找增广路时,就不考虑1和2号边了,直接从s出发去3号边找。就能省略很多层递归。

当前弧优化的实现

那么如何实现上述操作呢,我们加一个cur[ ]数组来帮助实现。cur数组最初就是h数组,就是邻接表存图的头指针。当我们在dfs时,从u点出发访问到了第i条边,就说明前i-1条边都已经被榨干了,所以才到了第i条边,因此我们直接改cur[u] = i,下次访问从u点出发的边时,就会直接访问第i条边,省略了前面i-1条边。
不懂的话看看下面代码模板,就会理解了。

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

int n,m,s,t,dis[N],cur[M];	//dis是存每个点的层数,cur是当前弧优化的头指针数组
int e[M],ne[M],w[M],h[M],idx;	//链式前向星建图

void add(int a,int b,int c)	//加边函数
{
	e[idx] = b;
	w[idx] = c;		//w是残留网络的残量
	ne[idx] = h[a];
	h[a] = idx++;
}

bool bfs()
{	
	queue<int> q;
	memset(dis,-1,sizeof dis);	//dis初始化为-1
	q.push(s);
	dis[s] = 0;	//s的层数记为0
	cur[s] = h[s];	//cur的初始化和h一样,也可以在开始的时候把h复制一份
	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])	//如果v点没有被访问过,且残量为正
			{
				dis[v] = dis[u] + 1;	//v的层数是u的层数+1
				cur[v] = h[v];		//复制一份h给cur
				if(v == t)		//如果找到了t,说明存在增广路
					return 1;	//返回1
				q.push(v);		//v点入队
			}
		}
	}
	return 0;	//未找到t点说明不存在增广路了,返回0
}

int dfs(int u,int limit)	//u是当前点,limit是u点还可以分给后面点的最大流量
{
	if(u == t)	//如果递归到了汇点t,返回limit
		return limit;
	int flow = 0;	//flow是从u点已经分出去的流量,最初是0
	for(int i = cur[u];~i;i = ne[i])	//循环u的所有出边,注意这里用的是cur数组,就是当前弧优化,访问所有还可以扩大流量的出边
	{
		cur[u] = i;	//更新cur,循环到i说明u点已经用到了第i条边,也说明前i-1条边已经被榨干了(所以才用到了第i条边),因此下次直接从u点用第i条边就可以了,所以cur[u]更新为i就可以了
		int v = e[i];
		if(dis[v] == dis[u]+1 && w[i])	//如果v点是u点的下一层,且残量为正,说明是一条增广路
		{
			int minf = dfs(v,min(w[i],limit-flow));	//递归v点,流给v点的流量是该边的容量和剩余的总流量limit-flow的最小值,minf是返回这条增广路上能增广的量
			w[i] -= minf;	//更新残留网络
			w[i^1] += minf;
			flow += minf;	//更新分出去的流量
			if(flow == limit)	//当前弧优化的细节,如果分出去的流量flow和最大流量limit相等了,说明所有流量全部分出去了,不可能再分了,直接返回
				return flow;
		}
	}
	return flow;	//return分出去的最大流量flow
}

int dinic()
{
	int ans = 0;
	while(bfs())	//当存在增广路时,bfs得到分层图
		ans += dfs(s,INF);	//dfs
	return ans;
}

int main()
{
	cin >> n >> m >> s >> t;
	memset(h,-1,sizeof 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+当前弧优化

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:有源汇上下界最小流

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

九、最大权闭合子图

闭合图

首先,先了解什么是闭合图。闭合图一般指一个图中点的集合,从该集合中所有的点出发,能到达的点要求都必须在该点集中。也就是说,从该集合中出发,一定要回到该集合中,不能出到集合外。

最大权闭合子图,顾名思义,就是所有闭合子图中点权之和最大的那个,注意这里的权指的是点权,因为闭合图是对于点集而言的。

最大权闭合子图

了解完概念后,要知道如何求最大权闭合子图。大体的思路是借助最小割模型,让每一种闭合图通过变形后都能和一种割相对应,这样求最大权就是求最小割。

具体做法

构造一个新的流网络,建一个源点s和汇点t,从s向原图中所有点权为正数的点建一条容量为点权的边,从点权为负数的点向t建一条容量为点权绝对值的边,原图中各点建的边都建成容量为正无穷的边。然后求从s到t的最小割,再用所有点权为正的权值之和减去最小割,就是我们要求的最大权值和了。

证明

简单证明,如果不想看证明的可跳过。
如何从最大权闭合子图转化到最小割的呢?首先我们要知道简单割的定义,简单割指的是割集的所有边都是从s出发的边或终点是t的边。由此,我们知道在上述建图方式中的最小割一定是一个简单割,因为其图内部的边的流量是正无穷,所以最小割集一定不包含内部的边,是一个简单割。
然后我们需要证明闭合子图和简单割是一一对应的,从而我们求闭合子图的最值就是简单割的最小值,也就是建的新图的最小割。
我们假设一个闭合子图是v,流网络分成两个集合S和T,构成一个割集的情况。S包含的是v+s(闭合子图的点+源点);T包含的是剩下的所有点(闭合子图外的点+汇点)。在这种情况下闭合子图v就和这个割[S,T]相对应,且这个割是个简单割:当前割的割集是S集合到T集合的所有边的集合,割集内一定不会出现原图内部的边,因为从S集合出发,若起点是s,则一定不包含原图内部的边,若起点是v,根据闭合子图的定义,回到的一定是v中的点,所以不包含到T集合中的点,因此该割一定是简单割。

这样我们就将闭合子图问题转化成了割的问题了,然后我们就要看看数量关系,找到最大权闭合子图和最小割间的数量表达式,就可以借助最小割模型计算最大权闭合子图了。
首先我们要找到最小割的计算表达式,下面是割集的所有情况:
在这里插入图片描述
我们将原图中的点分为两个集合,v1和v2,v1是闭合子图的点集,v2是原图中剩下的所有点,因此割的所有情况共有四种(S集合到T集合共四种情况),其中有两种情况不存在,第①种情况不存在是因为我们在建图时就不会建s到t的边,第②种情况不存在是根据闭合子图的定理,从v1出发的点一定会回到v1,不会到v2。这样割集就只剩两种情况s到v2和v1到t,根据见图方式可得:前者的容量和是v2中所有点权为正的和,后者容量和是v1中所有点权为负数的和的相反数,割集的容量之和就是v2中所有点权为正的和+v1中所有点权为负数的和的相反数

然后我们再看我们要求的闭合子图权值之和的计算表达式:最大权值和就等于v1中所有的权值和,即v1中所有点权为正的和+v1中所有点权为负的和

我们将上面求出来的两个表达式相加可得:割集容量之和+闭合子图权值之和=原图中所有点权为正数的点权之和(后面的相加后刚好抵消)。而等式的右边是个定值,我们为使闭合子图权值最大化,所以我们就要使得割集容量之和最小化,即求最小割的大小,再用所有点权为正的权值之和减去最小割,就是我们要求的最大权值了。

例题

原题链接

Acwing961:最大获利

思路

我们将中转站的成本记为负值,记成该点的点权,再将用户的收益记为记为该点的点权,然后从每一个用户向两个所需的中转站连一条有向边。之后就是求最大权闭合子图的模板了。最大权之和就是最大收益。

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 55010,M = (N+2*50000)*2+10,INF = 0x3f3f3f3f;

int n,m,s,t,sum,dis[N],cur[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++;
	e[idx] = a;
	w[idx] = 0;
	ne[idx] = h[b];
	h[b] = 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])
	{
		int v = e[i];
		cur[u] = 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+m+1;
	for(int i = 1;i <= n;i++)
	{
		int p;
		cin >> p;
		add(m+i,t,p);		//中转站到汇点建边
	}
	for(int i = 1;i <= m;i++)
	{
		int a,b,c;
		cin >> a >> b >> c;
		add(i,m+a,INF);		//内部的边建成无穷大
		add(i,m+b,INF);
		add(s,i,c);		//源点到用户建边
		sum += c;		//计算点权为正的权值之和
	}
	cout << sum-dinic() << endl;	//正数的点权和减去最小割就是答案(最大权闭合子图)
 
	return 0;
}

十、最大密度子图

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


via:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值