网络流与二分图(deserted)

本篇博客已被废弃,新版详见 网络流,二分图与图的匹配

0. Change log

  • 2021.12.5:更换模板代码。新增二分图部分。
  • 2022.1.11:重构网络流部分。新增网络流的应用与模型。
  • 2022.1.12~1.13:新增上下界网络流部分。

1. 网络流

网络流的核心在于 建图:建图是精髓,建图是人类智慧。网络流的建图方法从某种程度上刻画了 贪心问题的内在性质,从而简便地支持了 反悔,不需要我们为每道贪心问题都寻找一个反悔策略。

1.1. 基本定义

一个网络是一张有向图 G=(V,E),对于每条有向边 (u,v)E 都存在 容量限制 c(u,v)。特别的,若 (u,v)E,则 c(u,v)=0

网络的可行流分为有源汇(通常用 S 表示源点,T 表示汇点)和无源汇两种,但无论是哪一种,网络的 流函数 f 都具有以下三个性质:

  • 首先给出定义,流函数 f:(u,v)\R 是从二元 有序对 (u,v) 向实数集 \R 的映射,其中 u,vV
  • f 满足 容量限制f(u,v)c(u,v):每条边的流量不能超过容量。若 f(u,v)=c(u,v),则称边 uv 满流
  • f 具有 斜对称 性质:f(u,v)=f(v,u)uv1 的流量,也可以说 vu1 的流量。
  • f 具有 流量守恒 性质:除源汇点外(无源汇网络流则不存在源汇点),从每个结点流入和流出的流量相等,即 i,f(u,i)=f(i,v):每个结点 不储存流量,流进去多少就流出来多少。

  • 对于 有源汇 网络,根据斜对称和容量守恒性质,可以得到 f(S,i)=f(i,T),此时这个相等的和称为当前流 f流量

  • 定义一种流在网络 G 上的 残量网络 Gf=(V,Ef) 为容量函数 cf=cf 的网络,根据容量限制,我们有 cf(u,v)0。若 cf(u,v)=0,则视边 (u,v) 在残量网络上不存在,即 (u,v)Ef。换句话说,将每条边的容量减去流量后,删去满流边即可得到残量网络。

  • 定义 增广路 P残量网络 Gf 上从 源点 S汇点 T 的一条路径。一般来说,无源汇网络流不讨论增广路。

  • V 分成 互不相交 的两个点集 A,B,其中 SATB,这种 点的划分方式 叫做 。定义割的 容量uAvBc(u,v)流量 为 $\sum\limits_{u \in A}\sum_\limits{v \in B} f(u, v)。若 u,v 所属点集不同,则称有向边 (u,v)割边

接下来的讨论范围将限制于 有源汇 网络流,对于 无源汇 网络流,见更下方的无源汇网络流部分。

1.2. 网络最大流:EK 与 Dinic

网络最大流相关算法,最著名的是 Edmonds-Karp 和 Dinic。对于更高级的 SAP / ISAP / HLPP,此处不做介绍。

简单地说,给定一张网络 G=(V,E) 和源汇,求网络的最大流量(Maximum flow,简称 MF)。

1.2.1. 增广

接下来要介绍的两个算法均使用了 不断寻找增广路 的思想。具体地,找到残量网络 Gf 上的一条增广路 P,并为 P 上的每一条边增加 Gf(P)=min(u,v)Pcf(u,v)c(u,v)f(u,v) 的最小值的流量,因为如果增加的流量大于这一最小值,那么存在边不满足容量限制,而根据 **能流满就流满 **的贪心思想,增加的流量也不会小于这一值。

在为当前边 (u,v)P 增加 流量 Gf(P) 时,我们需要给其反边 (v,u)容量 加上 Gf(P),这样的目的在于:支持反悔,收回给出的一部分流量。体现在 Gf 上,就是新的 Gfcf(u,v) 相较于 cf(u,v) 减少了 Gf(P),而 cf(v,u) 相较于 cf(v,u) 增加了 Gf(P)

上述操作称为一次 增广

关于增广,有一个常用技巧:网络流建图一般使用链式前向星。我们会将每条边与它的反向边按编号 连续存储,编号分别记为 kk+1,其中 k 是偶数,从而快速求得 k 的反向边编号为 k xor 1。为此,初始边数 cnt 应设为 1(这一点千万不要忘记!)。

1.2.2. 最大流最小割定理

在介绍 EK 和 Dinic 之前,我们还需要一个贯穿网络流始终的最核心,最基本的结论:最大流等于最小割

  • 任何一组流的流量 不大于 任何一组割的容量:考虑一个可行流 f,其流量等于任意一种割的割边流量之和。

    考虑每单位流量,设其经过 uA,vB 的割边 uv 的次数为 to,经过 vu 的割边次数为 back,显然有 to=back+1,否则就不可能从 S 流到 T 了。

    根据斜对称性质与割的流量的定义,每单位流量对割边流量之和的贡献为 toback,因此网络总流量就等于割边流量之和。根据容量限制,推出流量 割的容量。

  • 存在一组流的流量 等于 一组割的容量:我们断言 最大流存在,显然此时 残量网络不连通(若连通可以继续增广,与最大流的最大性矛盾),这为我们自然地提供了一组割,使得其容量等于流量,即当前可行流的流量。

综上,我们证明了最大流等于最小割。

1.2.3. Edmonds-Karp 算法

Edmonds-Karp 算法的核心思想是不断找 长度最小的增广路 进行增广,通过 bfs 实现。为此,我们记录流向每个点的边的编号,然后从汇点 T 不断反推到源点 S。时间复杂度 O(nm2),证明在模板题代码下方。

