【学习笔记】网络最大流
更好的阅读体验
网络流简介 & 相关概念
网络流,属于图论算法,模型为一张有向无环图,然后各位需要知道以下要点:
-
源点:类似于这张图的起点,具体点可以看做一个自来水厂,它会源源不断向外送水,一般用
S 表示。 -
汇点:类似于这张图的终点,具体点可以看做一个住户家中,它会收到来自水厂的水,一般用
T 表示。 -
边:看做是水管。
-
容量:是边的边权,可以看做是水管的最大承受水量,每刻的水流量不能超过这个值。
-
流量:具体点就是当前时刻流过该边的水量。
-
残量网络:顾名思义,指的是剩余流量不为
0 的边所构成的网络。
网络最大流
首先我们再明确一点,一张网络图的流量定义某种路径设计方案下汇点的流量总和。
那么显而易见,网络最大流就是所有路径设计方案中流量总和最大的那种。
首先相信在座的各位同学肯定能够想到我们可以每次 dfs 找一条路径从
每次找到一条增广路,得到路径上最小流量后修改每条边的流量,直到再也找不到增广路后该网络流量来到最大。
但很明显,这个做法时间复杂度会爆炸,而且有的时候我们需要反悔(有可能存在一条更优的增广路遍历顺序),考虑如何优化。
EK 算法
EK 算法的核心就是利用初始流量为空的反向边进行一个悔的反,首先通过 bfs 找到经过边数最少的增广路,然后找出最小的流量
Dinic 算法
你发现 EK 算法在稠密图中直接寄掉,那么我们需要进一步的优化。
EK 算法每次会遍历整个残量网络但是最后只会找出一条增广路,那么我们有没有办法一次性找多条呢?
首先我们用按照每个点到
然后再引入当前弧优化,我们发现我们仍旧会遍历已经被榨干的边,那么我记录从当前点出发没有被榨干的边从其开始向下遍历,这样也会省去很多时间,最坏复杂度
对于二分图 Dinic 的复杂度为
只需要掌握以上两种算法可以解决大部分问题(另外还有两种更快的算法)。
P3376 【模板】网络最大流
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int N = 1e4 + 10;
constexpr int inf = 0x7f7f7f7f;
int n, m, S, T, idx, ans;
int head[N], cur[N], depth[N];
struct graph {
int to;
int nxt;
int val;
} E[N << 1];
void addedge (int u, int v, int w) {
E[idx].to = v;
E[idx].val = w;
E[idx].nxt = head[u];
head[u] = idx++;
}
bool bfs() {
memset (depth, 0, sizeof(depth));
queue<int> q;
q.emplace(S);
depth[S] = 1;
while (!q.empty()) {
int now = q.front(); q.pop();
for (int i = head[now]; ~i; i = E[i].nxt) {
int to = E[i].to, val = E[i].val;
if (val && !depth[to]) {
depth[to] = depth[now] + 1;
q.emplace(to);
}
}
}
return !depth[T] ? false : true;
}
int dfs (int now, int flow) {
if (now == T) return flow;
int sum = 0;
for (int i = cur[now]; ~i; i = E[i].nxt) {
cur[now] = i;
int to = E[i].to, val = E[i].val;
if (val && depth[to] == depth[now] + 1) {
int tmpv = dfs (to, min(flow, val));
E[i].val -= tmpv;
E[i ^ 1].val += tmpv;
sum += tmpv, flow -= tmpv;
if (!flow) break;
}
}
if (!sum)
depth[now] = 0;
return sum;
}
void dinic() {
while (bfs()) {
memcpy (cur, head, sizeof(cur));
ans += dfs (S, inf);
}
return;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> S >> T;
memset (head, -1, sizeof(head));
for (int i = 1; i <= m; i ++) {
int u, v, w;
cin >> u >> v >> w;
addedge (u, v, w), addedge (v, u, 0);
}
dinic();
cout << ans << '\n';
return 0;
}
下面来几道例题。
Ex.1 P3254 圆桌问题
记源点汇点为
看上去很二分图啊,直接连
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int N = 1e5 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;
int m, n, idx, sum, S, T;
int head[N], cur[N], dis[N];
struct graph {
int to;
int val;
int nxt;
} E[N];
void addedge (int u, int v, int w) {
E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], head[u] = idx++;
E[idx].to = u, E[idx].val = 0, E[idx].nxt = head[v], head[v] = idx++;
}
bool bfs() {
queue<int> q;
memcpy (cur, head, sizeof(int) * (T + 1));
memset (dis, 0x3f, sizeof(int) * (T + 1));
q.emplace(S), dis[S] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val;
if (w && dis[v] == inf) {
dis[v] = dis[u] + 1;
q.emplace(v);
}
}
}
return dis[T] != inf;
}
int dfs (int u, int flow) {
if (u == T)
return flow;
int res = 0;
for (int i = cur[u]; ~i; i = E[i].nxt) {
cur[u] = i;
int v = E[i].to, w = E[i].val;
if (w && dis[v] == dis[u] + 1) {
int tmpv = dfs (v, min(w, flow));
E[i].val -= tmpv;
E[i ^ 1].val += tmpv;
res += tmpv;
flow -= tmpv;
if (!flow) break;
}
}
if (!res)
dis[u] = inf;
return res;
}
bool dinic() {
int ans = 0;
while (bfs())
ans += dfs(S, inf);
return ans == sum;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> m >> n, T = n + m + 1;
memset (head, -1, sizeof(int) * (T + 1));
for (int i = 1, w; i <= m; i ++)
cin >> w, addedge (S, i, w), sum += w;
for (int i = 1, w; i <= n; i ++)
cin >> w, addedge (i + m, T, w);
for (int i = 1; i <= m; i ++) {
for (int j = 1; j <= n; j ++)
addedge (i, m + j, 1);
}
if (dinic()) {
cout << "1\n";
for (int u = 1; u <= m; u ++) {
for (int i = head[u]; ~i; i = E[i].nxt) {
if (E[i].to != S && !E[i].val)
cout << E[i].to - m << ' ';
}
cout << '\n';
}
} else {
cout << "0\n";
}
return 0;
}
Ex.2 P2762 太空飞行计划问题
超级源连实验,流量为赞助商费用,仪器连超级汇,流量为配置仪器费用,中间实验连仪器,流量
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int N = 1e3 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;
int m, n, idx, S, T, ans;
char buf[505];
int head[N], cur[N], dis[N];
struct graph {
int to;
int val;
int nxt;
} E[N << 1];
void addedge (int u, int v, int w) {
E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], head[u] = idx++;
E[idx].to = u, E[idx].val = 0, E[idx].nxt = head[v], head[v] = idx++;
}
bool bfs() {
queue<int> q;
memset (dis, 0x3f, sizeof(dis));
memcpy (cur, head, sizeof(head));
q.emplace(S), dis[S] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val;
if (w && dis[v] == inf) {
dis[v] = dis[u] + 1;
q.emplace(v);
}
}
}
return dis[T] != inf;
}
int dfs (int u, int flow) {
if (u == T)
return flow;
int sum = 0;
for (int i = cur[u]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val; cur[u] = i;
if (w && dis[v] == dis[u] + 1) {
int tmpv = dfs (v, min(w, flow));
E[i].val -= tmpv;
E[i ^ 1].val += tmpv;
sum += tmpv;
flow -= tmpv;
if (!flow) break;
}
}
if (!sum)
dis[u] = inf;
return sum;
}
void dinic() {
while (bfs())
ans -= dfs(S, inf);
for (int i = 1; i <= m; i ++) if (dis[i] != inf) cout << i << ' ';
cout << '\n';
for (int i = 1; i <= n; i ++) if (dis[m + i] != inf) cout << i << ' ';
cout << '\n' << ans << '\n';
return;
}
signed main() {
scanf ("%lld %lld", &m, &n), T = n + m + 1;
memset (head, -1, sizeof(head));
for (int i = 1; i <= m; i ++) {
int cost, x;
scanf ("%lld", &cost);
ans += cost;
addedge (S, i, cost);
cin.getline (buf, 500);
int ulen = 0;
while (sscanf (buf + ulen, "%lld", &x) == 1) {
addedge (i, x + m, inf);
if (!x)
ulen++;
while (x) {
ulen++;
x /= 10;
}
ulen++;
}
}
for (int i = 1, w; i <= n; i ++) {
scanf ("%lld", &w);
addedge (m + i, T, w);
}
dinic();
return 0;
}
Ex.3 P2764 最小路径覆盖问题
那么路径的终点出度一定为
方案的输出的话直接 dfs 出边对应在同一集合内的点,但是你发现这样会被最后链的点卡掉,此时我们需要并查集合并满足条件边的两端点然后 dfs 未匹配点。
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int N = 2e4 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;
int n, m, idx, S, T;
int head[N], cur[N], dis[N], fa[N];
bool vst[N];
struct graph {
int from;
int to;
int val;
int nxt;
} E[N];
int find (int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge (int x, int y) {
x = find(x), y = find(y);
if (x != y) fa[x] = y;
}
void addedge (int u, int v, int w) {
E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], E[idx].from = u, head[u] = idx++;
E[idx].to = u, E[idx].val = 0, E[idx].nxt = head[v], E[idx].from = v, head[v] = idx++;
}
bool bfs() {
queue<int> q;
memset (dis, 0x3f, sizeof(dis));
memcpy (cur, head, sizeof(head));
q.emplace(S), dis[S] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val;
if (w && dis[v] == inf) {
dis[v] = dis[u] + 1;
q.emplace(v);
}
}
}
return dis[T] != inf;
}
int dfs (int u, int flow) {
if (u == T)
return flow;
int sum = 0;
for (int i = cur[u]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val; cur[u] = i;
if (w && dis[v] == dis[u] + 1) {
int tmpv = dfs (v, min(w, flow));
E[i].val -= tmpv;
E[i ^ 1].val += tmpv;
sum += tmpv;
flow -= tmpv;
if (!flow) break;
}
}
if (!sum)
dis[u] = inf;
return sum;
}
void print (int u) {
if (vst[u])
return;
vst[u] = true;
cout << u << ' ';
for (int i = head[u]; ~i; i = E[i].nxt) {
int v = E[i].to;
if (v > n && v < T) {
if (!E[i].val)
print (v - n);
}
}
}
void dinic() {
int ans = 0;
while (bfs())
ans += dfs(S, inf);
for (int i = 0; i < idx; i ++) {
int u = E[i].from, v = E[i].to;
if (u >= 1 && u <= n) {
if (v > n && v < T) {
if (!E[i].val)
merge (v - n, u);
}
}
}
for (int i = 1; i <= n; i ++) {
if (fa[i] == i) {
print(i);
cout << '\n';
}
}
cout << n - ans << '\n';
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
T = 2 * n + 1;
memset (head, -1, sizeof(head));
for (int i = 1; i <= m; i ++) {
int u, v;
cin >> u >> v;
addedge (u, v + n, 1);
}
for (int i = 1; i <= n; i ++)
addedge (S, i, 1), addedge (n + i, T, 1);
for (int i = 1; i < T; i ++) fa[i] = i;
dinic();
return 0;
}
最小割
记原图边集为
引:最大流最小割定理
指网络的最大流的值等于最小割。
意会一下就是要使选出边集的容量之和最小,那么每条增广路一定是选容量最小的那条边,与最大流原理相同。
Ex. P1344 [USACO4.4] 追查坏牛奶 Pollutant Control
最小割模板。
但是要求割边数量,此时我们发现
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int base = 2025;
constexpr int N = 2e3 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;
int head[N], cur[N], dis[N];
int n, m, idx, anst, ansf, S, T;
struct graph {
int to;
int nxt;
int val;
} E[N];
void addedge (int u, int v, int w) {
E[idx].to = v;
E[idx].val = w;
E[idx].nxt = head[u];
head[u] = idx++;
}
bool bfs() {
queue<int> q;
memcpy (cur, head, sizeof(head));
memset (dis, 0x3f, sizeof(dis));
q.emplace(S), dis[S] = 0;
while (!q.empty()) {
int now = q.front(); q.pop();
for (int i = head[now]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val;
if (w && dis[v] == inf) {
dis[v] = dis[now] + 1;
q.emplace(v);
}
}
}
return dis[T] != inf;
}
int dfs (int now, int flow) {
if (now == T)
return flow;
int sum = 0;
for (int i = cur[now]; ~i; i = E[i].nxt) {
int v = E[i].to, w = E[i].val;
cur[now] = i;
if (w && dis[v] == dis[now] + 1) {
int tmpv = dfs (v, min(w, flow));
E[i].val -= tmpv;
E[i ^ 1].val += tmpv;
sum += tmpv;
flow -= tmpv;
if (!flow) break;
}
}
if (!sum)
dis[now] = inf;
return sum;
}
void dinic() {
while (bfs()) {
int tmpv = dfs(S, inf);
ansf += tmpv / base;
anst += tmpv % base;
}
return;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
S = 1, T = n;
memset (head, -1, sizeof(head));
for (int i = 1; i <= m; i ++) {
int u, v, w;
cin >> u >> v >> w;
addedge (u, v, w * base + 1);
addedge (v, u, 0);
}
dinic();
cout << ansf << ' ' << anst << '\n';
return 0;
}
费用流
顾名思义,费用流指在边上除容量外另增加一个权值为费用表示流过这条边的单位流量所需支付的费用,其实不难发现,Dinic 算法的本质是按照层数从小到大进行 dfs,费用就是相当于把层数换成最短路径长度,把 bfs 换成 spfa 就行,如果要求最大流时的费用,那么就是最大流乘上到
当然费用流分为最小费用最大流和最大费用最大流(分别对应最短路和最长路)。
Ex.1 P3381 【模板】最小费用最大流
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int N = 1e5 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;
int n, m, S, T, idx, ansc, ansf;
int head[N], cur[N], dis[N];
bool vst[N], pass[N];
struct graph {
int to;
int nxt;
int val;
int cost;
} E[N];
void addedge (int u, int v, int w, int c) {
E[idx].to = v, E[idx].val = w, E[idx].nxt = head[u], E[idx].cost = c, head[u] = idx++;
}
bool spfa() {
queue<int> q;
memset (dis, 0x3f, sizeof(dis));
memset (vst, false, sizeof(vst));
q.emplace(S);
dis[S] = 0, vst[S] = true;
while (!q.empty()) {
int now = q.front(); q.pop();
vst[now] = false;
for (int i = head[now]; ~i; i = E[i].nxt) {
int to = E[i].to, val = E[i].val, cost = E[i].cost;
if (val && dis[to] > dis[now] + cost) {
dis[to] = dis[now] + cost;
if (!vst[to]) {
vst[to] = true;
q.emplace(to);
}
}
}
}
return dis[T] != inf;
}
int dfs (int now, int flow) {
if (now == T)
return flow;
pass[now] = true; // 只遍历没有访问过的点
int sum = 0;
for (int i = cur[now]; ~i; i = E[i].nxt) {
cur[now] = i;
int to = E[i].to, val = E[i].val, cost = E[i].cost;
if (!pass[to] && val && dis[to] == dis[now] + cost) {
int tmpv = dfs (to, min(val, flow));
E[i].val -= tmpv, E[i ^ 1].val += tmpv;
sum += tmpv, flow -= tmpv;
if (!flow) break;
}
}
pass[now] = false;
if (!sum)
dis[now] = inf;
return sum;
}
void dinic() {
while (spfa()) {
memcpy (cur, head, sizeof(head));
int res = dfs (S, inf);
ansf += res, ansc += res * dis[T];
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> S >> T;
memset (head, -1, sizeof(head));
for (int i = 1; i <= m; i ++) {
int u, v, w, c;
cin >> u >> v >> w >> c;
addedge (u, v, w, c), addedge (v, u, 0, -c); // 反边花费为相反数
}
dinic();
cout << ansf << ' ' << ansc << '\n';
return 0;
}
Ex.2 P4014 分配问题
工作和人拉出来成二分图,然后连边,工作和人之间连费用为
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
constexpr int N = 1e4 + 10;
constexpr int inf = 0x3f3f3f3f3f3f3f3f;
int n, idx, S, T;
int head[N], cur[N], dis[N];
bool vst[N], pass[N];
struct graph {
int to;
int val;
int nxt;
int cost;
} E1[N << 1], E2[N << 1];
void addedge (int u, int v, int w, int c) {
E1[idx].to = v, E1[idx].val = w, E1[idx].cost = c, E1[idx].nxt = head[u], head[u] = idx++;
E1[idx].to = u, E1[idx].val = 0, E1[idx].cost = -c, E1[idx].nxt = head[v], head[v] = idx++;
}
bool spfa (bool opt) {
queue<int> q;
memset (vst, false, sizeof(vst));
memcpy (cur, head, sizeof(head));
memset (dis, opt ? 0x3f : ~0x3f, sizeof(dis));
q.emplace(S), vst[S] = true, dis[S] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
vst[u] = false;
for (int i = head[u]; ~i; i = E2[i].nxt) {
int v = E2[i].to, w = E2[i].val, c = E2[i].cost;
if (opt) {
if (dis[v] > dis[u] + c && w) {
dis[v] = dis[u] + c;
if (!vst[v]) {
vst[v] = true;
q.emplace(v);
}
}
} else {
if (dis[v] < dis[u] + c && w) {
dis[v] = dis[u] + c;
if (!vst[v]) {
vst[v] = true;
q.emplace(v);
}
}
}
}
}
return opt ? dis[T] != inf : dis[T] != ~inf;
}
int dfs (int u, int flow, bool opt) {
if (u == T) return flow;
int sum = 0;
pass[u] = true;
for (int i = cur[u]; ~i; i = E2[i].nxt) {
int v = E2[i].to, w = E2[i].val, c = E2[i].cost;
cur[u] = i;
if (dis[v] == dis[u] + c && w && !pass[v]) {
int tmpv = dfs (v, min (w, flow), opt);
E2[i].val -= tmpv;
E2[i ^ 1].val += tmpv;
sum += tmpv;
flow -= tmpv;
if (!flow) break;
}
}
pass[u] = false;
if (!sum) dis[u] = opt ? inf : -inf;
return sum;
}
void dinic (bool opt) {
int ans = 0;
while (spfa(opt))
ans += dfs (S, inf, opt) * dis[T];
cout << ans << '\n';
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
T = n << 1 | 1;
memset (head, -1, sizeof(head));
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n; j ++) {
int x; cin >> x;
addedge (j, n + i, 1, x);
}
}
for (int i = 1; i <= n; i ++)
addedge (S, i, 1, 0);
for (int i = n + 1; i < T; i ++)
addedge (i, T, 1, 0);
memcpy (E2, E1, sizeof(E1)), dinic(true);
memcpy (E2, E1, sizeof(E1)), dinic(false);
return 0;
}
结语
网络流其实最难的就是如何建模(建图),建出图来就都好说了,也有可能和分层图网络流。
习题
网络流 24 题。
list No.1。
list No.2。