题解:洛谷 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)\),要求:
- \(0 \leq f(u,v) \leq w(u,v)\)(每条边的流量不超过其流量限制);
- \(\forall p \in \{V \setminus \{s,t\}\}\),\(\sum_{(i,p) \in E}f(i,p)=\sum_{(p,i)\in E}f(p,i)\)(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
- \(\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
【核心思想】
-
问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向网络,每条边有容量限制 \(w_i\) 和单位流量费用 \(c_i\)。源点为 \(s\),汇点为 \(t\)。要求在网络流量达到最大的前提下,使总费用 \(\sum f_{uv} \cdot c_{uv}\) 最小。这是一个最小费用最大流(MCMF) 问题,本质是在所有最大流方案中找出费用最小的那一个。
-
算法选择:
- SPFA + EK(Edmonds‐Karp 的费用流版本):每次利用 SPFA 在残量网络上按费用(单位费用)求从 \(s\) 到 \(t\) 的最短路径(即费用最小的增广路),然后沿该路径增广流量,直到不存在增广路为止。由于每次增广都是当前残量网络中的最短路,可以保证最终得到的最大流费用最小。
- Dinic + Dijkstra(势能优化)(可选):当边权非负时,可用 Dijkstra 替代 SPFA,配合势能处理反向边的负费用,效率更高。但本题模板通常采用 SPFA,因为反向边费用为负。
-
关键步骤:
- 建图:对每条有向边 \((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\) 为最小费用。
-
时间/空间复杂度:
- 时间复杂度:\(O(F \cdot E \cdot V)\),其中 \(F\) 为最大流量,\(E\) 为边数,\(V\) 为点数。在流量较小时可行;实际中 SPFA 效率通常较高。
- 空间复杂度:\(O(V + E)\),存储邻接表和辅助数组。
-
最小费用最大流的核心思想:
- 增广路与残量网络:最大流问题中,每找到一条增广路就增加流量;费用流在此基础上,要求每次增广的路径费用最小,这样累积的总费用才能最小化。
- 最短路径的反复应用:将单位费用看作边权,每次用最短路算法找出从 \(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
浙公网安备 33010602011771号