int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v, int w) {
	nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
	nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, S, T, fr[N], vis[N], fl[N]; ll ans;
int main(){
	cin >> n >> m >> S >> T;
	for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w);
	while(1) {
		queue <int> q; mem(fl, 0, N), mem(vis, 0, N);
		fl[S] = inf, vis[S] = 1, q.push(S);
		while(!q.empty()) { // 整个 BFS 过程
			int t = q.front(); q.pop();
			for(int i = hd[t]; i; i = nxt[i]) {
				int it = to[i];
				if(!lim[i] || vis[it]) continue; // 如果剩余流量为 0,在残量网络上不存在这条边,不能走
				vis[it] = 1, fl[it] = min(fl[t], lim[i]); // 记录流量
				fr[it] = i ^ 1, q.push(it); // 记录流向每个点的边
			}
		} if(!fl[T]) break;
		int p = T; ans += fl[T];
		while(p != S) lim[fr[p]] += fl[t], lim[fr[p] ^ 1] -= fl[t], p = to[fr[p]]; // 从 T 一路反推到 S,并更新每条边的剩余流量
	} cout << ans << endl;
}

时间复杂度证明摘自 ycx 的博客。我们需要这样一条引理:每次增广后残量网络上 S 到每个结点的最短路长度 单调不减

考虑反证法,假设存在结点 x 使得 GfSx 的最短路 disx 小于 GfSx 的最短路 disx,则必然存在 x 使得在 GfSx 的最短路上除了 x 以外的结点 y 均满足 disydisy。设 ySx 的最短路上 x 的上一个结点,则 disx=disy+1

(y,x)Ef,则 disxdisy+1,有 disy+1=disx<disxdisy+1,得出 disy<disy,矛盾,因此有向边 (y,x) 不在原来的残量网络 Gf 上。从而我们得到有向边 (x,y) 必然被增广,即 disx+1=disy,所以 disy+1=disx<disx=disy1,矛盾。

设关键边 (x,y) 属于原来的残量网络,但不在增广后的残量网络上。这是因为 (x,y) 被增广且其剩余流量 cf(x,y) 等于增广流量 Gf(P) ((x,y)P)。根据 EK 算法的过程,我们有 disx+1=disy。设使得 (x,y) 再一次出现在增广路径上的增广对应的残量网络为 Gf,此时 disy+1=disx,因为 (y,x) 被增广。根据引理,disydisy,因此 disx1disx+1,即 (x,y) 每次作为关键边,disx 都增加 2,故每条边作为关键边的次数不超过 O(n)。总增广次数不超过 O(nm),时间复杂度 O(nm2)

1.2.4. Dinic 算法

Dinic 算法的核心思想是 分层图 以及 相邻层之间增广,通过 bfs 和 dfs 实现:首先宽搜给图分层,分层完毕后从 S 开始 dfs 多路增广:维护当前结点和剩余流量,向下一层结点继续流。

Dinic 算法有重要的 当前弧优化:增广时,容量已经等于流量的边无用,可以直接跳过,不需要每次深搜到同一个点时都从邻接表头开始遍历。为此,记录从每个点出发第一条没有流满的边,称为 当前弧。每次深搜到该结点就从当前弧开始增广。注意,每次多路增广前每个点的 当前弧应初始化设为邻接表头 ,因为并非一旦流量等于容量,这条边就永远无用,反向边流量的增加会让它重新出现在残量网络中。

当前弧优化后的 Dinic 时间复杂度 O(n2m),时间复杂度证明见 ycx 博客,链接在本章最下方。不加该优化,时间复杂度会退化至和 EK 一样的 O(nm2),由于 dfs 常数过大,实际效率并没有 EK 快。


关于当前弧优化的注意事项:

for(int i = cur[u]; res && i; i = nxt[i]) {
	cur[u] = i;
    // do something
}

上述代码不可以写成

for(int &i = cur[u]; res && i; i = nxt[i]) {
    // do something
}

因为若 uv 这条边让剩余流量 res 变成 0,第二种写法会 直接跳过 (u,v),但 (u,v) 不一定流满,所以不应跳过。这会导致当前弧 跳过很多未流满的边,使增广效率降低,从而大幅降低程序运行效率。实际表现比 EK 还要差。

另一种解决方法是在循环末尾判断 if(!res) return flow;。总之,在写当前弧优化时千万注意 不能跳过没有流满的边。模板题 P3381 网络最大流 代码如下:

int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v, int w) {
	nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
	nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, s, t, dis[N], cur[N]; ll ans;
ll dfs(int u, ll res) {
	if(u == t || !res) return res; ll flow = 0;
	for(int i = cur[u]; res && i; i = nxt[i]) {
		int it = to[i], c = min(res, (ll)lim[i]); cur[u] = i;
		if(c && dis[u] + 1 == dis[it]) { // 仅在相邻两层之间增广
			ll k = dfs(it, c);
			flow += k, res -= k, lim[i] -= k, lim[i ^ 1] += k;
		}
	} return dis[u] = flow ? dis[u] : 0, flow;
}
int main(){
	cin >> n >> m >> s >> t;
	for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w);
	while(1) {
		queue <int> q; mem(dis, 0x3f, N), dis[s] = 0, q.push(s);
		while(!q.empty()) { // bfs 分层图
			int t = q.front(); q.pop();
			for(int i = hd[t]; i; i = nxt[i])
				if(lim[i] && dis[to[i]] > 1e9)
					dis[to[i]] = dis[t] + 1, q.push(to[i]);
		} if(dis[t] > 1e9) break; cpy(cur, hd, N), ans += dfs(s, 1e18);
	} cout << ans << endl;
}

1.3. 无负环的费用流:SSP 与 Primal-Dual

