引言
在网络流问题中,我们不仅关心从源点到汇点能输送多少流量,还常常需要考虑输送这些流量的成本。例如物流配送中不同路线的运输费用、通信网络中不同链路的带宽成本、任务调度中不同工人完成任务的报酬差异等场景,都需要在流量约束下最小化总成本。最小费用流问题(Minimum Cost Flow)正是为此而设计的经典网络优化模型。
与最大流问题只求流量最大化不同,最小费用流问题在每条边上同时定义容量和费用,目标是找到满足指定流量的可行流,使得总费用最小。该问题是线性规划的特例,可以通过网络单纯形法或基于最短路径的增广算法高效求解。
核心原理
问题定义
给定一个有向图 G=(V, E),每条边 e 具有:
– 容量 c(e) ≥ 0:边上能通过的最大流量
– 费用 w(e):边上每单位流量的运输成本
指定源点 s、汇点 t 和目标流量 F,求一个从 s 到 t 的流量为 F 的可行流,使得总费用最小。
残量网络与费用
与最大流类似,费用流也使用残量网络:
– 对于原边 (u, v),残量容量为 c(u,v) - f(u,v),残量费用为 w(u,v)
– 对于反向边 (v, u),残量容量为 f(u,v),残量费用为 -w(u,v)
反向边的负费用允许算法”撤销”之前的选择,这是费用流算法能找到最优解的关键。
消圈定理
定理:一个可行流是最小费用流,当且仅当残量网络中不存在负费用增广圈。
基于该定理,算法设计有两条主线:
1. 连续最短路算法(SSP, Successive Shortest Path):每次在残量网络中找从s到t的最短费用路径进行增广
2. 消圈算法(Cycle Canceling):先找任意可行流,然后在残量网络中不断找负圈并沿其增广消去
本文实现SSP算法,因其在实际中效率更高且更容易实现。
SPFA算法求最短路
在费用流的残量网络中,边权(费用)可能为负,但不存在负圈(因为每次沿最短路增广后,残量网络中的距离势保证了无负圈)。因此可以使用SPFA(Shortest Path Faster Algorithm)来求最短路,它比Bellman-Ford更快,且能处理负权边。
Java实现
核心数据结构
import java.util.*;
/**
* 最小费用流实现
* 基于连续最短路算法(Successive Shortest Path)
* 使用SPFA求残量网络中的最短路
*/
public class MinCostFlow {
/**
* 边类:同时支持正向边和反向边
*/
static class Edge {
int to; // 目标节点
int rev; // 反向边在目标节点邻接表中的索引
int capacity; // 残量容量
int cost; // 单位费用
int originalCap; // 原始容量(用于调试)
Edge(int to, int rev, int capacity, int cost) {
this.to = to;
this.rev = rev;
this.capacity = capacity;
this.cost = cost;
this.originalCap = capacity;
}
}
private int n; // 节点数
private List<Edge>[] graph; // 邻接表
private int[] dist; // 最短距离
private int[] parentV; // 最短路径上的父节点
private int[] parentE; // 最短路径上的父边索引
private boolean[] inQueue; // SPFA标记
private static final int INF = Integer.MAX_VALUE / 2;
@SuppressWarnings("unchecked")
public MinCostFlow(int n) {
this.n = n;
this.graph = new ArrayList[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
this.dist = new int[n];
this.parentV = new int[n];
this.parentE = new int[n];
this.inQueue = new boolean[n];
}
/**
* 添加一条带费用的边
* @param from 起点
* @param to 终点
* @param capacity 容量
* @param cost 单位费用
*/
public void addEdge(int from, int to, int capacity, int cost) {
Edge forward = new Edge(to, graph[to].size(), capacity, cost);
Edge backward = new Edge(from, graph[from].size(), 0, -cost);
graph[from].add(forward);
graph[to].add(backward);
}
/**
* SPFA求最短路(可处理负权边)
* @param source 源点
* @param sink 汇点
* @return 是否存在从source到sink的路径
*/
private boolean spfa(int source, int sink) {
Arrays.fill(dist, INF);
Arrays.fill(inQueue, false);
dist[source] = 0;
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
inQueue[source] = true;
while (!queue.isEmpty()) {
int u = queue.poll();
inQueue[u] = false;
for (int i = 0; i < graph[u].size(); i++) {
Edge e = graph[u].get(i);
if (e.capacity > 0 && dist[u] + e.cost < dist[e.to]) {
dist[e.to] = dist[u] + e.cost;
parentV[e.to] = u;
parentE[e.to] = i;
if (!inQueue[e.to]) {
queue.offer(e.to);
inQueue[e.to] = true;
}
}
}
}
return dist[sink] < INF;
}
/**
* 求解最小费用流
* @param source 源点
* @param sink 汇点
* @param maxFlow 需要输送的最大流量
* @return [实际输送流量, 总费用],如果无法输送maxFlow则返回实际能输送的最大流量及对应费用
*/
public int[] minCostMaxFlow(int source, int sink, int maxFlow) {
int flow = 0;
int cost = 0;
while (flow < maxFlow && spfa(source, sink)) {
// 计算当前最短路径上的可增广流量
int addFlow = maxFlow - flow;
int v = sink;
while (v != source) {
int u = parentV[v];
Edge e = graph[u].get(parentE[v]);
addFlow = Math.min(addFlow, e.capacity);
v = u;
}
// 增广
v = sink;
while (v != source) {
int u = parentV[v];
Edge e = graph[u].get(parentE[v]);
Edge rev = graph[v].get(e.rev);
e.capacity -= addFlow;
rev.capacity += addFlow;
v = u;
}
flow += addFlow;
cost += addFlow * dist[sink];
}
return new int[]{flow, cost};
}
/**
* 获取当前某条边上的实际流量
* 通过原始容量减去残量容量得到
*/
public int getFlow(int from, int edgeIdx) {
Edge e = graph[from].get(edgeIdx);
return e.originalCap - e.capacity;
}
/**
* 打印当前网络状态(调试用)
*/
public void printNetwork() {
System.out.println("Network State:");
for (int u = 0; u < n; u++) {
for (int i = 0; i < graph[u].size(); i++) {
Edge e = graph[u].get(i);
if (e.originalCap > 0) { // 只打印原始边
int flow = e.originalCap - e.capacity;
System.out.printf(" Edge %d -> %d: flow=%d/%d cost=%d%n",
u, e.to, flow, e.originalCap, e.cost);
}
}
}
}
}
消圈算法实现
/**
* 费用流的消圈算法实现
* 先通过最大流找可行流,再在残量网络中不断消去负圈
*/
public class CycleCanceling {
static class Edge {
int to, rev, capacity, cost, originalCap;
Edge(int to, int rev, int capacity, int cost) {
this.to = to; this.rev = rev;
this.capacity = capacity; this.cost = cost;
this.originalCap = capacity;
}
}
private int n;
private List<Edge>[] graph;
private static final int INF = Integer.MAX_VALUE / 2;
@SuppressWarnings("unchecked")
public CycleCanceling(int n) {
this.n = n;
this.graph = new ArrayList[n];
for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
}
public void addEdge(int from, int to, int capacity, int cost) {
Edge f = new Edge(to, graph[to].size(), capacity, cost);
Edge b = new Edge(from, graph[from].size(), 0, -cost);
graph[from].add(f);
graph[to].add(b);
}
/**
* 使用SPFA找负圈,返回负圈上的节点列表
*/
private List<Integer> findNegativeCycle() {
int[] dist = new int[n];
int[] parentV = new int[n];
int[] parentE = new int[n];
int[] count = new int[n];
boolean[] inQueue = new boolean[n];
Arrays.fill(dist, 0); // 初始化为0,更容易找到负圈
Arrays.fill(parentV, -1);
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
queue.offer(i);
inQueue[i] = true;
}
while (!queue.isEmpty()) {
int u = queue.poll();
inQueue[u] = false;
for (int i = 0; i < graph[u].size(); i++) {
Edge e = graph[u].get(i);
if (e.capacity > 0 && dist[u] + e.cost < dist[e.to]) {
dist[e.to] = dist[u] + e.cost;
parentV[e.to] = u;
parentE[e.to] = i;
count[e.to] = count[u] + 1;
if (count[e.to] >= n) {
// 找到负圈,回溯获取圈上的节点
boolean[] visited = new boolean[n];
int cur = e.to;
while (!visited[cur]) {
visited[cur] = true;
cur = parentV[cur];
}
List<Integer> cycle = new ArrayList<>();
int start = cur;
do {
cycle.add(cur);
cur = parentV[cur];
} while (cur != start);
Collections.reverse(cycle);
return cycle;
}
if (!inQueue[e.to]) {
queue.offer(e.to);
inQueue[e.to] = true;
}
}
}
}
return null; // 无负圈
}
/**
* 沿负圈增广
*/
private int augmentAlongCycle(List<Integer> cycle) {
int minFlow = INF;
int m = cycle.size();
// 找圈上的最小残量
for (int i = 0; i < m; i++) {
int u = cycle.get(i);
int v = cycle.get((i + 1) % m);
for (Edge e : graph[u]) {
if (e.to == v && e.capacity > 0) {
minFlow = Math.min(minFlow, e.capacity);
break;
}
}
}
// 增广
for (int i = 0; i < m; i++) {
int u = cycle.get(i);
int v = cycle.get((i + 1) % m);
for (int j = 0; j < graph[u].size(); j++) {
Edge e = graph[u].get(j);
if (e.to == v && e.capacity > 0) {
e.capacity -= minFlow;
graph[v].get(e.rev).capacity += minFlow;
break;
}
}
}
return minFlow;
}
/**
* 消圈算法求解最小费用流
* 注意:此实现需要先确保可行流存在
*/
public int[] solve(int source, int sink, int requiredFlow) {
// 第一步:通过最大流算法找可行流(简化版:直接用SSP找)
// 这里为了展示消圈思想,先假设可行流已通过其他方式找到
// 实际使用时,可以先跑一遍最大流确保流量足够
int iterations = 0;
while (iterations < 10000) {
List<Integer> cycle = findNegativeCycle();
if (cycle == null) break;
augmentAlongCycle(cycle);
iterations++;
}
// 计算总流量和总费用
int flow = 0, cost = 0;
for (int u = 0; u < n; u++) {
for (Edge e : graph[u]) {
if (e.originalCap > 0) {
int f = e.originalCap - e.capacity;
cost += f * e.cost;
}
}
}
// 费用被正向和反向边各算了一次,除以2
cost /= 2;
return new int[]{flow, cost};
}
}
测试用例
public class MinCostFlowDemo {
public static void main(String[] args) {
// 案例:从仓库(0)向三个商店(1,2,3)配送货物
// 节点4是虚拟汇点,收集所有商店的流量
System.out.println("=== 最小费用流案例 ===");
int n = 5;
MinCostFlow mcf = new MinCostFlow(n);
// 仓库到各商店的配送路线(容量,单位费用)
mcf.addEdge(0, 1, 10, 4); // 路线0->1:容量10,费用4
mcf.addEdge(0, 2, 8, 3); // 路线0->2:容量8,费用3
mcf.addEdge(0, 3, 12, 6); // 路线0->3:容量12,费用6
// 商店到汇点
mcf.addEdge(1, 4, 10, 0);
mcf.addEdge(2, 4, 8, 0);
mcf.addEdge(3, 4, 12, 0);
// 额外路线:商店之间的中转
mcf.addEdge(1, 2, 5, 1); // 1->2中转,费用1
mcf.addEdge(2, 3, 6, 2); // 2->3中转,费用2
int source = 0, sink = 4;
int requiredFlow = 15;
int[] result = mcf.minCostMaxFlow(source, sink, requiredFlow);
System.out.println("请求流量: " + requiredFlow);
System.out.println("实际流量: " + result[0]);
System.out.println("总费用: " + result[1]);
mcf.printNetwork();
// 案例2:最小费用最大流
System.out.println("\n=== 最小费用最大流案例 ===");
MinCostFlow mcf2 = new MinCostFlow(4);
mcf2.addEdge(0, 1, 3, 1);
mcf2.addEdge(0, 2, 2, 2);
mcf2.addEdge(1, 2, 2, 1);
mcf2.addEdge(1, 3, 3, 3);
mcf2.addEdge(2, 3, 4, 1);
int[] result2 = mcf2.minCostMaxFlow(0, 3, Integer.MAX_VALUE);
System.out.println("最大流: " + result2[0]);
System.out.println("最小费用: " + result2[1]);
mcf2.printNetwork();
}
}
复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| SSP + SPFA | O(F · E · V) | O(V + E) | 费用非负或小规模 |
| SSP + Dijkstra + 势函数 | O(F · E log V) | O(V + E) | 大规模,效率最高 |
| 消圈算法 | O(V · E · C · F) | O(V + E) | 理论研究,实际较少使用 |
其中 F 为流量,V 为顶点数,E 为边数,C 为最大费用。使用势函数(Johnson技巧)将边权转换为非负后,可以用Dijkstra替代SPFA,将每次最短路的复杂度从O(VE)降至O(E log V)。
势函数优化
/**
* 使用势函数的Dijkstra优化版本
* 通过维护势函数h,将边权转化为非负值
*/
public int[] minCostMaxFlowWithPotential(int source, int sink, int maxFlow) {
int[] potential = new int[n]; // 势函数
int flow = 0, cost = 0;
while (flow < maxFlow) {
// Dijkstra求最短路(边权已非负)
Arrays.fill(dist, INF);
dist[source] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
pq.offer(new int[]{source, 0});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int u = cur[0];
if (cur[1] > dist[u]) continue;
for (int i = 0; i < graph[u].size(); i++) {
Edge e = graph[u].get(i);
if (e.capacity > 0) {
// 约化费用 = 原费用 + h[u] - h[v]
int reducedCost = e.cost + potential[u] - potential[e.to];
if (dist[u] + reducedCost < dist[e.to]) {
dist[e.to] = dist[u] + reducedCost;
parentV[e.to] = u;
parentE[e.to] = i;
pq.offer(new int[]{e.to, dist[e.to]});
}
}
}
}
if (dist[sink] == INF) break;
// 更新势函数
for (int i = 0; i < n; i++) {
if (dist[i] < INF) potential[i] += dist[i];
}
// 增广(使用原始费用计算)
int addFlow = maxFlow - flow;
int v = sink;
while (v != source) {
int u = parentV[v];
Edge e = graph[u].get(parentE[v]);
addFlow = Math.min(addFlow, e.capacity);
v = u;
}
v = sink;
while (v != source) {
int u = parentV[v];
Edge e = graph[u].get(parentE[v]);
Edge rev = graph[v].get(e.rev);
e.capacity -= addFlow;
rev.capacity += addFlow;
v = u;
}
flow += addFlow;
cost += addFlow * potential[sink]; // 势函数累积了原始最短路长度
}
return new int[]{flow, cost};
}
应用场景
- 物流调度:最小化运输成本的同时满足配送需求
- 任务分配:将任务分配给工人,使总报酬最小
- 网络路由:在带宽约束下选择成本最低的数据传输路径
- 生产计划:在产能约束下最小化生产成本
- 图像分割:基于图割的费用流模型进行图像前景背景分离
总结
最小费用流问题通过引入费用维度,将网络流从单纯的流量最大化扩展为成本优化模型。本文实现了基于SPFA的连续最短路算法,并展示了消圈算法的核心思想。理解费用流的关键在于掌握残量网络中的反向边机制和消圈定理,这些是保证算法最优性的理论基础。