网络流 复习笔记


网络流是图论中的经典问题,应用非常广泛,思想并不难理解,但实现部分大部分文章都不够详细,导致蒟蒻君学的时候就看了很长时间,这篇文章会较为详细的阐述细节部分。

最大流

问题

有向带权图中,每一条(注意这里不能叫边…) < u , v > <u,v> <u,v>权值为 w w w,含义为 u u u v v v的容量,即最大能流过去的量,为 w w w。有源点 S S S和汇点 T T T,表示要从 S S S流到 T T T,问最多能流多少。

→ \to

如图,左为容量,右为流量。

这里先给出一些定义。

  • 容量:一条弧上的最大流量,记为 c ( u , v ) c(u,v) c(u,v)
  • 流量:实际流的量,记为 f ( u , v ) f(u,v) f(u,v)
  • 饱和弧:流满,即 f ( u , v ) = c ( u , v ) f(u,v)=c(u,v) f(u,v)=c(u,v)
  • 零流弧:没流,即 f ( u , v ) = 0 f(u,v)=0 f(u,v)=0
  • 容量网络:对于有向带权图 G ( V , E ) G(V,E) G(V,E),有指定 S , T ∈ V S,T\in V S,TV,表示源点和汇点;对于 ∀ < u , v > ∈ E \forall <u,v>\in E <u,v>∈E,有 c ( u , v ) > 0 c(u,v)>0 c(u,v)>0
  • 网络流:所有 f ( u , v ) f(u,v) f(u,v)的集合;
  • 可行流:可行的网络流;
  • 最大流:总流量最大的可行流。