费用流一般指 最小费用最大流(Minimum cost maximum flow,简称 MCMF)。

相较于一般的网络最大流,在原有网络 G 的基础上,每条边多了一个属性: 权值 w(x,y)。最小费用最大流在要求我们在 保证最大流 的前提下,求出 (x,y)Ef(x,y)×w(x,y)最小值

通俗地说,w 就是每条边流 1 单位流量的费用,我们需要 最小化 这一费用,因此被称为费用流。

1.3.1. 连续最短路算法

连续最短路算法 Successive shortest path,简称 SSP。顾名思义,这一算法的核心思想是每次找到 长度最短的增广路 进行增广,且仅在网络 无负环 时能得到正确答案。

SSP 算法有两种实现,一种基于 EK 算法,另一种基于 Dinic 算法。这两种实现均要求将 bfs 换成 SPFA(每条边的权值即 w),且 Dinic 的 dfs 多路增广仅在 disx+w(x,y)=disy 之间的边进行。

时间复杂度 O(nmf),其中 f 为最大流流量。实际应用中此上界非常松,因为不仅增广次数远远达不到 f,同时 SPFA 的复杂度也远远达不到 nm,可以放心大胆使用:OI 圈一般以 Dinic 作为网络最大流的标准算法,以基于 EK 的 SSP 作为费用流的标准算法,「最大流不卡 Dinic,费用流不卡 EK」是业界公约。

正确性证明见下方 ycx 的博客。这里给出重要结论:只要初始网络无负环,则 任意时刻残量网络无负环

注意,SPFA 在队首为 T 时不能直接 break,因为第一次取出 Tdis[T] 不一定取得最短路。

int cnt = 1, hd[N], nxt[M << 1], to[M << 1], lim[M << 1], cst[M << 1];
void add(int u, int v, int w, int c) {
	nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
	nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
} int MCMF(int T) {
	int flow = 0, cost = 0;
	while(1) {
		queue <int> q; static int dis[N], vis[N], fr[N];
		mem(dis, 0x3f, T + 1), mem(vis, 0, T + 1), dis[0] = 0, q.push(0);
		while(!q.empty()) { // SPFA
			int t = q.front(); q.pop(), vis[t] = 0;
			for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
				int it = to[i], d = dis[t] + cst[i];
				if(d < dis[it]) {
					dis[it] = d, fr[it] = i;
					if(!vis[it]) vis[it] = 1, q.push(it);
				}
			}
		} if(dis[T] > 1e9) break; int fl = 1e9;
		for(int u = T; u; u = to[fr[u] ^ 1]) cmin(fl, lim[fr[u]]);
		for(int u = T; u; u = to[fr[u] ^ 1]) lim[fr[u]] -= fl, lim[fr[u] ^ 1] += fl; // Edmonds-Karp
		flow += fl, cost += 1ll * dis[T] * fl;
	} return cost;
}

1.3.2. Primal-Dual 原始对偶算法

建议首先学习 Johnson 全源最短路算法。

和 SSP 一样,Primal-Dual 原始对偶算法也仅适用于 无负环 的网络中,其核心思想为:我们为每个点赋一个 hi,让原图的最短路不变且 边权非负。使用 Johnson 全源最短路算法的思想,我们先用一遍 SPFA 求出源点到每个点的最短路 hi,那么 ij 的新边权 wi,j=wi,j+hihj。根据三角形不等式,显然 wi,j0,因此经过上述转化,我们可以使用更加稳定的 Dijkstra 而非 SPFA 求解增广路。

找到增广路后,每次增广都会改变残量网络的形态。为此,我们只需用每次增广时 Dijkstra 跑出来的最短路加在 h 上即可,即 hihi+disi。原因如下:

  • 如果 ij 在增广路上,有 disi+wi,j+(hihj)=disj。由于 wi,j=wj,i,所以 wj,i+(disj+hj)(disi+hi)=0,即 反边边权为 0
  • 对于原有的边,我们有 disi+wi,j+(hihj)disj,即 wi,j+(disi+hi)(disj+hj)0原边权仍然非负

实际表现方面,Primal-Dual 相较于 SSP 并没有很大的优势,大概率是因为 SPFA 本身已经够快了,且堆优化的 Dijkstra 常数较大。同时,在实际意义的限制下,很难建出一些能够卡掉 SPFA 的网络。

const int N = 5e3 + 5;
const int M = 5e4 + 5;
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1], cst[M << 1];
void add(int u, int v, int w, int c) {
	nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
	nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
} int n, m, s, t, h[N], vis[N], dis[N], fr[N], flow, cost;
int main(){
	cin >> n >> m >> s >> t;
	for(int i = 1, u, v, w, c; i <= m; i++) cin >> u >> v >> w >> c, add(u, v, w, c);
	queue <int> q; mem(h, 0x3f, N), h[s] = 0, q.push(s);
	while(!q.empty()) {
		int t = q.front(); q.pop(); vis[t] = 0;
		for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
			int it = to[i], d = h[t] + cst[i];
			if(d < h[it]) {h[it] = d; if(!vis[it]) vis[it] = 1, q.push(it);}
		}
	} while(1) {
		priority_queue <pii, vector <pii>, greater <pii>> q;
		mem(dis, 0x3f, N), mem(vis, 0, N), q.push({dis[s] = 0, s});
		while(!q.empty()) {
			pii t = q.top(); q.pop();
			if(vis[t.se]) continue; vis[t.se] = 1;
			for(int i = hd[t.se]; i; i = nxt[i]) if(lim[i]) {
				int it = to[i], d = t.fi + cst[i] + h[t.se] - h[it];
				if(d < dis[it]) fr[it] = i, q.push({dis[it] = d, it});
			}
		} if(dis[t] > 1e9) break;
		int c = (1ll << 31) - 1;
		for(int i = 1; i <= n; i++) h[i] += dis[i];
		for(int i = t; i != s; i = to[fr[i] ^ 1]) cmin(c, lim[fr[i]]);
		for(int i = t; i != s; i = to[fr[i] ^ 1]) lim[fr[i]] -= c, lim[fr[i] ^ 1] += c;
		flow += c, cost += h[t] * c;
	} cout << flow << " " << cost << "\n";
}

