题解:洛谷 P3381 最小费用最大流

【题目来源】

洛谷:P3381 【模板】最小费用最大流 - 洛谷

【题目描述】

给出一个包含 \(n\) 个点和 \(m\) 条边的有向图(下面称其为网络) \(G=(V,E)\),该网络上所有点分别编号为 \(1 \sim n\),所有边分别编号为 \(1\sim m\),其中该网络的源点为 \(s\),汇点为 \(t\),网络上的每条边 \((u,v)\) 都有一个流量限制 \(w(u,v)\) 和单位流量的费用 \(c(u,v)\)

你需要给每条边 \((u,v)\) 确定一个流量 \(f(u,v)\),要求:

  1. \(0 \leq f(u,v) \leq w(u,v)\)(每条边的流量不超过其流量限制);
  2. \(\forall p \in \{V \setminus \{s,t\}\}\)\(\sum_{(i,p) \in E}f(i,p)=\sum_{(p,i)\in E}f(p,i)\)(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
  3. \(\sum_{(s,i)\in E}f(s,i)=\sum_{(i,t)\in E}f(i,t)\)(源点流出的流量等于汇点流入的流量)。

定义网络 \(G\) 的流量 \(F(G)=\sum_{(s,i)\in E}f(s,i)\),网络 \(G\) 的费用 \(C(G)=\sum_{(i,j)\in E} f(i,j) \times c(i,j)\)

你需要求出该网络的最小费用最大流,即在 \(F(G)\) 最大的前提下,使 \(C(G)\) 最小。

【输入】

输入第一行包含四个整数 \(n,m,s,t\),分别代表该网络的点数 \(n\),网络的边数 \(m\),源点编号 \(s\),汇点编号 \(t\)

接下来 \(m\) 行,每行四个整数 \(u_i,v_i,w_i,c_i\),分别代表第 \(i\) 条边的起点,终点,流量限制,单位流量费用。

【输出】

输出两个整数,分别为该网络的最大流 \(F(G)\),以及在 \(F(G)\) 最大的前提下,该网络的最小费用 \(C(G)\)

【输入样例】

4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5

【输出样例】

50 280

【核心思想】

  1. 问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向网络,每条边有容量限制 \(w_i\) 和单位流量费用 \(c_i\)。源点为 \(s\),汇点为 \(t\)。要求在网络流量达到最大的前提下,使总费用 \(\sum f_{uv} \cdot c_{uv}\) 最小。这是一个最小费用最大流(MCMF) 问题,本质是在所有最大流方案中找出费用最小的那一个。

  2. 算法选择

    • SPFA + EK(Edmonds‐Karp 的费用流版本):每次利用 SPFA 在残量网络上按费用(单位费用)求从 \(s\)\(t\) 的最短路径(即费用最小的增广路),然后沿该路径增广流量,直到不存在增广路为止。由于每次增广都是当前残量网络中的最短路,可以保证最终得到的最大流费用最小。
    • Dinic + Dijkstra(势能优化)(可选):当边权非负时,可用 Dijkstra 替代 SPFA,配合势能处理反向边的负费用,效率更高。但本题模板通常采用 SPFA,因为反向边费用为负。
  3. 关键步骤

    • 建图:对每条有向边 \((u, v, w, c)\),添加正向边 \((u \to v)\),容量为 \(w\),费用为 \(c\);同时添加反向边 \((v \to u)\),容量为 \(0\),费用为 \(-c\)(用于退流)。使用链式前向星存储,反向边与正向边成对添加(编号 \(i\)\(i \oplus 1\))。
    • SPFA 求最短费用增广路:以费用作为边权,在残量网络(剩余容量 \(> 0\) 的边)上从 \(s\) 跑 SPFA,记录每个节点的最短距离 \(dis[v]\)(费用前缀和)、前驱边编号 \(pre[v]\) 和到该点的最小剩余容量 \(mf[v]\)。若 \(dis[t] = INF\)(或 \(mf[t] = 0\)),则无增广路。
    • 更新残量网络:从 \(t\) 回溯到 \(s\),对于路径上的每条正向边,容量减少 \(mf[t]\);反向边容量增加 \(mf[t]\)
    • 累加流量与费用:流量 \(flow\) 增加 \(mf[t]\),费用 \(cost\) 增加 \(mf[t] \times dis[t]\)
    • 循环:重复 SPFA 和更新,直到不存在增广路,此时 \(flow\) 为最大流,\(cost\) 为最小费用。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(F \cdot E \cdot V)\),其中 \(F\) 为最大流量,\(E\) 为边数,\(V\) 为点数。在流量较小时可行;实际中 SPFA 效率通常较高。
    • 空间复杂度:\(O(V + E)\),存储邻接表和辅助数组。
  5. 最小费用最大流的核心思想

    • 增广路与残量网络:最大流问题中,每找到一条增广路就增加流量;费用流在此基础上,要求每次增广的路径费用最小,这样累积的总费用才能最小化。
    • 最短路径的反复应用:将单位费用看作边权,每次用最短路算法找出从 \(s\)\(t\) 的最小费用路径,沿该路径增广,保证了在同等流量增量下费用增加最少。由于每次增广后残量网络发生变化,需要不断重新计算最短路。
    • 反向边的作用:反向边费用为负,允许流量的“撤销”和重新分配,使得算法可以修正之前可能非最优的流量分配,从而找到全局最优解。
    • 适用场景:适用于带权流量的分配问题,如运输问题、任务调度、最小费用最大流是网络流领域最重要的模型之一。