那可行流具体要满足什么条件呢?

  1. 对于 ∀ < u , v > ∈ E \forall <u,v>\in E <u,v>∈E,有 0 ≤ f ( u , v ) ≤ c ( u , v ) 0\le f(u,v)\le c(u,v) 0f(u,v)c(u,v)
  2. 对于 u ∈ V u\in V uV,有 从 u 流出的量 − 流入 u 的量 = { u = V s → ∣ f ∣ u ≠ V s , u ≠ V t → 0 u = V t → − ∣ f ∣ 从u流出的量-流入u的量=\left\{\begin{array}{ll} u=V_s & \to|f| \\ u \neq V_s,u\neq V_t &\to 0 \\ u = V_t &\to -|f| \end{array}\right. u流出的量流入u的量= u=Vsu=Vs,u=Vtu=Vtf0f

对于任何容量网络,总是存在可行流,如全为零流。

EK

这里还会有些定义。

  • 链:顶点序列中相邻两点均要有弧连接(这里不限制方向),其方向为 S S S T T T

  • 前向弧:与链方向相同的弧,即沿着链,经过这条弧时按其方向走,能从 S S S走到 T T T。注意这里弧的方向是对于指定链的,令链为 P P P,则前向弧记为 P + P^+ P+

  • 后向弧:与链方向相反的弧,记为 P − P^- P

    这里举个例子。

    如图,此时链为 P = { S , 1 , 2 , 3 , T } P=\{S,1,2,3,T\} P={S,1,2,3,T},则 P + = { < S , 1 > , < 2 , 4 > , < 4 , T > } P^+=\{<S,1>,<2,4>,<4,T>\} P+={<S,1>,<2,4>,<4,T>} P − = { < 2 , 1 > } P^-=\{<2,1>\} P={<2,1>},当然,一条弧的方向对于不同的链可能是不同的,如对于链 P ′ = { S , 2 , 4 , T } P'=\{S,2,4,T\} P={S,2,4,T},弧 < 2 , 1 > <2,1> <2,1>为正向弧。

  • 增广路

    若有链 P P P,从 S S S T T T,任意一条弧都有剩余容量,即对于所有 < u , v > <u,v> <u,v>,有 f ( u , v ) < c ( u , v ) f(u,v)<c(u,v) f(u,v)<c(u,v)这样的 P P P显然可以通过让所有弧的流量都加上最小的 c ( u , v ) − f ( u , v ) c(u,v)-f(u,v) c(u,v)f(u,v)使最大流更大。

  • 残量

    表示只考虑当前弧合法的情况下还可以增加的流量,即 c ′ ( u , v ) = c ( u , v ) − f ( u , v ) c'(u,v)=c(u,v)-f(u,v) c(u,v)=c(u,v)f(u,v)

  • 残量网络

    对于容量网络 G ( V , E ) G(V,E) G(V,E),令其残量网络为 G ′ ( V ′ , E ′ ) G'(V',E') G(V,E)

    其中 V ′ = V V'=V V=V

    对于弧 < u , v > ∈ E <u,v>\in E <u,v>∈E

    • 若有 f ( u , v ) < c ( u , v ) f(u,v)<c(u,v) f(u,v)<c(u,v),则有 < u , v > ∈ E ′ <u,v>\in E' <u,v>∈E c ′ ( u , v ) = c ( u , v ) − f ( u , v ) c'(u,v)=c(u,v)-f(u,v) c(u,v)=c(u,v)f(u,v)
    • 若有 f ( u , v ) > 0 f(u,v)>0 f(u,v)>0,则有 < v , u > ∈ E ′ <v,u>\in E' <v,u>∈E c ′ ( v , u ) = f ( u , v ) c'(v,u)=f(u,v) c(v,u)=f(u,v)

想必大家已经知道EK算法的核心思想了:构建残量网络,不断bfs寻找增广路,只要存在,流量就可以增大;否则,当前流就是最大流,复杂度为 O ( V E 2 ) O(VE^2) O(VE2)

#include <bits/stdc++.h>
using namespace std;
const int N = 205, M = 405;
struct edge {
	int u, v, c, nxt;	// c: c',即剩余可用流量
} e[M];
int head[N], cnt, S, T;
inline void add(int u, int v, int c) {
	e[++cnt] = {u, v, c, head[u]}, head[u] = cnt;
}
inline void adde(int u, int v, int c) {
	add(u, v, c), add(v, u, 0);
}
int E[N]; // E[u]: 到达u以到达T的边的编号
bool bfs() {	// 返回值:是否找到增广路
	queue<int> q;
	q.push(S);
	memset(E, -1, sizeof E);
	while (q.size()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (E[v] == -1 && e[i].c) {   // 若v未访问且目前弧非零流
				E[v] = i;
				if (v == T) {
					break;
				}
				q.push(v);
			}
		}
		if (E[T] != -1) {	// 成功到达T,即找到增广路
			return 1;
		}
	}
	return 0;
}
int EK() {
	int res = 0;
	while (bfs()) {
		int del = 1e9;	// del: 增广路中最小c',即最大流增加的量
		for (int i = T; i != S; i = e[E[i]].u) {
			del = min(del, e[E[i]].c);
		}
		for (int i = T; i != S; i = e[E[i]].u) {	// 因为是剩余所以正减反加
			e[E[i]].c += del;
			e[E[i] ^ 1].c -= del;
		}
		res += del;
	}
	return res;
}
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int n, m;
	cin >> n >> m;
	while (m--) {
		int u, v, c;
		cin >> u >> v >> c;
		adde(u, v, c);
	}
	cin >> S >> T;
	cout << EK() << '\n';
	return 0;
}

Dinic

相当于EK的优化,不同之处有以下两点:

  • bfs分层,寻找增广路时只会从深度为 d d d的结点走到深度为 d + 1 d+1 d+1的,这样做避免了 G G G中一条弧在 G ′ G' G中变为两条时的影响;
  • dfs多路增广,从每次只能找一条增广路变成多条。

复杂度为 O ( V 2 E ) O(V^2E) O(V2E)