*1.4. 关于网络流的理解

在费用流的过程中,我们的策略是 贪心找到长度最短的增广路 并进行增广,但当前决策并不一定最优,因此需要为反边添加流量,表示 支持反悔。因此,网络流就是可反悔贪心,而运用上下界网络流等技巧可以很方便地处理问题的一些限制。

更一般的,网络流是一种特殊的贪心,它们之间可以相互转化:对于具有特定增广模式(网络具有某种性质)的网络流,可以从贪心的角度思考,从而使用数据结构维护。而大部分贪心题目也可以通过网络流解释。

换句话说,网络流 将贪心用图的形式刻画,而解决网络流问题的算法与某种支持反悔的贪心策略相对应,这使得我们不需要为每道贪心题目都寻找某种反悔策略,相反,建出图后就是一遍最大流或者费用流的事儿了。


网络流相关问题,关键在于发现 题目的每一种方案与一种流或割对应。例如在 P2057 [SHOI2007]善意的投票 一题中,直接将每个小朋友拆点不可行,因为无法考虑到他与他的朋友意见不一致时的贡献。为此,我们应用 最小割等于最大流 这一结论,考虑如何用一组割来表示一种意见方案,最终得到解法:每割掉一条边都表示付出 1 的代价,因此,将支持和反对的小朋友分别与 S,T 连边,同时对于一对朋友,他们互相之间需要连边,得到的图的最小割即为所求:割掉 S,i 之间的边表示 i 由支持变为反对,付出 1 的代价,i,T 之间类似。而若割掉两个朋友 u,v 之间的边,表示两个人意见不一,因为在残量网络上 u,v 分别与 S,T 相连。换句话说,对于一组割,其唯一对应了一种方案:残量网络上与 S 相连的人支持,与 T 相连的人反对。这就是经典的 集合划分模型

1.5. 上下界网络流

上下界网络流相较于原始网络 G,每条边多了一个属性:流量下界 b(u,v),它使可行的流函数需满足的 流量限制 更加严格:b(u,v)f(u,v)c(u,v)

1.5.1. 无源汇可行流

无源汇上下界可行流是上下界网络流的基础。我们需要为一张无源汇的网络寻找一个流函数 f,使得其满足流量限制,斜对称以及流量守恒限制。

解决该问题的核心思想,是 先满足流量下界限制,再尝试调整。具体地,我们首先让每条边 (u,v)流满下界 b(u,v),算出每个点的净流量 wi=f(u,i)f(i,u)。当 wi>0 时,说明流到点 i 的流量太多了,还要再还出去 wi 才能流量守恒。相反,若 wi<0,说明 i 还要流进 wi 单位流量。根据斜对称,我们有 wi=0,因此不妨设 Δ=wi>0wi=wi<0wi

这启发我们新建一个网络 GG,但每条边的流量限制 c=cf=cb。此外新建 独立于原有点集超级源点 SS超级汇点 TT(尽管当前的 G 无源无汇,但这样定义是为了接下来方便区分有源汇时不同最大流过程中的源点和汇点),若 wi>0,则 SSi 连容量为 wi 的边,否则从 iTT 连容量为 wi 的边。不难发现从 SS 连出了总容量为 Δ 的边,且总容量为 Δ 的边连向了 TT

SSTT 的最大流不等于 Δ,说明我们找不到一种合法方案,使得在满足 流量限制 的前提下具有 流量守恒 性质。相反,若等于 Δ,则 f(u,v)=b(u,v)+f(u,v) 显然合法,因为此时每个点的 wi 均为 0,流量守恒,且 f=b+fb+c=b+(cf)=b+(cb)=c,即 bfc代码

1.5.2. 有源汇可行流

TS 连容量为 + 的边,转化为 无源汇 上下界可行流。注意连边是 源汇 之间而非 超源超汇

1.5.3. 有源汇最大流

有源汇上下界最大流算法基于一个非常有用的结论:给定 任意 一组 可行流,对其运行最大流算法,我们总能得到正确的最大流。这是因为最大流算法本身 支持撤销,即退流操作。所以,无论初始的流函数 f 如何,只要 f 合法,就一定能求出最大流。

因此,我们考虑先求出任意一组可行流,再进行 调整:首先对网络 G 跑一遍有源汇 可行流,过程中我们会新建网络 G。然后,撤去 SS,TT 以及 TS 容量为 + 的边。这是因为 SS,TT 存在的意义是求解无源汇可行流,TS 的边是将有源汇可行流转化为无源汇可行流。这说明现在我们已经得到了一组有源汇可行流,除非转化成的无源汇可行流问题无解。若要得到当前流量,TS反边 ST 的流量即为所求。

接下来进行调整:根据结论,我们只需要以 S 为源,T 为汇在 G 上再跑一遍最大流,并将可行流流量与最大流流量(新增流量)相加即为答案。注意与在此之前求解无源汇上下界可行流时,以 SSTT 为源汇作区分。

易错点 1:调整的整个过程在 G 上进行,千万不能在 G 上面跑最大流,因为 G 上面的退流操作会使得 f 不符合容量限制,而 G 不会。因为 G 的实际流量 f 等于 b+f,其中 fG 上的流函数,所以只要 f 符合容量限制,那么 f 一定也符合。代码

