最小费用最大流
代码模板最小费用最大流的原理与实现:每次找费用最小的增广路,流量优先、费用最小。
一句话算法
最小费用最大流就是每次在残量网络里找一条费用最小的增广路,先把流量做到最大,再让总费用尽量小。
问题模型
给定一个有向网络
- 容量
:这条边最多能通过多少流量。 - 单位费用
:每通过 单位流量要付出的费用。
如果边
目标是从源点
最小费用最大流
在所有最大流中,总费用最小的那一个流,称为最小费用最大流。
注意顺序:先最大流,再最小费用。不是为了少花钱而主动少送流。
核心直觉
普通最大流只关心“还能不能送更多流”。费用流还要关心“这次送流走哪条路最便宜”。
因此我们在残量网络里做两件事:
- 找一条从
到 的最短路,边权是费用。 - 沿着这条路尽量增广。
反向边是费用流的关键。若正向边费用为
它表示:如果后面发现之前的选择不够好,可以沿反向边把这段流退回去,同时退回之前支付的费用。
残量网络
对每条原始边:
u -> v, capacity = cap, cost = cost
建两条边:
u -> v, capacity = cap, cost = cost
v -> u, capacity = 0, cost = -cost
当沿 f 单位流量后:
- 正向边剩余容量减少
f。 - 反向边剩余容量增加
f。
如果以后走反向边
算法步骤
SPFA 版最小费用最大流的步骤:
-
建立残量网络,每条边加一条费用相反的反向边。
-
在残量网络中,用 SPFA 找从
到 的最短费用路。 -
若找不到路,说明不能继续增广,算法结束。
-
沿最短路找到可增广的最小剩余容量
pushed。 -
总流量增加
pushed。 -
总费用增加:
-
沿路径更新正向边和反向边容量。
-
回到第 2 步。
算法证明
为什么反向边费用是负数
如果之前沿
后来沿反向边
因此反向边费用必须是:
这样残量网络里的路径费用变化,才和真实总费用变化一致。
为什么每次找最短增广路
关键不变量:当前流量大小固定时,算法维护的是该流量下费用最小的流。
从当前流量增加到更大流量时,需要在残量网络中找一条增广路。残量网络中一条路径的费用,正好表示“把当前流改造成新流时,总费用的增量”。
选择最短增广路,就是选择当前这次增加流量的最小费用增量。
反向边允许路径撤销之前的局部选择,因此算法不只是贪心固定旧路径,而是在残量网络中重新调整整个流。
当残量网络中不存在
复杂度分析
设点数为
SPFA 每次最坏
实际竞赛中,SPFA 版容易写、能处理负费用边,但在大数据上可能被卡。更稳定的做法是使用势能函数把费用重标号,再用 Dijkstra 找最短路。
本文代码选择 SPFA 版作为入门模板。
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <bits/stdc++.h>
using namespace std;
struct MinCostMaxFlow {
struct Edge {
int to;
int rev;
int cap;
int cost;
};
static constexpr int INF = 1e9;
int n;
vector<vector<Edge>> g;
vector<int> dist, prev_v, prev_e;
explicit MinCostMaxFlow(int n) : n(n), g(n + 1), dist(n + 1), prev_v(n + 1), prev_e(n + 1) {}
void add_edge(int from, int to, int cap, int cost) {
Edge forward{to, (int)g[to].size(), cap, cost};
Edge backward{from, (int)g[from].size(), 0, -cost};
g[from].push_back(forward);
g[to].push_back(backward);
}
bool spfa(int s, int t) {
fill(dist.begin(), dist.end(), INF);
vector<bool> in_queue(n + 1, false);
queue<int> q;
dist[s] = 0;
in_queue[s] = true;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
in_queue[u] = false;
for (int i = 0; i < (int)g[u].size(); ++i) {
const Edge &e = g[u][i];
if (e.cap <= 0) continue;
if (dist[e.to] > dist[u] + e.cost) {
dist[e.to] = dist[u] + e.cost;
prev_v[e.to] = u;
prev_e[e.to] = i;
if (!in_queue[e.to]) {
in_queue[e.to] = true;
q.push(e.to);
}
}
}
}
return dist[t] != INF;
}
pair<int, int> min_cost_max_flow(int s, int t) {
int flow = 0;
int cost = 0;
while (spfa(s, t)) {
int pushed = INF;
for (int v = t; v != s; v = prev_v[v]) {
const Edge &e = g[prev_v[v]][prev_e[v]];
pushed = min(pushed, e.cap);
}
flow += pushed;
cost += pushed * dist[t];
for (int v = t; v != s; v = prev_v[v]) {
Edge &e = g[prev_v[v]][prev_e[v]];
e.cap -= pushed;
g[v][e.rev].cap += pushed;
}
}
return {flow, cost};
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, s, t;
cin >> n >> m >> s >> t;
MinCostMaxFlow solver(n);
for (int i = 0; i < m; ++i) {
int u, v, cap, cost;
cin >> u >> v >> cap >> cost;
solver.add_edge(u, v, cap, cost);
}
auto [flow, cost] = solver.min_cost_max_flow(s, t);
cout << flow << ' ' << cost << '\n';
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 最小费用最大流(SPFA 连续最短路 SSP):点编号 1..n,add_edge 自动补反向边。
# 反向边容量 0、费用 -cost,保证可以撤销之前的流;所有费用先取负再累加会抵消。
# INF=10**9:C++ 用 int 存 dist,加费用可能溢出;Python int 任意精度,不会溢出。
from collections import deque
class Edge:
"""残量网络边:to 终点、rev 反向边在 g[to] 里的下标、cap 剩余容量、cost 单位费用。"""
__slots__ = ("to", "rev", "cap", "cost")
def __init__(self, to: int, rev: int, cap: int, cost: int) -> None:
self.to = to
self.rev = rev
self.cap = cap
self.cost = cost
class MinCostMaxFlow:
"""g[u] 是 u 的出边表;dist/prev_v/prev_e 记录 SPFA 的最短路与路径。"""
INF = 10**9
def __init__(self, n: int) -> None:
self.n = n
self.g: list[list[Edge]] = [[] for _ in range(n + 1)]
self.dist = [0] * (n + 1)
self.prev_v = [0] * (n + 1)
self.prev_e = [0] * (n + 1)
def add_edge(self, from_: int, to: int, cap: int, cost: int) -> None:
# from 是 Python 关键字,参数改名 from_,语义与 C++ 的 from 一致。
# 反向边 rev 记的是对方在各自表中的下标,加完两条边后下标才对齐。
forward = Edge(to, len(self.g[to]), cap, cost)
backward = Edge(from_, len(self.g[from_]), 0, -cost)
self.g[from_].append(forward)
self.g[to].append(backward)
def spfa(self, s: int, t: int) -> bool:
"""按费用做 SPFA 求 s->t 的最短路;存在通路时返回 True 并留下路径。"""
for i in range(1, self.n + 1):
self.dist[i] = self.INF
in_queue = [False] * (self.n + 1)
q: deque[int] = deque([s])
self.dist[s] = 0
in_queue[s] = True
while q:
u = q.popleft()
in_queue[u] = False
for i, edge in enumerate(self.g[u]):
if edge.cap <= 0:
continue
if self.dist[edge.to] > self.dist[u] + edge.cost:
self.dist[edge.to] = self.dist[u] + edge.cost
self.prev_v[edge.to] = u
self.prev_e[edge.to] = i
if not in_queue[edge.to]:
in_queue[edge.to] = True
q.append(edge.to)
return self.dist[t] != self.INF
def min_cost_max_flow(self, s: int, t: int) -> tuple[int, int]:
"""返回 (最大流, 最小费用);s 到不了 t 时是 (0, 0)。"""
flow = 0
cost = 0
while self.spfa(s, t):
pushed = self.INF
v = t
while v != s:
edge = self.g[self.prev_v[v]][self.prev_e[v]]
pushed = min(pushed, edge.cap)
v = self.prev_v[v]
flow += pushed
cost += pushed * self.dist[t]
v = t
while v != s:
edge = self.g[self.prev_v[v]][self.prev_e[v]]
edge.cap -= pushed
self.g[v][edge.rev].cap += pushed
v = self.prev_v[v]
return flow, cost
测试用例
输入:
4 5 1 4
1 2 1 3
1 3 1 1
2 3 1 1
2 4 1 1
3 4 1 3
含义:
- 源点
1,汇点4。 - 每条边格式为
u v cap cost。
输出:
2 8
解释:
最终发送 2 单位流量。两条实际路径可以看作:
1 -> 2 -> 4, cost = 3 + 1 = 4
1 -> 3 -> 4, cost = 1 + 3 = 4
总费用为 8。
反悔机制示例
还是上面的图:
1 -> 2 cost 3
1 -> 3 cost 1
2 -> 3 cost 1
2 -> 4 cost 1
3 -> 4 cost 3
如果第一轮选择了:
1 -> 3 -> 2 -> 4
费用是:
这看起来很便宜,但它占用了中间边。
第二轮残量网络可能选择:
1 -> 2 -> 3 -> 4
其中
第二轮增量费用:
两轮合并后,中间的
1 -> 2 -> 4
1 -> 3 -> 4
总费用:
这说明反向边不是“多出来的奇怪边”,而是算法自动修正旧决策的工具。
应用分类详解
费用流的本质是“带容量限制的最优分配”。只要题目既有流量/匹配/分配约束,又有代价最小或收益最大,就应该考虑费用流。
一、带费用的最大匹配
典型模式: 左边对象匹配右边对象,每个匹配有代价或收益。
识别信号: 工人分配任务、学生分配宿舍、二分图匹配加权。
核心建模: 源点连左部,右部连汇点,中间边容量为 1,费用为匹配代价。
二、多商品分配
典型模式: 若干供应点向需求点运输,每条运输线路有单位成本。
识别信号: 仓库、工厂、运输、供给量、需求量、最小总成本。
核心建模: 源点连供应点,供应点连需求点,需求点连汇点。
三、路径选择与边不重复
典型模式: 要选若干条路径,边或点容量有限,总代价最小。
识别信号: 两条不相交路径、每条边只能用一次、总长度最短。
核心建模: 边容量限制可使用次数,费用为路径长度。
四、最大收益模型
典型模式: 在容量限制下最大化收益。
识别信号: 最大利润、最大收益、选择若干交易。
核心建模: 把收益取负,求最小费用;或改成最大费用最大流。
经典例题
1. 最小费用最大流模板
直接使用本文模板,输入边为容量和费用,输出最大流和最小费用。
2. 运输问题
多个仓库向多个商店供货,每条线路有运输成本。供给和需求都可以转成容量,运输成本转成费用。
3. 带权二分图匹配
每个左部点最多匹配一个右部点,每条匹配边有代价。用费用流可以处理“必须匹配尽量多,且总费用最小”的版本。
参考
- 本书最大流 Edmonds-Karp:
graph/netflow/ek/index.md - 本书 Dinic:
graph/网络流/dinic/index.md - ZKW 费用流旧笔记:
graph/网络流/费用流/zkw.md