#include <bits/stdc++.h>
using namespace std;
const int N = 105, M = 10005;
struct edge {
	int v, c, nxt;	// 这里c同样表示c',即剩余可用流量
} e[M];
int head[N], cnt;
inline void add(int u, int v, int c) {
	e[++cnt] = {v, c, head[u]}, head[u] = cnt;
}
inline void adde(int u, int v, int c) {
	add(u, v, c), add(v, u, 0);
}
int S, T, d[N];	// d: 层数
bool bfs() {	// 返回值:是否还会有有增广路,即G'中存在一条S到T且流量非0的链P
	memset(d, -1, sizeof d);
	queue<int> q;
	q.push(S);
	d[S] = 0;
	while (q.size()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (d[v] == -1 && e[i].c) {	// 未访问过且顺序正确
				q.push(v);
				d[v] = d[u] + 1;
			}
		}
	}
	return d[T] != -1;
}
// 返回值:增量
int dfs(int u, int f) {	// f: 剩余可用流量,这里“可用”指还能增大,即最小的c'
	if (u == T) {
		return f;
	}
	int res = 0;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (d[v] == d[u] + 1 && e[i].c) {
			int t = dfs(v, min(f, e[i].c));	// 增大的量
			e[i].c -= t;
			e[i ^ 1].c += t;
			res += t;
			f -= t;
			if (!f) {	// 不能增大就结束
				break;
			}
		}
	}
	if (res == 0) {	// 在非零流情况下不能到达点u,说明u已经无效,这样在判断顺序的时候会有效
		d[u] = -1;
	}
	return res;
}
int Dinic() {
	int res = 0;
	while (bfs()) {
		res += dfs(S, 1e9);
	}
	return res;
}
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int n, m;
	cin >> n >> m;
	while (m--) {
		int u, v, f;
		cin >> u >> v >> f;
		adde(u, v, f);
	}
	cin >> S >> T;
	cout << Dinic() << '\n';
	return 0;
}

最小割

##问题

​ 额定义还是有点的。

  • 对于容量网络 G ( V , E ) G(V,E) G(V,E),删去一些边,使得 G G G被划分为 S 1 S1 S1 S 2 = V − S 1 S2=V-S1 S2=VS1两部分,我们用 ( S 1 , S 2 ) (S1,S2) (S1,S2)来表示一个割。

  • s − t s-t st

    对于一个割 ( S 1 , S 2 ) (S1,S2) (S1,S2),若源点 S ∈ S 1 S\in S1 SS1,汇点 T ∈ S 2 T\in S2 TS2,则称 ( S 1 , S 2 ) (S1,S2) (S1,S2)为s-t割,称 < u , v > ( u ∈ S 1 , v ∈ S 2 ) <u,v>(u\in S1,v\in S2) <u,v>(uS1,vS2)为前向弧, < u , v > ( u ∈ S 2 , v ∈ S 1 ) <u,v>(u\in S2,v\in S1) <u,v>(uS2,vS1)为后向弧。

  • 割的容量

    对于容量网络 G ( V , E ) G(V,E) G(V,E)和其s-t割 ( S 1 , S 2 ) (S1,S2) (S1,S2),令其容量为所有前向弧容量总和,记为 c ( S 1 , S 2 ) c(S1,S2) c(S1,S2)

  • 最小割

    容量最小的s-t割。

给定容量网络,求其最小割的容量。

定理

结论

最小割的容量和最大流的流量相等。

证明

有引理:对于每个割,通过其中所有弧的流量之和是恒定的

证明:显然,割中弧要阻拦住所有流量,所以对于所有割,通过其中所有弧的流量之和就是 S S S的净流出量。

又有流量小于等于容量,所以任意流流量都会小于等于任意割的容量,对于最小割和最大流,就是等于。

实现

我们尝试求出 S 1 S1 S1,过程分为两步:

  • 求出最大流;
  • 在此时残量网络中搜索,能在流量非零条件下访问到的即为 S 1 S1 S1中的点。

这里注意一个地方,最小割中的弧一定满流,但满流弧不一定属于最小割

#include <bits/stdc++.h>
using namespace std;
const int N = 105, M = 10005;
struct edge {
	int v, c, nxt;
} e[M];
int head[N], cnt;
inline void add(int u, int v, int c) {
	e[++cnt] = {v, c, head[u]}, head[u] = cnt;
}
inline void adde(int u, int v, int c) {
	add(u, v, c), add(v, u, 0);
}
int S, T, d[N];
bool bfs() {
	memset(d, -1, sizeof d);
	queue<int> q;
	q.push(S);
	d[S] = 0;
	while (q.size()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (d[v] == -1 && e[i].c) {
				q.push(v);
				d[v] = d[u] + 1;
			}
		}
	}
	return d[T] != -1;
}
int dfs(int u, int f) {
	if (u == T) {
		return f;
	}
	int res = 0;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (d[v] == d[u] + 1 && e[i].c) {
			int t = dfs(v, min(f, e[i].c));
			e[i].c -= t;
			e[i ^ 1].c += t;
			res += t;
			f -= t;
			if (!f) {
				break;
			}
		}
	}
	if (res == 0) {
		d[u] = -1;
	}
	return res;
}
int Dinic() {
	int res = 0;
	while (bfs()) {
		res += dfs(S, 1e9);
	}
	return res;
}
vector<int> v;
bool vis[N];
void dfs_cut(int u) {
	vis[u] = 1;
	v.push_back(u);
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (!vis[v] && e[i].c) {
			dfs_cut(v);
		}
	}
}
void min_cut() {
	dfs_cut(S);
	for (int i : v) {
		cout << i << ' ';
	}
}
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int n, m;
	cin >> n >> m;
	while (m--) {
		int u, v, f;
		cin >> u >> v >> f;
		adde(u, v, f);
	}
	cin >> S >> T;
	cout << Dinic() << '\n';
	min_cut();
	return 0;
}