易错点 2:可行流流量是 TS 的反边流量,而不是 SSTT 的流量!

1.5.4. 有源汇最小流

根据 ST 的最小流等于 TS 的最大流的相反数这一结论,用可行流流量减掉 GTS​ 的最大流。

1.5.5. 有源汇费用流

只需将最大流算法换成费用流即可。所有 SS,TT 相关的连边代价均为 0

1.6. 应用与模型

1.6.1. 最小割点

通常情况下题目要求的最小割是 最小割边,但如果问题变成:删去每个 i 有代价 wi,求使得 S,T 不连通的最小代价,应该如何求解呢?

应用网络流的核心技巧:点边转化,将每个点拆成入点 iin 和出点 iout,从 iiniout 连一条容量为 wi 的边,表示删去这个点,使得 iiniout 不连通需要 wi 的代价。对于原图的每一条边 (u,v),从 uoutvin 连容量为正无穷的边,因为我们只能删点,而不是割掉边。不难发现 SoutTin 的最小割即最大流即为所求。例题见 IV. & VII.

1.6.2. 集合划分模型

minx1,x2,,xnu,vEcu,vxuxv+uauxu+buxu

其中 xi=0/1xi 表示将 xi 取反得到的结果。给定 Ec,我们的任务就是为 xi 选择合适的值,使得整个和式的值最小。我们可以为上式赋予实际意义:n 个物品,A,B 两个集合,物品 i 分到集合 A 有代价 ai,分到集合 B 有代价 bi。此外,给定若干限制 (u,v,cu,v),表示若 u,v 不在同一集合 还需要 cu,v 的代价。

建模:将 iS,T 连边,容量分别为 bi,ai不要弄反了)。此外,将限制 (u,v,cu,v) 表示为 u,v 之间容量为 cu,v双向边,得到网络 G。上述问题和 最小割 是等价的:iS 相连,此时割开了 iT,表示将 i 划分到 A,有 ai 的代价;iT 相连,此时割开了 Si,表示将 i 划分到 B,有 bi 的代价;此外, (u,v) 不属于同一集合,则 uvvu 之间有一条被割开(因为 S,T 分别与 u,v 相连,如果不割开一条边,S,T 就连通了),方向取决于 S 究竟与 u 还是 v 相连。

因此,对上述网络 G 求最小割,即最大流,就得到了答案。接下来我们讨论一些扩展问题:

  • ai,bi 出现负值时,普通的最大流不能得到正确结果,因为我们无法解决 容量为负的最大流问题。考虑将 ai,bi 同时加上 δi,最后再在求出的最小割中减掉 δi。这是因为 i 必须在选或不选 任选一种 方案,所以同时为 ai,bi 加上 δi 对最小割的影响为 δi。体现在图上即 SiT,为了使 S,T 不连通,必须 至少割掉一条边。要使代价最小化,我们不会同时割掉两条边,所以 恰好割掉一条边
  • cu,v 出现负值时,除非所有 c 均为负值且要求代价最大化,此时所有边权取反,否则问题不可做。取反可以通过 代价和贡献的转化 理解,即若代价为 1,则贡献为 1,一般我们希望 最大化贡献最小化代价

1.6.3. 最大权闭合子图

一张 有向图 G=(V,E)闭合子图 G 定义在点集 VV 上。一个点集 V 符合要求当且仅当 V 内部每个点的 所有出边 仍指向 V,即点集内部每个点在有向图上能够到达的点仍属于该点集。V点导出子图G

最大权闭合子图问题,即每个点 u 有点权 wu,点集的权值为点集内每个点的权值之和。求闭合子图的最大权值。

考虑 集合划分模型,对于每个结点,我们可以将其划分到 不选 的集合当中,体现在建图上即 Si 相连表示选,此时需要割开 iT,贡献为 wi,因此 iT 有容量 wi,同理,Si 有容量 0。如果 (u,v)E,说明若 u 分到选的集合中,v必须 被分到选的集合当中,即 uv 有容量 。对上图求 最大割 即可。

由于最大割是 NP-Hard 问题,所以考虑权值取相反数求最小割。对于 wi0 的点,iT 的容量 wi0。但对于 wi>0 的点,iT 的容量 wi<0。我们 不允许 最大流的求解过程中出现 负容量边,因为不可做。考虑应用我们上面讨论过的集合划分模型的扩展问题,将 SiiT 同时加上 wi,体现在建图上即对于 wi>0Si 容量为 wiTi 容量为 0,最后减去所有正权点权值之和,并对所得结果 取相反数

综上,我们得到了求解最大权闭合子图的一般算法:对于 wi>0Si 连容量为 wi 的边。对于 wi<0iT 连容量为 wi 的边。对于 (u,v)Euv 连容量为 + 的边。设得到的网络为 G,最终答案即 (wi>0wi)Minimum Cut(G)。例题见 VIII. & IX. & X. & XII.

另一种理解方式是我们首先全选所有正权点,然后减掉代价。删掉正权点的代价为 wi,加入负权点的代价为 wi​,于是有上述建图方法。

1.6.4. 有负环的费用流

考虑运用上下界网络流将负权边强制满流,并令反边 b(v,u)=0c(v,u)=c(u,v) 表示退流。此时,(u,v) 由于 b(u,v)=c(u,v),不会出现在 G 中,所以不可能存在负环,即任意时刻网络无负环(G 处理完毕后删掉 TS 显然也不可能让网络出现负环)。正确性得证。模板题 代码如下:

const int N = 200 + 5;
const int M = 3e4 + 5;

