【板子】费用流

一、费用流板子(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) 添加一条从 uv、容量为 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. 节点编号 :板子本身不限制编号,但通常从 1 开始(与题目一致即可)。

  2. 边数要开够M 至少要开到 (题目最大边数 × 2),因为每条边要建一条反向边。

  3. 多组测试数据 :一定要调用 init() 重置 lnktot

  4. 溢出问题 :如果费用或流量很大,记得把 feecapcost 改成 long long

  5. 负环 :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 个" + "求最大/最小价值" 流量限制 + 费用流
"一一对应"/"每人分配一个任务" 二分图匹配 → 费用流
"经过每个点最多一次" + 有权值 拆点 + 费用流
"连续几天"/"区间覆盖" + 最小花费 志愿者招募类,链式建图
"不能同时选相邻的" + 网格 黑白染色 + 最小割/费用流
答案要求"最大收益"或"最小成本" 费用流(取负或直接跑)
相关推荐
玖玥拾1 小时前
LeetCode 80 删除有序数组中的重复项 II
算法·leetcode
青山木2 小时前
Hot 100 --- 电话号码的字母组合
java·数据结构·算法·leetcode·逻辑回归
闪电悠米2 小时前
力扣hot100-240.搜索二维矩阵2-单调性剪枝详解
数据结构·算法·leetcode·矩阵·哈希算法
imuliuliang2 小时前
关于基于图论的最短路径算法性能对比分析7
算法
hurrycry_小亦2 小时前
洛谷题目:P1233 [ICPC 2001 Taejon R] 木棍加工 题解(本题较难)
c++·算法·贪心算法·动态规划
imuliuliang2 小时前
关于从栈与队列看算法思维的演化路径7
算法
春生野草3 小时前
个人笔记--大顶堆和基数排序
java·数据结构·算法
白狐_7983 小时前
【408计算机网络|第04章·下|408-CN-04B】网络层(下):路由算法、RIP、OSPF、BGP、IPv6与路由器
计算机网络·算法·智能路由器
donoot4 小时前
PaddleOCR + PyMuPDF 生成【全兼容双层 PDF】完整实操指南
人工智能·算法·pymupdf·paddleocr·双层pdf