费用流

问题

给每条弧加上一个单位流量的费用,要求在最大流的基础上使得流量总和最小或最大。

分析

这里我们考虑基于EK实现,此时Dinic并不见得比EK更优。

算法思想:我们考虑给每条边加上一个边权,求解最短路的过程就变成求解最短或最长增广路,因为增广顺序对最大流并没有影响,我们发现这个过程可以在最大流的前提下求出最优代价,这个过程一般使用SPFA实现,其余步骤不变。

##实现

以最小费用最大流为例。

#include <bits/stdc++.h>
using namespace std;
const int N = 1005, M = 10005;
struct edge {
	int v, c, w, nxt;
} e[M];
int head[N], S, T, cnt;
inline void add(int u, int v, int c, int w) {
	e[cnt] = {v, c, w, head[u]}, head[u] = cnt++;
}
inline void adde(int u, int v, int c, int w) {
	add(u, v, c, w), add(v, u, 0, -w);	// 这里注意反向边要用负代价
}
bool vis[N];
int d[N], E[N];	// 这里注意E是基于最短路的
bool spfa() {	// 返回值:是否可以从S走到T
	memset(vis, 0, sizeof vis);
	memset(d, 0x3f, sizeof d);
	memset(E, -1, sizeof E);
	d[S] = 0;
	vis[S] = 1;
	queue<int> q;
	q.push(S);
	while (q.size()) {
		int u = q.front(); q.pop();
		vis[u] = 0;
		for (int i = head[u]; ~i; i = e[i].nxt) {
			if (e[i].c) {
				int v = e[i].v;
				if (d[u] + e[i].w < d[v]) {
					d[v] = d[u] + e[i].w;
					E[v] = i;
					if (!vis[v]) {
						q.push(v);
						vis[v] = 1;
					}
				}
			}
		}
	}
	return E[T] != -1;
}
int costflow() {
	int res = 0;
	while (spfa()) {
		int del = 1e9;
		for (int i = T; i != S; i = e[E[i] ^ 1].v) {	// 为什么遍历的下一个是反向边呢,因为这里写的是v
			del = min(e[E[i]].c, del);
		}
		for (int i = T; i != S; i = e[E[i] ^ 1].v) {
			e[E[i]].c -= del;
			e[E[i] ^ 1].c += del;
			res += e[E[i]].w * del;
		}
	}
	return res;
}
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	memset(head, -1, sizeof head);
	int n, m;
	cin >> n >> m;
	while (m--) {
		int u, v, c, w;
		cin >> u >> v >> c >> w;
		adde(u, v, c, w);
	}
	cin >> S >> T;
	cout << costflow() << '\n';
	return 0;
}

技巧:拆点

方法

我们有时会希望通过一个的流量不超过一个定值 v a l val val,那该怎么办呢?

考虑将点 u u u拆成两个点 u ′ u' u u ′ ′ u'' u′′,原图中连向 u u u的弧连向 u ′ u' u,从 u u u连出的弧从 u ′ ′ u'' u′′连出,再从 u ′ u' u u ′ ′ u'' u′′连一条容量为 v a l val val的弧即可,相当于若流入 u ′ u' u的流量 > v a l >val >val,通过 u ′ u' u连出的唯一的容量为 v a l val val的弧时,就不合法了。

例题

题意: n n n个人,两种物品,分别有 m 1 m1 m1 m 2 m2 m2种,每种一个,每个人能拿一个物品1和一个物品2,每个人对两种物品都分别有一个喜欢列表,问最多多少人能同时选到喜欢的物品1和喜欢的物品2。( 1 ≤ n , m 1 , m 2 ≤ 100 1\le n,m1,m2\le 100 1n,m1,m2100)