int n, m, s, t;
struct Graph {
	int cnt = 1, hd[N], nxt[M << 1], to[M << 1], lim[M << 1], cst[M << 1];
	void add(int u, int v, int w, int c) {
		nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
		nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
	} int S, T;
	pii mincost(int _S, int _T) {
		int flow = 0, cost = 0; S = _S, T = _T;
		while(1) {
			queue <int> q; static int fr[N], dis[N], vis[N];
			mem(dis, 0x3f, N), mem(vis, 0, N), q.push(S), dis[S] = 0;
			while(!q.empty()) {
				int t = q.front(); q.pop(), vis[t] = 0;
				for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
					int it = to[i], d = dis[t] + cst[i];
					if(d < dis[it]) {
						dis[it] = d, fr[it] = i;
						if(!vis[it]) vis[it] = 1, q.push(it);
					}
				}
			} if(dis[T] > 1e9) return make_pair(flow, cost);
			int fl = 1e9;
			for(int i = T; i != S; i = to[fr[i] ^ 1]) cmin(fl, lim[fr[i]]);
			for(int i = T; i != S; i = to[fr[i] ^ 1]) lim[fr[i]] -= fl, lim[fr[i] ^ 1] += fl;
			cost += fl * dis[T], flow += fl;
		}
	}
};

struct Bound {
	int m, u[M], v[M], lo[M], hi[M], val[M]; Graph G;
	void add(int a, int b, int c, int d) {
		if(d < 0)
			u[++m] = a, v[m] = b, lo[m] = c, hi[m] = c, val[m] = d,
			u[++m] = b, v[m] = a, lo[m] = 0, hi[m] = c, val[m] = -d;
		else u[++m] = a, v[m] = b, lo[m] = 0, hi[m] = c, val[m] = d;
	} pii mincost(int S, int T) {
		static int w[N], dt = 0, flow, cost = 0;
		for(int i = 1; i <= m; i++) {
			w[u[i]] -= lo[i], w[v[i]] += lo[i], cost += lo[i] * val[i];
			G.add(u[i], v[i], hi[i] - lo[i], val[i]);
		} int SS = n + 1, TT = n + 2;
		for(int i = 1; i <= n; i++)
			if(w[i] > 0) dt += w[i], G.add(SS, i, w[i], 0);
			else G.add(i, TT, -w[i], 0);
		G.add(T, S, 1e9, 0);
		pii res = G.mincost(SS, TT); cost += res.se, flow += G.lim[G.hd[S]];
		G.hd[S] = G.nxt[G.hd[S]], G.hd[T] = G.nxt[G.hd[T]];
		res = G.mincost(S, T), cost += res.se, flow += res.fi;
		return make_pair(flow, cost);
	}
} f;

int main() {
	cin >> n >> m >> s >> t;
	for(int i = 1, a, b, c, d; i <= m; i++)
		cin >> a >> b >> c >> d, f.add(a, b, c, d);
	pii ans = f.mincost(s, t);
	cout << ans.fi << " " << ans.se << endl;
	return flush(), 0;
}

1.6.5. 最大费用最大流

将所有权值取相反数转化为最小费用最大流,根据上一部分的技巧,求解(可能)有负环的费用流。

1.7. 例题

现在你已经对网络流的基本原理有了一定了解,就让我们来看一看下面这个简单的例子,把我们刚刚学到的知识运用到实践中吧。

*I. P4249 [WC2007] 剪刀石头布

注意到对于任意三个不相同的点 i,j,k,若它们不能构成三元环,则一定有且只有一个点击败了另外两个点。枚举这个点 i,那么对于它所有出点中任意两个 j,k 都无法组成三元环。因此最终答案为 (n3)i=1n(degi2)。故我们需要最小化 i=1n(degi2)

本题的核心技巧:拆组合数贡献。将 (x2) 写作 1+2++x,这启发我们使用网络流:把未被定向的边 (u,v) 抽象成一个结点 cSccucv 都连一条容量为 1,代价为 0 的边,表示有且仅有一个点被选择。此外,从每个点 uT 连若干条容量为 1,代价分别为 degu,degu+1,,n1,其中 degu 是已经确定的边中 u 的出度,然后跑最小费用最大流即可。

*II. P1251 餐巾计划问题

网络流相关问题,一个十分重要的技巧是拆点。如果把每天仅看成一个点,我们无法区分干净的餐巾和脏餐巾,即干净的餐巾用完后还能作为脏餐巾继续流,而不是直接流到汇点去了。

因此考虑拆点,每天晚上得到 Sri 条脏餐巾,每天早上向 Tri 条干净餐巾,对于延迟送洗相邻两天晚上之间连边,对于买毛巾 S 向每天早上连边,对于送洗,每天晚上向其对应的得到餐巾那天早上连边,跑最小费用最大流即可。

III. P2936 [USACO09JAN] Total Flow S

最大流模板题。

*IV. P1345 [USACO5.4] 奶牛的电信 Telecowmunication

最小割点模板题。

*V. P4016 负载平衡问题

考虑求出平均值 avg,若 ai>avgSi 连容量 aiavg,边权为 0 的边,否则 iT 连容量 avgai,边权为 0 的边,这也是网络流常见技巧:多出来的,从源点连出去;需要的,连到汇点去。相邻的点连容量 +,边权为 1 的边,最小费用最大流即为答案。

*VI. P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查

好题,分析见 1.3. 部分。

VII. P2045 方格取数加强版

考虑借鉴最小割点的 点边转化 思想,将每个点 i 拆成 iinioutiiniout 分别连容量为 1,边权为 ci 和容量为 k1,边权为 0 的边,表示每个点的贡献只会算一次。每个点的出点向右侧和下侧点的入点连边,此外 S(1,1)in(n,n)outT 连边,容量均为 k,边权均为 0。最大费用最大流即为所求。