【算法标签】

提高+ #费用流

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 5005, M = 50005 * 2;  // N: 最大点数, M: 最大边数(双向边,乘2)
int n, m, s, t;                     // n: 点数, m: 边数, s: 源点, t: 汇点
int h[N], e[M], w[M], c[M], ne[M], idx; // 链式前向星:
                                        // h: 表头, e: 终点, w: 单位费用, c: 剩余容量, ne: 下一条, idx: 当前边数
int d[N];                           // d[u]: 从源点到u的最短距离(费用距离)
int mf[N];                          // mf[u]: 从源点到u的当前路径上的最小剩余容量(瓶颈流量)
int pre[N];                         // pre[u]: 记录到达u的边的编号,用于回溯增广路径
int vis[N];                         // vis[u]: SPFA中标记u是否在队列中
int flow, cost;                     // flow: 最大流, cost: 最小费用

// 链式前向星:添加有向边,同时添加反向边(残量网络)
void add(int a, int b, int cc, int dd)
{
    // 正向边:终点b,容量cc,费用dd
    e[idx] = b, c[idx] = cc, w[idx] = dd, ne[idx] = h[a], h[a] = idx++;
}

// SPFA寻找从源点s到汇点t的最短费用增广路径(按费用作为边权)
bool spfa()
{
    // 初始化距离为无穷大
    memset(d, 0x3f, sizeof(d));
    // 初始化瓶颈流量为0(表示未访问)
    memset(mf, 0, sizeof(mf));
    queue<int> q;
    q.push(s);                      // 从源点出发
    d[s] = 0;                       // 源点到自身距离为0
    mf[s] = 1e9;                    // 源点瓶颈流量为无穷大
    vis[s] = 1;                     // 标记源点在队列中

    while (!q.empty())
    {
        int u = q.front(); q.pop();
        vis[u] = 0;                 // u出队,标记为不在队列中

        // 遍历u的所有出边
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int v = e[i];           // 边的终点

            // 松弛操作:若经过u到v更短,且该边还有剩余容量(c[i] > 0)
            if (d[v] > d[u] + w[i] && c[i])
            {
                d[v] = d[u] + w[i];                 // 更新v的最短距离
                mf[v] = min(mf[u], c[i]);           // 更新v的瓶颈流量
                pre[v] = i;                         // 记录到达v的边编号

                if (!vis[v])                        // v不在队列中,入队
                {
                    q.push(v);
                    vis[v] = 1;
                }
            }
        }
    }
    // 若mf[t] > 0,说明找到了增广路径
    return mf[t] > 0;
}

// Edmonds-Karp算法的费用流版本:不断SPFA找最短费用增广路
void EK()
{
    while (spfa())                  // 当还存在增广路径时
    {
        // 从汇点回溯到源点,更新残量网络
        for (int v = t; v != s; )
        {
            int i = pre[v];         // 到达v的边编号

            // 正向边容量减少(使用了mf[t]的流量)
            c[i] -= mf[t];
            // 反向边容量增加(i^1是i的反向边,因为边是成对添加的)
            c[i ^ 1] += mf[t];

            // 通过反向边的终点信息,回溯到上一个节点
            v = e[i ^ 1];
        }
        flow += mf[t];              // 累加本次增广的流量
        cost += mf[t] * d[t];       // 累加本次增广的费用(流量 * 单位费用距离)
    }
}

int main()
{
    cin >> n >> m >> s >> t;      // 读入点数、边数、源点、汇点

    // 初始化链式前向星
    memset(h, -1, sizeof(h));

    // 读入m条有向边
    while (m--)
    {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        add(a, b, c, d);            // 添加正向边:容量c,费用d
        add(b, a, 0, -d);           // 添加反向边:容量0,费用-d(退流时减少费用)
    }

    EK();                           // 运行最小费用最大流算法

    // 输出最大流和最小费用
    cout << flow << " " << cost;

    return 0;
}

【运行结果】

4 5 4 3
4 2 30 2
4 3 20 3
2 3 20 1
2 1 30 9
1 3 40 5
50 280
posted @ 2026-09-01 22:40  团爸讲算法  阅读(12)  评论(0)    收藏  举报