我们考虑以 S → 物 1 → 人 → 物 2 → T S\to 物1\to 人\to 物2\to T S12T的方式建立网络,所有弧容量均为 1 1 1

但是此时明显可能有一个人选多个物的情况,对答案会造成影响。则我们可以按照上述方法限制点流,将网络结构改成 S → 物 1 → 人 ′ → 人 ′ ′ → 物 2 → T S\to 物1\to 人'\to 人''\to 物2\to T S1′′2T,其中每个 人 ′ 人' 连向对应的 人 ′ ′ 人'' ′′一条容量为 1 1 1的弧,再跑最大流就好了。

#include <bits/stdc++.h>
using namespace std;
const int N = 105, M = 10005;
struct edge {
	int v, c, nxt;
} e[M];
int head[N], cnt;
inline void add(int u, int v, int c) {
	e[++cnt] = {v, c, head[u]}, head[u] = cnt;
}
inline void adde(int u, int v, int c) {
	add(u, v, c), add(v, u, 0);
}
int S, T, d[N];
bool bfs() {
	memset(d, -1, sizeof d);
	queue<int> q;
	q.push(S);
	d[S] = 0;
	while (q.size()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (d[v] == -1 && e[i].c) {
				d[v] = d[u] + 1;
				q.push(v);
			}
		}
	}
	return d[T] != -1;
}
int dfs(int u, int f) {
	if (u == T) {
		return f;
	}
	int res = 0;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (d[v] == d[u] + 1 && e[i].c) {
			int t = dfs(v, min(f, e[i].c));
			e[i].c -= t;
			e[i ^ 1].c += t;
			res += t;
			f -= t;
			if (!f) {
				break;
			}
		}
	}
	return res;
}
int Dinic() {
	int res = 0;
	while (bfs()) {
		res += dfs(S, 1e9);
	}
	return res;
}
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int n, m1, m2;
	cin >> n >> m1 >> m2;
	for (int i = 1; i <= n; ++i) {
		adde(m1 + i, m1 + n + i, 1);	// 人' -> 人''
		int k1, k2, x;	// 两种物品个数
		cin >> k1 >> k2;
		while (k1--) {
			cin >> x;
			adde(x, m1 + i, 1);	// 物1 -> 人'
		}
		while (k2--) {
			cin >> x;
			adde(m1 + n + i, m1 + 2 * n + x, 1);	// 人'' -> 物2
		}
	}
	S = 0;
	T = m1 + m2 + 2 * n + 1;
	for (int i = 1; i <= m1; ++i) {
		adde(S, i, 1);	// S -> 物1
	}
	for (int i = 1; i <= m2; ++i) {
		adde(m1 + 2 * n + i, T, 1);	 // 物2 -> T
	}
	cout << Dinic() << '\n';
	return 0;
}

应用:混合图欧拉回路

问题

混合图,即有向边和无向边都有的图;欧拉路径,即一笔画最终回到起点。

分析

回顾一下之前有向图的结论:若图中每个点的入度和出度均相等,图中存在欧拉回路

然鹅,对于混合图中的无向边,我们不知道其方向,我们的主要难点就是给其定向。

考虑随机定向再改方向,设随机定向后第 i i i个点的入度为 i n d i ind_i indi,出度为 o u t d i outd_i outdi,则显然可以把所有点分为三类:

  1. i n d i > o u t d i ind_i>outd_i indi>outdi
  2. i n d i < o u t d i ind_i<outd_i indi<outdi
  3. i n d i = = o u t d i ind_i==outd_i indi==outdi

第三类显然不用管。对于第一类,我们需要把 i n d i − o u t d i 2 \frac{ind_i-outd_i}{2} 2indioutdi条指向 i i i的边翻转,第二类同理,则若存在 ∣ i n d i − o u t d i ∣ m o d    2 = = 1 |ind_i-outd_i|\mod 2==1 indioutdimod2==1个点,必然不存在欧拉回路,然后考虑网络流。