VIII. P2805 [NOI2009] 植物大战僵尸

对于每个植物,从它的攻击位置向它连边,表示若选择攻击位置,则必须选择该植物。对反图拓扑排序(因为环以及能到达环的点都不可以选择)后就是最大权闭合子图问题了。

IX. P2762 太空飞行计划问题

最大权闭合子图板子题。

X. P3410 拍照

草。

XI. P2604 [ZJOI2010]网络扩容

对于第一问跑一边最大流板子,第二问为每条边新建与其平行且容量为 +,代价为扩容费用的边,跑 k 次增广流量为 1 的最小费用最大流,或通过建超级汇点限制流量为 k

XI. CF1082G Petya and Graph

如果一条边被选,那么其两个端点也必须选,这是最大权闭合子图模型。

XII. P5192 Zoj3229 Shoot the Bullet | 东方文花帖

对于每一天 i,从 Si 连流量限制为 [0,Di] 的边,从 i 向每个少女 ki 连流量限制为 [Lki,Rki] 的边。对于每个少女 i,向 T 连流量限制为 [Gi,] 的边,跑有源汇上下界最大流。注意在编号上区分 S,T,SS,TT 以及少女 i 和第 i 天。

XIII. P4843 清理雪道

有源汇上下界最小流板题:从 SiT 连容量范围为 [0,+] 的边,原图的边容量范围为 [1,+] 表示必须被流。

XIV. P4553 80 人环游世界

有源汇上下界费用流板题。

XV. P2754 [CTSC1999]家园 / 星际转移问题

考虑建出分层图,在分层图上求解网络最大流。答案每增加 1 就新建一层点,然后在残量网络上跑最大流。

1.8. 参考资料与博客

2. 二分图

2.1. 定义,判定与性质

二分图的定义:设无向图 G=(V,E),若能够将 V 分成两个点集 V1,V2 满足:V1V2=V1V2=V(u,v)E 都满足 u,v 同属于 V1V2,那么称 G 是一张二分图,V1,V2 分别是其左部点和右部点。

一张图是二分图的 充分必要条件不存在奇环。必要性:从任意一个点出发,必须经过偶数条边才能回到这个点;充分性:对不存在奇环的图 黑白染色可以得到一组划分 V1,V2 的方案。

通过这一性质,我们得到在线性时间 |V|+|E| 内快速判定二分图的方法:从任意没有被染色的结点对其所在连通块进行黑白染色,若存在一条边使得其两端点颜色相同,则存在奇环,不是二分图。反之则是二分图。

2.2. 二分图匹配

二分图的匹配的定义如下:给定一张二分图 G=(V,E),若其 边导出子图 G=(V,E)G 满足对于 E 中任意两条边不交于同一端点,那么称 G 是二分图 G​ 的一组 匹配

2.2.1. 最大匹配

二分图最大匹配问题即求出选出边集 E 的大小的最大值。特别的,若二分图左部点和右部点个数均为 n,且该二分图最大匹配包含 n 条边,则称其具有 完美匹配

  • 边导出子图:选出若干条边,以及这些边所连接的所有顶点组成的图。
  • 点导出子图:选出若干个点,以及两端都在该点集的所有边组成的图。

求解该问题的经典算法是匈牙利算法,见 Part 3.1 匈牙利算法。我们尝试用网络流解决该问题:一个结点最多与一条边相连,即 结点度数 1。这启发我们从源点 SV1 每个结点连边,从 V2 每个结点向汇点 T 连边,再加上原图的边,所有边容量均为 1ST 的最大流即最大匹配。正确性不难证明:一组可行流与一组匹配一一对应,且流量等于匹配数量。

使用 Dinic 求解二分图最大匹配,时间复杂度是优秀的 O(EV),证明见 OI-Wiki

2.2.2. 最大多重匹配

多重匹配指任意一个结点 u 不能与超过 Lu 条边相连。不难发现一般匹配即 Lu=1 的特殊情况。求解最大多重匹配,只需要将 SV1 的每条边 Su 的容量设为 Lu 而非 1,对于 V2T 同理。同时,二分图内部每条边的容量不变,仍为 1​。对上述网络求最大流即最大多重匹配。

2.2.3. 带权最大匹配

对于最小权最大匹配,将最大流算法换成最小费用最大流;

对于最大权最大匹配,将最大流算法换成最大费用最大流。由于图中不可能出现正环,所以只需要朴素地权值取反求最小费用最大流。

对于最大权 完美 匹配,有专门的 KM 算法 解决这类问题。详见 Part 3.2 KM 算法。

2.3. 二分图相关问题

2.3.1. 最小点覆盖集

二分图点覆盖集定义如下:给定一张二分图 G=(V,E),若点集 VV 满足对于任意 (u,v)E 都有 uVvV,那么称 V 是二分图 G 的一个覆盖集。i.e. 一个点可以覆盖以该点为端点的边,求覆盖所有边的最小点集。

二分图的点覆盖集与割相对应:建出我们求最大匹配时的图,我们钦定割仅在两端取到。若在两部点之间的边取到,可以调整至两端,即割 (u,v) 等价于割掉 (S,u)(v,T),因为若 u,v 之间有流量,那么 SuvT 显然满流。对于左部点 uV1,如果 Su 被割掉,那么 u 属于最小点覆盖集。类似地,若右部点 vV2 满足 vT 被割掉,v 也属于最小点覆盖集。

上述构造是一组合法的点覆盖集,反证法可证:若存在边 (u,v)E 没有被覆盖,这说明 SuvE 存在增广路,与流的最大性矛盾。综上,最小点覆盖集等于最小割,即最大流,也即二分图最大匹配。

