一、费用流板子(SPFA + DFS 当前弧优化)
时间复杂度:与流量大小和费用范围有关,一般能处理 n≤5000,m≤10e5 的规模。
特点:支持负费用边(但不能有负环),使用 SPFA 找最短路。
最大流最小费用
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 5005; // 最大点数(按实际需求调整)
constexpr int M = 200005; // 最大边数 × 2(每条边要建反向边)
constexpr int INF = 0x3f3f3f3f;
struct MCMF {
int tot, lnk[N], cur[N], ter[M], nxt[M], cap[M], cost[M];
int dis[N], fee; // fee 记录最小费用
bool vis[N];
void init(int n = 0) { // 传 n 可不传,这里只是重置
tot = 1;
memset(lnk, 0, sizeof(lnk));
}
// 加边:u->v,容量 w,单位费用 c
void addedge(int u, int v, int w, int c) {
ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, cap[tot] = w, cost[tot] = c;
ter[++tot] = u, nxt[tot] = lnk[v], lnk[v] = tot, cap[tot] = 0, cost[tot] = -c;
}
bool spfa(int s, int t) {
memset(dis, 0x3f, sizeof(dis));
memcpy(cur, lnk, sizeof(lnk));
queue<int> q;
q.push(s), dis[s] = 0, vis[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop(), vis[u] = false;
for (int i = lnk[u]; i; i = nxt[i]) {
int v = ter[i];
if (cap[i] && dis[v] > dis[u] + cost[i]) {
dis[v] = dis[u] + cost[i];
if (!vis[v]) q.push(v), vis[v] = true;
}
}
}
return dis[t] != INF;
}
int dfs(int u, int t, int flow) {
if (u == t) return flow;
vis[u] = true;
int ans = 0;
for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
int v = ter[i];
if (!vis[v] && cap[i] && dis[v] == dis[u] + cost[i]) {
int x = dfs(v, t, min(cap[i], flow - ans));
if (x) {
fee += x * cost[i];
cap[i] -= x, cap[i ^ 1] += x, ans += x;
}
}
}
vis[u] = false;
return ans;
}
// 返回 {最大流, 最小费用}
pair<int, int> solve(int s, int t) {
int flow = 0;
fee = 0;
while (spfa(s, t)) {
int x;
while ((x = dfs(s, t, INF))) flow += x;
}
return {flow, fee};
}
};
二、基本用法
1. 建图与求解
MCMF g;
int main() {
int n, m, s, t;
scanf("%d%d%d%d", &n, &m, &s, &t);
g.init();
for (int i = 0; i < m; i++) {
int u, v, w, c;
scanf("%d%d%d%d", &u, &v, &w, &c);
g.addedge(u, v, w, c); // 加边:容量 w,单位费用 c
}
auto [maxflow, mincost] = g.solve(s, t);
printf("maxflow = %d, mincost = %d\n", maxflow, mincost);
return 0;
}
2. 常见接口总结
| 操作 | 说明 |
|---|---|
g.init() |
初始化(多组数据时必调) |
g.addedge(u, v, w, c) |
添加一条从 u 到 v、容量为 w、单位费用为 c 的有向边 |
g.solve(s, t) |
从源点 s 到汇点 t 跑最小费用最大流,返回 {最大流, 最小费用} |
三、进阶用法
限定流量求最小费用
有时候你不需要跑满最大流,而是希望流量恰好为 K 时费用最小:
// 在 solve 的基础上稍作修改
pair<int, int> solve_limit(int s, int t, int K) {
int flow = 0;
fee = 0;
while (flow < K && spfa(s, t)) {
int x;
while (flow < K && (x = dfs(s, t, K - flow))) flow += x;
}
if (flow < K) return {-1, -1}; // 流量达不到 K,无解
return {flow, fee};
}
最大费用最大流
把费用取相反数建边,跑完再把结果取反即可:
g.addedge(u, v, w, -c); // 费用取负
// 跑完后 mincost = -maxcost
四、注意事项
-
节点编号 :板子本身不限制编号,但通常从
1开始(与题目一致即可)。 -
边数要开够 :
M至少要开到(题目最大边数 × 2),因为每条边要建一条反向边。 -
多组测试数据 :一定要调用
init()重置lnk和tot。 -
溢出问题 :如果费用或流量很大,记得把
fee、cap、cost改成long long。 -
负环 :SPFA 能处理负权边,但图中不能存在从源点可达的负环,否则会死循环。
原理
一、原理
1. 核心目标
在保证流量最大的前提下,让总费用最小。
打个比方:你是一个物流公司老板,要把尽可能多的货物从仓库(源点 S)运到客户(汇点 T),每条路有容量限制 (最多运多少车)和过路费(每车多少钱)。你既想多赚钱(流量大),又想省钱(费用低)------这就是最小费用最大流。
2. 贪心策略:每次走"最便宜的路"
算法流程本质上就是一个带费用的增广路算法:
while (在残量网络中还能找到一条 S→T 的费用最短路径) {
沿着这条最短路,尽可能多地运货(受限于路径上最小的剩余容量);
更新剩余容量,同时累加费用;
}
为什么这样做能保证"最小费用"?
因为每次选的都是当前单位费用总和最低的路径。数学上可以证明(类似 Edmonds-Karp),这样得到的费用序列是单调不减的,最终停下来的时候就是全局最小费用。
反向边为啥费用是 -c?
这是最妙的设计。假设你之前从 A→B 运了一批货,后来发现绕路更便宜,你想"反悔"------把 A→B 的货运回来到别的路走。
反向边 B→A 容量恢复、费用设为 -c,意味着:
-
退掉 1 单位流量 = 收回
c块钱 -
相当于"撤销"之前的操作,账本自动对齐
3. 板子每一步在干嘛
| 函数 | 干了啥 |
|---|---|
spfa(s, t) |
在残量网络上找 S→T 的费用最短路 ,用 dis[] 记录每个点到源的最小费用距离 |
dfs(s, t, flow) |
沿着 dis[v] == dis[u] + cost[i] 的边(也就是在最短路上的边)进行多路增广,一次 SPFA 可能增广多次 |
cur[](当前弧) |
避免重复走已经流满的边,加速 |
vis[] |
防止 DFS 在残量网络里绕圈死循环(因为可能有零环/负环残留) |
二、常见题型 → 网络流建图套路
套路 1:二分图最优匹配(最经典)
场景:N 个人做 N 项工作,每个人做每项工作的收益/成本不同,每人只能做一项,每项只需一人,求最大总收益(或最小总成本)。
建图:
源点 S ──(容量1, 费用0)──→ 每个人
每个人 ──(容量1, 费用=成本)──→ 每项工作
每项工作 ──(容量1, 费用0)──→ 汇点 T
-
最大收益 → 费用取负,跑最小费用最大流,最后答案取反。
-
如果人数和工作数不等,就是一般的二分图带权匹配。
例题:洛谷 P4014 分配问题、P2053 SCOI2007 修车
套路 2:供需运输问题
场景:有若干个工厂(供应量)和商店(需求量),工厂到商店有单位运费,求满足所有需求的最小运费。
建图:
源点 S ──(容量=供应量, 费用0)──→ 工厂
工厂 ──(容量=∞, 费用=单位运费)──→ 商店
商店 ──(容量=需求量, 费用0)──→ 汇点 T
例题:洛谷 P4015 运输问题
套路 3:点/边有使用次数限制的最优化
场景:经过一个点最多 K 次,或者选某些物品有依赖关系,求最大收益。
常用技巧:拆点!
比如"每个点最多经过一次":
把点 u 拆成 u_in 和 u_out:
S → u_in (容量1, 费用0)
u_in → u_out (容量1, 费用=点权/-点权) ← 在这里体现"选这个点的代价/收益"
u_out → 它能到达的点 (容量∞, 费用0)
相当于uin uout之间加一个单边限制了最大u容量
例题:洛谷 P4011 孤岛营救(不是这个)、P1251 餐巾计划(超级经典)
套路 4:网格图取数(黑白染色)
场景:N×M 网格,每个格子有数字,选了某个格子就不能选相邻格子,求选出数的最大和。
建图:
黑白染色(像国际象棋棋盘):
黑色格子 ── 连源点 S (容量1, 费用0)
白色格子 ── 连汇点 T (容量1, 费用0)
黑色 → 相邻白色 (容量1, 费用=-(格子值))
跑最小费用最大流,答案是 -fee。
(本质是二分图最大权独立集 = 总权 - 最小割 = 跑费用流)
例题:洛谷 P2774 方格取数问题
套路 5:最大费用最大流(利润最大化)
场景:和最小费用一样,但你要求的是"收入最大"。
做法 :所有费用 c 变成 -c 建边,跑完 MCMF 后 max_profit = -mincost。
注意:SPFA 找的是最短路,所以取负之后,"最大收入路"就变成了"最短路",完美适配。
套路 6:流量限制(不是跑满最大流)
场景:不一定非要运最多的货,而是"刚好运 K 个单位时,费用最小"。
做法 :在 solve 里加个流量上限判断(我之前给你的 solve_limit 函数),或者更简单------在源点 S 连一个超级源 SS,边容量为 K,这样整个网络最多就只能流 K 了。
SS ──(容量K, 费用0)──→ S
套路 7:线性规划 / 差分约束(进阶)
有些形如"满足若干不等式,求目标函数最值"的问题,可以写成费用流。
比如经典题 志愿者招募(P3980):
-
每天有最少人数需求
-
志愿者按连续天数签约,每人有费用
-
求满足需求的最小花费
建图核心思想:把天与天之间连边表示"前一天剩下的人",招募志愿者就是走一条跨越几天的边。
三、快速识别"费用流"
看到题目里有这些关键词,立刻往费用流上靠:
| 信号 | 说明 |
|---|---|
| "最多选 K 个" + "求最大/最小价值" | 流量限制 + 费用流 |
| "一一对应"/"每人分配一个任务" | 二分图匹配 → 费用流 |
| "经过每个点最多一次" + 有权值 | 拆点 + 费用流 |
| "连续几天"/"区间覆盖" + 最小花费 | 志愿者招募类,链式建图 |
| "不能同时选相邻的" + 网格 | 黑白染色 + 最小割/费用流 |
| 答案要求"最大收益"或"最小成本" | 费用流(取负或直接跑) |