建立源点 S S S和汇点 T T T

  • 对于所有 i n d i > o u t d i ind_i>outd_i indi>outdi,从 S S S i i i连一条容量为 i n d i − o u t d i 2 \frac{ind_i-outd_i}{2} 2indioutdi的弧;
  • 对于所有 i n d i < o u t d i ind_i<outd_i indi<outdi,从 i i i T T T连一条容量为 o u t d i − i n d i 2 \frac{outd_i-ind_i}{2} 2outdiindi的弧;
  • 原图中的有向边因为方向固定,不用管;
  • 对于随机定向的弧 < u , v > <u,v> <u,v>,从 v v v u u u连一条容量为 1 1 1的弧。

这里容量的含义想必大家已经懂了,就是最多能改方向的弧的条数。那么,当从 S S S发出的弧均为满流,即 f m a x = ∑ i n d i > o u t d i i n d i − o u t d i 2 f_{max}=\sum_{ind_i>outd_i}{\frac{ind_i-outd_i}{2}} fmax=indi>outdi2indioutdi时,也就是所有需要改方向的弧都改了,存在欧拉回路,欧拉回路即为在随机定向的基础上将最大流中流量为 1 1 1的弧反过来后的图。

实现

#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 105, M = 10005;
struct edge {
	int v, c, nxt;
} e[M];
int head[N], cnt;
inline void add(int u, int v, int c) {
	e[++cnt] = {v, c, head[u]}, head[u] = cnt;
}
inline void adde(int u, int v, int c) {
	add(u, v, c), add(v, u, 0);
}
int S, T, d[N];
bool bfs() {
	memset(d, -1, sizeof d);
	queue<int> q;
	q.push(S);
	d[S] = 0;
	while (q.size()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (d[v] == -1 && e[i].c) {
				q.push(v);
				d[v] = d[u] + 1;
			}
		}
	}
	return d[T] != -1;
}
int dfs(int u, int f) {
	if (u == T) {
		return f;
	}
	int res = 0;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (d[v] == d[u] + 1 && e[i].c) {
			int t = dfs(v, min(f, e[i].c));
			e[i].c -= t;
			e[i ^ 1].c += t;
			res += t;
			f -= t;
			if (!f) {
				break;
			}
		}
	}
	if (!res) {
		d[u] = -1;
	}
	return res;
}
int Dinic() {
	int res = 0;
	while (bfs()) {
		res += dfs(S, 1e9);
	}
	return res;
}
int ind[N], outd[N];
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int n, m;
	cin >> n >> m;
	while (--m) {
		int u, v, op;	// 这里 op == 0 表示无向边,op == 1表示有向边
		cin >> u >> v >> op;
		if (!op) {
			adde(v, u, 1);
		}
	}
	S = 0, T = n + 1;
	int sum = 0;
	for (int i = 1; i <= n; ++i) {
		int k = ind[i] - outd[i];
		if (k % 2 == 0) {
			if (k) {
				adde(S, i, k / 2);
				sum += k / 2;
			} else {
				adde(i, T, -k / 2);
			}
		} else {
			cout << "No\n";
			return 0;
		}
	}
	cout << (Dinic() == sum ? "Yes\n" : "No\n");
	return 0;
}

应用:最大权闭合图

问题

给定一个无边权但有点权的有向图,求其点权和最大且其中所有点的所有出边都指向其中点的子图。

分析

建立源点 S S S和汇点 T T T,将原图中边替换成容量为 ∞ \infty 的弧,从 S S S向所有正权点连一条容量为其权值的弧,从所有负权点向 T T T连一条容量为其权值绝对值的弧,此时的最大权闭合图的权值为所有正权点权值和减去最小割容量,具体子图即为在在网络中dfs,在流量不为 0 0 0的情况下能到达点的集合。

