文章目录
网络流是图论中的经典问题,应用非常广泛,思想并不难理解,但实现部分大部分文章都不够详细,导致蒟蒻君学的时候就看了很长时间,这篇文章会较为详细的阐述细节部分。
最大流
问题
有向带权图中,每一条弧(注意这里不能叫边…) < 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,T∈V,表示源点和汇点;对于 ∀ < 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)的集合;
- 可行流:可行的网络流;
- 最大流:总流量最大的可行流。
那可行流具体要满足什么条件呢?
- 对于 ∀ < 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) 0≤f(u,v)≤c(u,v);
- 对于 u ∈ V u\in V u∈V,有 从 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=Vt→∣f∣→0→−∣f∣
对于任何容量网络,总是存在可行流,如全为零流。
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=V−S1两部分,我们用 ( S 1 , S 2 ) (S1,S2) (S1,S2)来表示一个割。
-
s − t s-t s−t割
对于一个割 ( S 1 , S 2 ) (S1,S2) (S1,S2),若源点 S ∈ S 1 S\in S1 S∈S1,汇点 T ∈ S 2 T\in S2 T∈S2,则称 ( 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>(u∈S1,v∈S2)为前向弧, < u , v > ( u ∈ S 2 , v ∈ S 1 ) <u,v>(u\in S2,v\in S1) <u,v>(u∈S2,v∈S1)为后向弧。
-
割的容量
对于容量网络 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 1≤n,m1,m2≤100)
我们考虑以 S → 物 1 → 人 → 物 2 → T S\to 物1\to 人\to 物2\to T S→物1→人→物2→T的方式建立网络,所有弧容量均为 1 1 1。
但是此时明显可能有一个人选多个物的情况,对答案会造成影响。则我们可以按照上述方法限制点流,将网络结构改成 S → 物 1 → 人 ′ → 人 ′ ′ → 物 2 → T S\to 物1\to 人'\to 人''\to 物2\to T S→物1→人′→人′′→物2→T,其中每个 人 ′ 人' 人′连向对应的 人 ′ ′ 人'' 人′′一条容量为 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,则显然可以把所有点分为三类:
- i n d i > o u t d i ind_i>outd_i indi>outdi;
- i n d i < o u t d i ind_i<outd_i indi<outdi;
- 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} 2indi−outdi条指向 i i i的边翻转,第二类同理,则若存在 ∣ i n d i − o u t d i ∣ m o d 2 = = 1 |ind_i-outd_i|\mod 2==1 ∣indi−outdi∣mod2==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} 2indi−outdi的弧;
- 对于所有 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} 2outdi−indi的弧;
- 原图中的有向边因为方向固定,不用管;
- 对于随机定向的弧 < 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>outdi2indi−outdi时,也就是所有需要改方向的弧都改了,存在欧拉回路,欧拉回路即为在随机定向的基础上将最大流中流量为 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里收益的弧即可。
这次关于网络流的学习就到这里,感谢收看。