【Coel.学习笔记】费用流的含义与基础运用
基本含义
在一张流网络中,最大流是不唯一的。那么给每条边再加上一个费用值,所有最大流中费用和的极值就叫费用流。对应地,费用最小值为最小费用最大流,费用最大值为最大费用最大流。
算法内容
使用 EK 算法或 Dinic 算法,把 bfs 换成 SPFA 就可以求出最小费用最大流。
需要注意,当流网络上存在费用负环,那么最小费用最大流无法求出。这时需要使用消圈法删除负圈。
代码如下(使用 EK 算法):
// Problem: P3381 【模板】最小费用最大流
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3381
// Memory Limit: 128 MB
// Time Limit: 1000 ms
// Author: Coel
//
// Powered by CP Editor (https://cpeditor.org)
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int maxn = 5e5 + 10, inf = 1e8;
int n, m, S, T;
int head[maxn], nxt[maxn], to[maxn], c[maxn], w[maxn], cnt;
int d[maxn], pre[maxn], incf[maxn];
bool vis[maxn];
void add(int u, int v, int x, int y) {
nxt[cnt] = head[u], to[cnt] = v, c[cnt] = x, w[cnt] = y, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, w[cnt] = -y, head[v] = cnt++;
}
bool spfa() {
queue<int> Q;
memset(d, 0x3f, sizeof(d));
memset(incf, 0, sizeof(incf));
Q.push(S), d[S] = 0, incf[S] = inf;
while (!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = false;
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (c[i] && d[v] > d[u] + w[i]) {
d[v] = d[u] + w[i];
pre[v] = i;
incf[v] = min(c[i], incf[u]);
if (!vis[v]) {
Q.push(v);
vis[v] = true;
}
}
}
}
return incf[T] > 0;
}
void Edmond_Karp(int& flow, int& cost) {
flow = cost = 0;
while (spfa()) {
int t = incf[T];
flow += t, cost += t * d[T];
for (int i = T; i != S; i = to[pre[i] ^ 1]) {
c[pre[i]] -= t;
c[pre[i] ^ 1] += t;
}
}
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
memset(head, -1, sizeof(head));
cin >> n >> m >> S >> T;
for (int i = 1; i <= m; i++) {
int u, v, x, y;
cin >> u >> v >> x >> y;
add(u, v, x, y);
}
int flow, cost;
Edmond_Karp(flow, cost);
cout << flow << ' ' << cost;
return 0;
}
实战应用
下面给出几个比较简单的例题。
网络流 24 题:运输问题
洛谷传送门
有若干个仓库和零售商店,每个仓库拥有一定货物,每个商店需要一定货物,保证仓库存放的货物量等于商店需要的货物量,从不同仓库运到不同商店的运输费用各不相同。求出运输费用的最大值和最小值。
解析:这有点像多源多汇问题,建立一个源点与仓库相连,一个汇点与商店相连,容量等于供需量,费用等于零;接下来给每个仓库和商店连边,容量正无穷,费用等于运输花费,这个问题就变成了最小费用最大流问题。
由于要同时输出最大值和最小值,这里可以在做完最大值之后还原网络并跑一边负费用,就不需要再把 spfa 再写一遍了。
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
S = 0, T = m + n + 1;
memset(head, -1, sizeof(head));
for (int i = 1, x; i <= m; i++) {
cin >> x;
add(S, i, x, 0);
}
for (int i = 1, x; i <= n; i++) {
cin >> x;
add(m + i, T, x, 0);
}
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++) {
int cost;
cin >> cost;
add(i, m + j, inf, cost);
}
cout << Edmond_Karp() << '\n';
for (int i = 0; i < cnt; i += 2) {
c[i] += c[i ^ 1], c[i ^ 1] = 0;
w[i] = -w[i], w[i ^ 1] = -w[i ^ 1];
}
cout << -Edmond_Karp();
return 0;
}
网络流 24 题:负载平衡问题
转眼间网络流 24 题已经做完三分之一了……
洛谷传送门
有 \(n\) 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。如何用最少搬运量可以使 \(n\) 个仓库的库存数量相同。搬运货物时,只能在相邻的仓库之间搬运。
解析:这题实际上是一个贪心题,贪心做法略,留给读者锻炼思维。
费用流做法的话,把仓库分成两大类:存储量大于平均数,存储量小于平均数。
类似上题,把存储量大的与源点相连,存储量小的与汇点相连,容量等于存储量与平均数之差的绝对值。再给相邻的仓库连边,容量正无穷,费用等于 1。
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
S = 0, T = n + 1;
memset(head, -1, sizeof(head));
for (int i = 1; i <= n; i++) {
cin >> a[i];
ave += a[i];
add(i, i < n ? i + 1 : 1, inf, 1);
add(i, i > 1 ? i - 1 : n, inf, 1);
}
ave /= n;
for (int i = 1; i <= n; i++)
if (a[i] > ave)
add(S, i, a[i] - ave, 0);
else if (a[i] < ave)
add(i, T, ave - a[i], 0);
cout << Edmond_Karp();
return 0;
}
网络流 24 题:分配问题
洛谷传送门
有 \(n\) 件工作要分配给 \(n\) 个人做。第 \(i\) 个人做第 \(j\) 件工作产生的效益为 \(c_{ij}\) 。求出效益之和的最大值和最小值。
解析:网络流 24 题的题面都挺简洁明了的,看起来很舒适。
不难发现这是一个二分图,所以还是老套路。源点连人,汇点连工作,容量为 1;人和工作连边,费用为工作效益。还是一个最大费用最大流问题,直接做就行。
求最大值和最小值的套路和运输问题一样,不再赘述。
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
S = 0, T = n * 2 + 1;
memset(head, -1, sizeof(head));
for (int i = 1; i <= n; i++) {
add(S, i, 1, 0);
add(n + i, T, 1, 0);
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
int C;
cin >> C;
add(i, n + j, 1, C);
}
cout << Edmond_Karp() << '\n';
for (int i = 0; i < cnt; i += 2) {
c[i] += c[i ^ 1], c[i ^ 1] = 0;
w[i] = -w[i], w[i ^ 1] = -w[i ^ 1];
}
cout << -Edmond_Karp();
return 0;
}
网络流 24 题:数字梯形问题
洛谷传送门
梯形的第一行有 \(m\) 个数字。从梯形的顶部的 \(m\) 个数字开始,在每个数字处可以沿左下或右下方向移动,形成一条从梯形的顶至底的路径。分别遵守以下规则:
-
从梯形的顶至底的 \(m\) 条路径互不相交;
-
从梯形的顶至底的 \(m\) 条路径仅在数字结点处相交;
-
从梯形的顶至底的 \(m\) 条路径允许在数字结点相交或边相交。
对于给定的数字梯形,分别按照规则 \(1\),规则 \(2\),和规则 \(3\) 计算出从梯形的顶至底的 \(m\) 条路径,使这 \(m\) 条路径经过的数字总和最大。
解析:三个问,有点小麻烦。但无论是哪一个问,建立一个源点与最顶层连边,汇点与最底层连边,做最大费用最大流即可得到答案。
对于第一问,边只能走一次,就设容量为 1;点只能走一次,拆点即可。
对于第二问,把拆点得到边的容量改成正无穷,边保持不变。
对于第三问,边的容量也改为正无穷。
虽然看起来要写很多东西,但由于都是跑那几条边,直接复制几遍然后改一改就行了,调试难度也不是很大,如果问题不在加边上,调对一个问就可以解决所有问题~
顺带一提,这题要求最大值,所以 spfa 要跑最长路。改一下 dis 数组的初始化,再换一下松弛操作就行了。
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
S = ++idx, T = ++idx;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m + i - 1; j++) {
cin >> cost[i][j];
id[i][j] = ++idx;
}
// Rule 1st
memset(head, -1, sizeof(head));
cnt = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m + i - 1; j++) {
add(id[i][j] * 2, id[i][j] * 2 + 1, 1, cost[i][j]);
if (i == 1) add(S, id[i][j] * 2, 1, 0);
if (i == n) add(id[i][j] * 2 + 1, T, 1, 0);
if (i < n) {
add(id[i][j] * 2 + 1, id[i + 1][j] * 2, 1, 0);
add(id[i][j] * 2 + 1, id[i + 1][j + 1] * 2, 1, 0);
}
}
cout << Edmond_Karp() << endl;
// Rule 2nd
memset(head, -1, sizeof(head));
cnt = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m + i - 1; j++) {
add(id[i][j] * 2, id[i][j] * 2 + 1, inf, cost[i][j]);
if (i == 1) add(S, id[i][j] * 2, 1, 0);
if (i == n) add(id[i][j] * 2 + 1, T, inf, 0);
if (i < n) {
add(id[i][j] * 2 + 1, id[i + 1][j] * 2, 1, 0);
add(id[i][j] * 2 + 1, id[i + 1][j + 1] * 2, 1, 0);
}
}
cout << Edmond_Karp() << endl;
// Rule 3rd
memset(head, -1, sizeof(head));
cnt = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m + i - 1; j++) {
add(id[i][j] * 2, id[i][j] * 2 + 1, inf, cost[i][j]);
if (i == 1) add(S, id[i][j] * 2, 1, 0);
if (i == n) add(id[i][j] * 2 + 1, T, inf, 0);
if (i < n) {
add(id[i][j] * 2 + 1, id[i + 1][j] * 2, inf, 0);
add(id[i][j] * 2 + 1, id[i + 1][j + 1] * 2, inf, 0);
}
}
cout << Edmond_Karp() << endl;
return 0;
}