实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 5, M = 1e5 + 5;
struct edge {
	int v, c, nxt;
} e[M];
int head[N], cnt;
inline void add(int u, int v, int c) {
	e[++cnt] = {v, c, head[u]}, head[u] = cnt;
}
inline void adde(int u, int v, int c) {
	add(u, v, c), add(v, u, 0);
}
int S, T, d[N];
bool bfs() {
	memset(d, -1, sizeof d);
	queue<int> q;
	q.push(S);
	d[S] = 0;
	while (q.size()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (d[v] == -1 && e[i].c) {
				q.push(v);
				d[v] = d[u] + 1;
			}
		}
	}
	return d[T] != -1;
}
int dfs(int u, int f) {
	if (u == T) {
		return f;
	}
	int res = 0;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (d[v] == d[u] + 1 && e[i].c) {
			int t = dfs(v, min(f, e[i].c));
			e[i].c -= t;
			e[i ^ 1].c += t;
			res += t;
			f -= t;
			if (!f) {
				break;
			}
		}
	}
	if (!res) {
		d[u] = -1;
	}
	return res;
}
int Dinic() {
	int res = 0;
	while (bfs()) {
		res += dfs(S, 1e9);
	}
	return res;
}
bool vis[N];
void dfs1(int u) {
	vis[u] = 1;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (!vis[v] && e[i].c)  {
			dfs1(v);
		}
	}
}
int main() {
	ios :: sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	int n, m, sum = 0;
	cin >> n >> m;
	S = 0, T = n + 1;
	for (int i = 1; i <= n; ++i) {
		int w;
		cin >> w;
		if (w == 0) {
			continue;
		}
		if (w) {
			adde(S, i, w);
			sum += w;
		} else {
			adde(i, T, -w);
		}
	}
	while (m--) {
		int u, v;
		cin >> u >> v;
		adde(u, v, 1e9);
	}
	cout << sum - Dinic() << '\n';
	dfs1(S);
	for (int i = 1; i <= n; ++i) {
		if (vis[i]) {
			cout << i << ' ';
		}
	}
	return 0;
}

习题

[NOI2012] 美食节

题意

n n n种菜,每种菜 p i p_i pi个, m m m个厨师,第 i i i个厨师做第 j j j道菜用时 t i , j t_{i,j} ti,j,一道菜的等待时间为开始做到做完这道菜的用时,求所有菜的最小等待时间和。

分析

考虑费用流,要建立以下几种点:

  • 源点 S S S和汇点 T T T
  • 每道菜;
  • i i i和厨师做倒数第 j j j道菜

连以下几种边:

  • S S S到每道菜,容量为 p i p_i pi,费用为 0 0 0
  • 厨师做菜的状态到 T T T,容量为 1 1 1,费用为 0 0 0
  • i i i道菜到第 j j j个厨师做倒数第 k k k道菜的状态,容量为 1 1 1,费用为 t i , j × k t_{i,j}\times k ti,j×k,表示第 i i i道菜就是第 j j j个厨师做的倒数第 k k k道,则剩下 k k k道菜都要等 t i , j t_{i,j} ti,j的时间

这样连边发现边数很多,费用流明显会超时。

观察可发现,对于同样的第 i i i道菜和第 j j j个厨师,做菜的顺序肯定越靠后越好,后面没处理过的话绝对不会用到前面的,则先与 k = 1 k=1 k=1的状态连边,用到了再连就好。

洛谷P1361 小M的作物

题意

n n n个种子, A , B A,B A,B两个耕地,每个种子只能放在其中一个耕地中,各有一定收益,一些组合放在同一个耕地中会有额外收益,求收益最大值。

分析

先不考虑额外收益,则方案为:

  • 建立点 A A A B B B,每个种子建点;
  • A A A向每个种子连容量为这个种子放到耕地 A A A里的收益的弧,种子向 B B B连的同理;
  • 因为一个种子不能同时放到 A A A B B B,还要求收益最大,则考虑最小割,剩下部分即为答案

对于组合,我们建立一个点 u u u,代表将这个组合放到 A A A里的方案, v v v代表放到 B B B里的方案,向组合中每个点连容量为 ∞ \infty 的弧,因为不能割,再从 S S S u u u连容量为放到 A A A里收益的弧,从 v v v T T T连容量为放到 B B B里收益的弧即可。

这次关于网络流的学习就到这里,感谢收看。

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
很抱歉,我无法回答关于"计算机网络复习笔记 王道"的问题。我的回答只基于提供的引用内容。如果您有其他关于计算机网络的问题,我会很乐意帮助您回答。 #### 引用[.reference_title] - *1* [计算机网络笔记(王道考研) 第一章:计算机网络体系结构](https://blog.csdn.net/zimuzi2019/article/details/126634216)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [计算机网络笔记(王道考研) 第四章:网络层](https://blog.csdn.net/zimuzi2019/article/details/126634461)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [计算机网络笔记(王道考研) 第五章:传输层](https://blog.csdn.net/zimuzi2019/article/details/126634511)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蒟蒻一枚

谢谢鸭~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值