应用:对于每条限制 e,都恰好有两种方案 u,v 能满足,且一种方案可以满足多条限制。每条限制都必须被满足,求最少需要选择多少组方案。显然,问题可以转化为二分图最小点覆盖集进行求解,如例题 VI.

2.3.2. 最大独立集

二分图的独立集定义如下:给定一张二分图 G=(V,E),若点集 VV 满足对于点集中任意两点不存在连边,则称 V 是二分图 G 的一个独立集。

考虑二分图 G=(V,E) 的最小点覆盖集 V。因为每一条边都被至少一个 uV 所覆盖,所以 VV 的所有点之间互不相连。实际上,独立集与点覆盖集一一对应,且独立集与点覆盖集交为空,并为 V,即 点覆盖集与独立集互补。因此 二分图最大独立集等于 |V| 减去最小点覆盖集

2.3.3. 最大团

二分图的团定义如下:给定一张二分图 G=(V,E),若其 点导出子图 G=(V,E) 满足将 V 分成点集 V1,V2 之后,对于任意 uV1,vV2 都有 (u,v)E,那么称 G 是二分图 G 的一个团。

建出 G 的补图 G=(V,E),若 (u,v)E 那么 u,v 不能同时出现在最大团中。故 二分图最大团等于补图最大独立集


使 V1V2 尽可能大 的最大团算法:最大团 补图最大独立集 补图最小点覆盖集的补集。不妨设我们要使最大团中的 |V1| 尽可能大,对应就是使补图最小点覆盖的 |V1| 尽可能小。根据结论 “二分图的点覆盖集与割相对应”,问题等价于求原二分图 G 的补图 G=(V,E) 的最小割,使得 V1V 中被割掉的点尽量少。

如果仅仅在原图上面跑最大流,我们无法控制二分图某部点被割掉的点的数量,考虑如何在 不影响最大匹配的前提下 为每条边附上权值,使得我们求出的最小割 尽量割 vV2T 之间的边

不妨将 SuV1 之间的边容量修改为 c+1vV2T 之间的边容量修改为 c,这样可以优先割 (v,T),让 V1 被割的点尽可能少。但为了保证最大匹配的正确性,c 应当不小于 n=min(|V1|,|V2|):不能出现 x 个左部点匹配了 y>x 个右部点的情况,即 (n1)×(c+1)<nc,化简得到 c>n1。此外,不要忘记将 (u,v)E 之间的边权附上 n:因为 (v,T) 的容量限制为 n(u,v) 的流量不可能超过 n,所以赋成 n 即可。

2.5. 应用与模型

2.5.1. DAG 最小路径覆盖

给定一张 DAG G=(V,E),定义其路径覆盖为一个路径集合 P,满足每个结点至少被一条路径覆盖。根据路径是否可以有交,即一个结点是否只能恰好被一条路径覆盖,可以分为不相交路径覆盖与可相交路径覆盖。

最小 不相交 路径覆盖:考虑一个点是否有入边和出边。对于 (u,v)E,我们可以将 P 中以 u 结尾的路径和以 v 开头的路径相连,减少一条路径,相当于占用了 u 的出边和 v 的入边。每个点只能有一条入边和一条出边,这启发我们使用二分图最大匹配解决这一问题:建出一张左右部点大小均为 |V| 的二分图 G,左部点表示出边被选中,右部点表示入边被选中。对于 (u,v)E,从 u 对应的左部点 ulv 对应的右部点 vr 连边,G 的最小路径覆盖大小即 n 减去 G 的最大匹配。构造方案也是容易的。

最小相交路径覆盖:

2.6. 例题

*I. [BZOJ2162] 男生女生

本题可以看做割裂的两部分,一部分是求二分图使某部点尽量多的最大团,另一部分则需要用到二项式反演,见 反演与狄利克雷卷积 Part 2. 二项式反演。

II. P2055 [ZJOI2009] 假期的宿舍

非常裸的二分图最大匹配。对于右部点,所有回家的人向汇点连边,对于左部点,所有要在学校住下的人从源点连边,两部点之间认识的人连边,求最大匹配是否与左部点个数相等即可。

III. P3701 主主树

对于能左边能打败右边的,连一条容量为 1 的边。对于左部点从 S 连一条容量为生命值的边,对于右部点向 T 连一条容量为生命值的边。注意 YYYJ +1s 不会让自己 -1s,因此每个 J 的生命值还要加上对应的 YYY 个数。带权最大匹配对 mmin 即可。

IV. P2756 飞行员配对方案问题

二分图匹配模板题。对于输出方案,只需找到所有满流(即剩余流量为 0)的连在两部图之间的边即可。

V. P6268 [SHOI2002] 舞会

二分图最大独立集模板题,答案即 n 减去最小点覆盖,即 n 减去最大匹配。

VI. P7368 [USACO05NOV] Asteroids G

一个小行星被消除当且仅当它所在的行或列被选中,考虑建出二分图,则题目转化为二分图最小点覆盖集,直接跑最大匹配即可。

VII. P1231 教辅的组成

三分图最大匹配(大雾)。注意点:为限制中间点的度数为 1,需要拆成两个点,之间连容量为 1 的边。直接连会导致中间点度数 >1,就寄了。

VIII. P2774 方格取数问题

二分图最大带权独立集问题,转化为最大带权匹配,直接做就行了。

IX. P2765 魔术球问题

显然的 DAG 最小路径覆盖。本题还可以贪心,感觉很神。

X. P3254 圆桌问题

二分图多重匹配板子题。

XI. P4014 分配问题

3. 图的匹配

3.1.

posted @   qAlex_Weiq  阅读(1040)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示