资讯动态

网络流算法实战:从最大流到最小费用流,解决物流与调度优化问题

发布时间:2026/8/28 10:43:05 来源:尧图企业网站定制
1. 从现实问题到图论模型为什么我们需要“流”如果你曾经负责过物流中心的车辆调度或者参与过城市供水管网的优化设计甚至只是简单地规划过一场大型活动的物资配送路线那么你很可能已经与“流”这个概念打过交道了。这些看似不同领域的问题背后都隐藏着一个共同的数学结构如何在一个有容量限制的网络中高效、经济地将“东西”从源头运送到目的地。这个“东西”可以是货物、水流、数据包甚至是资金。而图论中的最大流与最小费用流模型就是解决这类问题的两把利器。我最初接触最大流问题是在一个电商仓储的“爆仓”项目中。当时仓库的出库口是瓶颈传送带、分拣员、打包台构成了一张复杂的网络。我们需要计算在现有设备和人力的限制下每小时最多能处理多少订单。这本质上就是一个典型的最大流问题仓库是源点出库口是汇点各个处理环节是节点它们的处理能力就是边的容量。通过建模和计算我们不仅找到了理论上的最大吞吐量更重要的是发现了几个意料之外的瓶颈环节比如某段传送带的实际效率远低于设计值。这就是图论模型的魅力它能把一个复杂的现实系统抽象成点和线让隐藏的问题浮出水面。最小费用流则更进一步。假设你的物流公司有多个仓库源点和多个客户点汇点每条运输路线不仅有运力上限容量还有不同的单位运输成本。你的目标不再是简单地追求最大运输量而是在满足所有客户需求的前提下让总运输成本最低。这就不再是“能运多少”的问题而是“怎样运最划算”的问题。最小费用流模型正是为此而生。很多人觉得图论、网络流这些概念过于理论化离实际应用很远。但恰恰相反它们可能是你工具箱里最实用的“工程数学”之一。接下来我会抛开复杂的数学符号用尽可能直白的方式带你理解这两个模型的核心思想、经典算法以及在实际建模中如何避坑。2. 图论基础一张网到底是怎么画出来的在讨论“流”之前我们必须先统一语言理解图论的基本“语法”。图论中的“图”Graph不是指我们通常看到的柱状图、折线图而是由“顶点”Vertex和“边”Edge组成的抽象网络。这是我们将现实世界复杂关系数学化的第一步。2.1 图的要素与分类一个图 G 通常表示为 G(V, E)其中 V 是顶点集合E 是边集合。边可以是有方向的从顶点A指向顶点B称为有向边或弧也可以是无方向的A和B相连不分彼此称为无向边。物流网络中单行线就是有向边而双行道路可以看作两条反向的有向边或者一条无向边。对于网络流问题我们几乎总是在处理有向图。因为“流”是有明确方向的从源点流向汇点。图中每条有向边 e(u, v) 都有一个重要的属性容量 c(e) 或 c(u, v)。它表示这条边所能允许通过的最大流量。例如一条输油管道的最大每小时输油量或者一条公路的最大车流量。在网络流模型中有两个特殊的顶点源点Source 常记为 s流的起点理论上只发出流不接收流实际计算中流入源点的流量净值为负。汇点Sink 常记为 t流的终点只接收流不发出流。除了源点和汇点其他顶点称为中间点或转运点。流经这些点的流量必须满足一个黄金法则流量守恒。即流入一个中间点的总流量必须等于流出该点的总流量。这就像十字路口开进去的车必须全部开出来不考虑停留不能凭空消失或产生。注意这里容易产生一个误解。流量守恒是针对每个中间顶点而言的。对于源点s净流出量流出减流入是一个正值我们称之为流量值 |f|。对于汇点t净流入量是一个相等的正值。整个网络的流量值就是最终成功从s运送到t的“货物”总量。2.2 如何为实际问题构建图模型这是将图论应用于实际最关键也最容易出错的一步。构建的模型是否准确直接决定了后续分析和求解的有效性。核心在于识别“什么作为顶点”、“什么作为边”以及“边的容量和成本如何定义”。以一个简单的供水系统为例顶点水库、水泵站、加压站、居民区水箱都可以作为顶点。其中水库是源点居民区水箱是汇点。边连接这些设施的管道就是边。边的方向就是水的流向。容量每条管道的最大通水能力就是边的容量。费用如果考虑泵站的电费成本那么经过泵站所在的边就可以赋予一个单位流量所需的费用。再举一个更复杂的例子项目任务调度。假设一个项目有多个任务任务之间有先后依赖关系例如任务B必须在任务A完成后才能开始。我们可以用顶点表示任务的开始或结束事件用有向边表示任务本身。边的容量可以设为无穷大或者一个很大的数而边的权重费用可以设为完成该任务所需的时间。这样寻找关键路径决定项目最短工期的任务序列的问题就可以转化为在这个有向图中寻找最长路径的问题而最小费用流算法经过变通也能用于此类调度优化。我个人的经验是在建模初期一定要在白板或纸上反复画图和业务人员确认每个顶点和边的实际含义。经常遇到的坑是“顶点粒度太粗”。比如在物流问题中如果把一个城市作为一个顶点那么这个城市内部的道路拥堵、仓库装卸能力等约束就无法体现。这时可能需要将城市拆解为“入口”、“仓库”、“出口”等多个顶点并用内部边来表示这些约束。3. 最大流问题如何榨干网络的运输潜能最大流问题的目标非常直接在一个有容量限制的网络中找到从源点 s 到汇点 t 的最大可能流量。它回答的是网络的理论极限吞吐量问题。3.1 核心思想增广路径与剩余网络最大流算法最经典的思想是 Ford-Fulkerson 方法及其众多改进版如 Edmonds-Karp, Dinic 算法。其核心概念是“增广路径”和“剩余网络”。首先我们从一个流量为0的流开始。剩余网络是针对当前流而言的。在原图中对于每条边 (u, v)如果当前流量 f(u, v) 容量 c(u, v)那么这条边还有“剩余空间”可以推送更多流量。我们在剩余网络中创建一条同向边 (u, v)其剩余容量为 c(u, v) - f(u, v)。如果当前流量 f(u, v) 0那么我们可以“反悔”一部分流量。我们在剩余网络中创建一条反向边 (v, u)其容量就等于 f(u, v)。这相当于提供了一个回收已分配流量的通道。增广路径就是在剩余网络中从源点 s 到汇点 t 的一条路径。这条路径上所有边的剩余容量都大于0。如果我们找到这样一条路径就意味着我们还可以沿着这条路径增加一定的流量。增加的流量值等于这条路径上所有边剩余容量的最小值木桶的短板。算法过程可以概括为初始化所有边流量为0。构建当前流对应的剩余网络。在剩余网络中寻找一条从 s 到 t 的增广路径。如果找不到当前流就是最大流算法结束。如果找到计算这条路径的“瓶颈容量” delta。沿着这条路径正向边增加 delta 流量反向边减少 delta 流量相当于释放容量。更新流量回到步骤2。为什么需要反向边这是算法正确性的关键。考虑一个简单的交叉路径A-B-D 和 A-C-DB-C也有一条边。初始时流可能占用了A-B-C-D这条非最优的路径阻塞了更优的流。反向边 (C, B) 的存在允许算法在后续步骤中“撤销”从B到C的流从而将流重新路由到A-B-D和A-C-D这两条路上达到全局最优。反向边提供了一种“后悔机制”。3.2 实战算法Edmonds-Karp 与 DinicEdmonds-Karp 算法是 Ford-Fulkerson 方法的一个具体实现它规定使用广度优先搜索BFS来寻找增广路径。这保证了每次找到的都是最短的边数最少增广路径。其时间复杂度为 O(V * E^2)其中V是顶点数E是边数。对于边数不是特别多的中等规模网络这是一个简单可靠的选择。Dinic 算法则更为高效是竞赛和实际工程中更常用的算法。它引入了“分层图”的概念。首先使用 BFS 对剩余网络进行分层记录每个顶点到源点 s 的距离边数。在增广时只允许从第 i 层的顶点走向第 i1 层的顶点。同时它使用深度优先搜索DFS进行多路增广一次性榨干一条路径上的所有可能流量。Dinic 算法的时间复杂度为 O(V^2 * E)对于稀疏图E远小于V^2或特定结构的图实际表现比 Edmonds-Karp 好很多。下面用一个极简的例子对比一下。假设我们用手算一个网络的最大流用 Edmonds-Karp 的思路会更直观一步步 BFS 找路。而用 Dinic 的思路则是先分层然后在分层图上尝试进行更激进的流量推送。在实际编程中例如使用 Python我们可以用邻接表来存储图。每条边需要记录终点、容量、反向边的索引。这样在更新流量时可以同步更新正向边和反向边。# 一个非常简化的 Edmonds-Karp 算法框架示意 from collections import deque def edmonds_karp(graph, capacity, s, t): n len(graph) flow [[0] * n for _ in range(n)] parent [-1] * n max_flow 0 def bfs(): # 使用BFS寻找增广路径 visited [False] * n queue deque([s]) visited[s] True while queue: u queue.popleft() for v in graph[u]: if not visited[v] and capacity[u][v] flow[u][v]: # 还有剩余容量 visited[v] True parent[v] u if v t: return True # 找到汇点 queue.append(v) return False # 未找到增广路径 while bfs(): # 不断寻找增广路径 # 计算路径上的最小剩余容量瓶颈值 path_flow float(Inf) v t while v ! s: u parent[v] path_flow min(path_flow, capacity[u][v] - flow[u][v]) v u # 更新路径上的流量 v t while v ! s: u parent[v] flow[u][v] path_flow flow[v][u] - path_flow # 更新反向边流量 v u max_flow path_flow return max_flow提示上面的代码是概念性示意使用了邻接矩阵capacity和flow对于稀疏图效率不高。实际应用时应采用邻接表存储并封装边对象。3.3 最大流模型的应用与变形最大流模型本身用途广泛但其真正的威力在于它能作为子程序解决其他问题。二分图最大匹配例如求职者和工作岗位的匹配。可以构造一个网络源点连接所有求职者容量1求职者连接到他们能胜任的工作容量1工作连接到汇点容量为该岗位招聘人数。这个网络的最大流值就是最大匹配数。项目选择问题有多个项目每个项目有收益但需要消耗多种资源。资源有限。如何选择项目组合使总收益最大这可以转化为一个带有“项目”和“资源”两类顶点的网络流问题最终求解一个最小割其容量对应放弃的收益和超出的资源惩罚之和。多源点多汇点如果有多个仓库和多个市场怎么办很简单建立一个超级源点连接到所有仓库边的容量就是仓库的供应量。同样建立一个超级汇点所有市场连接到它边的容量就是市场的需求量。这样就转化为了单源单汇的最大流问题。一个我遇到过的变形是“点有容量”的情况。例如一个中转站有处理上限。标准的边容量无法处理这个约束。解决方法是将该顶点 v 拆分成两个顶点v_in 和 v_out并用一条有向边 (v_in, v_out) 连接这条边的容量就设置为该顶点的处理容量。所有原指向 v 的边改为指向 v_in所有从 v 指出的边改为从 v_out 指出。这个“顶点拆边”的技巧非常实用。4. 最小费用最大流问题既要多又要省最大流只关心数量不关心成本。但在现实中我们几乎总是在预算或成本约束下行事。最小费用最大流问题Minimum Cost Maximum Flow的目标是在所有可能的最大流中找到总运输成本最低的那个方案。每条边 e(u, v) 除了容量 c(u, v)还有一个属性单位流量费用 w(u, v)。总费用是每条边的流量乘以单位费用之和。4.1 从最短路径到最小费用流连续最短路算法最常用的算法是连续最短路算法Successive Shortest Path Algorithm它其实是最大流中“寻找增广路径”思想的自然延伸。在最大流中我们找的是任意一条增广路径。在这里我们找的是当前剩余网络中从源点 s 到汇点 t 的“单位费用最短路径”。算法的骨架如下初始化流量为0总费用为0。构建剩余网络。注意反向边的费用是原边费用的相反数w(v, u) -w(u, v)。这很好理解如果你退回一单位流量那么你应该“赚回”当初花费的成本。在剩余网络中寻找从 s 到 t 的、以单位费用为边权的最短路径即费用最小的增广路径。这里需要使用能处理负权边的最短路径算法因为反向边的费用是负的。最常用的是SPFAShortest Path Faster Algorithm或经过特殊处理的Dijkstra 算法通过“势能”函数消除负权。如果找不到最短路径或者最短路径的长度总费用已经为正无穷或超过某个阈值说明无法再增加流量算法结束。如果找到计算这条路径的瓶颈容量 delta。沿着这条路径增加 delta 的流量并更新总费用总费用 delta * 路径总单位费用。更新流量回到步骤2。这个算法会一直执行直到无法再找到从 s 到 t 的路径即已达到最大流。由于每次都是沿着当前“最便宜”的路径推送流量最终得到的就是在达到最大流量的前提下总费用最小的方案。4.2 处理负权边势能技术与 Dijkstra 的改造SPFA 算法虽然能处理负权边但在最坏情况下时间复杂度不佳。而经典的 Dijkstra 算法要求边权非负。为了高效地使用 Dijkstra我们引入“势能”的概念。为每个顶点 u 维护一个势能 h[u]。我们将剩余网络中每条边 (u, v) 的修正费用定义为w(u, v) w(u, v) h[u] - h[v]。可以证明只要初始势能设置合理例如通过一次 SPFA 计算得到并且在每次增广后按照h[u] h[u] dist[u]其中 dist[u] 是本次 Dijkstra 计算出的从源点到 u 的最短距离来更新势能那么修正后的边权 w’ 就总是非负的。这样我们就可以在每次迭代中使用更快的 Dijkstra 算法来求最短路径了。这听起来有点绕但其核心思想是通过势能调整我们把有负权边的图“扭曲”成一个所有边权都为非负的等价图从而可以使用高效的 Dijkstra。这是最小费用流算法实现中的一个关键优化点。# 最小费用流基于势能的Dijkstra核心步骤概念示意 import heapq def min_cost_flow(n, edges, s, t, target_flow): # 构建邻接表 graph[u] [(v, cap, cost, rev_index), ...] graph [[] for _ in range(n)] for u, v, cap, cost in edges: graph[u].append([v, cap, cost, len(graph[v])]) graph[v].append([u, 0, -cost, len(graph[u]) - 1]) # 反向边 potential [0] * n # 顶点势能 prevv [-1] * n # 前驱顶点 preve [-1] * n # 前驱边索引 total_flow 0 total_cost 0 while total_flow target_flow: # 使用Dijkstra基于势能寻找最短增广路 dist [float(inf)] * n dist[s] 0 pq [(0, s)] # (距离 顶点) while pq: d, u heapq.heappop(pq) if dist[u] d: continue for i, (to, cap, cost, rev) in enumerate(graph[u]): if cap 0: # 有剩余容量 new_dist dist[u] cost potential[u] - potential[to] if new_dist dist[to]: dist[to] new_dist prevv[to] u preve[to] i heapq.heappush(pq, (new_dist, to)) if dist[t] float(inf): # 无法到达汇点 break # 更新势能 for v in range(n): if dist[v] float(inf): potential[v] dist[v] # 计算增广量 d target_flow - total_flow v t while v ! s: u prevv[v] edge_index preve[v] d min(d, graph[u][edge_index][1]) # 取瓶颈容量 v u if d 0: break # 增广 v t while v ! s: u prevv[v] edge_index preve[v] graph[u][edge_index][1] - d # 减少正向边容量 rev_index graph[u][edge_index][3] graph[v][rev_index][1] d # 增加反向边容量 total_cost d * graph[u][edge_index][2] # 原费用非修正费用 v u total_flow d return total_flow, total_cost4.3 建模实战物流配送成本优化假设你是一家生鲜电商的物流规划员。你有3个中央仓库W1, W2, W3供应量分别为50、30、40吨。有4个配送站D1, D2, D3, D4需求量分别为20、35、25、40吨。从每个仓库到每个配送站都有运输路线每条路线有最大运力容量和单位运输成本元/吨。目标是满足所有配送站需求的前提下最小化总运输成本。建模步骤顶点建立超级源点 S 和超级汇点 T。顶点集合为 {S, W1, W2, W3, D1, D2, D3, D4, T}。边与容量/费用S - Wi容量 仓库i的供应量费用 0。Wi - Dj容量 路线(i, j)的最大运力费用 路线(i, j)的单位运输成本。Dj - T容量 配送站j的需求量费用 0。求解对这个网络运行最小费用最大流算法。如果算法得到的最大流值等于总需求量20352540120则说明需求可以被满足并且得到了成本最低的运输方案每条边 Wi-Dj 上的流量就是应从仓库i运往配送站j的吨数。如果最大流小于总需求量则说明现有运力网络无法满足所有需求需要检查哪些路线是瓶颈。这里有一个关键点这是一个带供需平衡的运输问题。我们通过设置超级源点/汇点以及连接它们的边的容量将供需约束转化为了流的守恒约束。最小费用流算法在寻找最大流的过程中会自动优先选择费用低的路径从而在满足供需的前提下最小化总成本。注意在实际中配送站的需求和仓库的供应可能是软约束允许少量不满足但有惩罚。这时可以将 S-Wi 和 Dj-T 的边容量设为一个很大的值表示理论上无限供应/接收但同时增加从 S 直接到 T 的边不更常见的做法是引入“虚拟仓库”或“虚拟配送站”并为未满足的部分赋予一个很高的惩罚成本即费用这样算法会在“使用高成本真实运输”和“支付惩罚成本”之间做出权衡。5. 从理论到实践建模竞赛与工程中的避坑指南无论是参加数学建模竞赛还是在实际工程中应用网络流模型都会遇到一些教科书上不会细讲的“坑”。这里分享几点我的切身经验。5.1 数据规模与算法选择网络流算法的时间复杂度与顶点数V和边数E紧密相关。在建模竞赛中数据规模通常不会大到需要极度优化的程度Edmonds-Karp 或基础的 Dinic 算法通常够用。但在工程中面对城市交通网络成千上万个路口或大型通信网络就必须谨慎选择。小规模V, E 200几乎任何算法都可以优先选择代码简单的。中规模V, E 在 10^3 量级Dinic 算法是稳健的选择。如果是最小费用流基于势能的 Dijkstra 实现比纯 SPFA 更可靠。大规模V, E 10^4需要考虑更高级的算法如 Push-Relabel 算法族对于最大流或者使用专门的线性规划求解器如 Gurobi, CPLEX来求解最小费用流问题它本质是一个线性规划问题。有时问题本身具有特殊结构如二分图、平面图可以使用更快的专用算法。一个常见的误区是盲目追求“最优算法”。在工程中数据的获取和清洗、模型的正确构建所花费的时间往往远超过算法运行时间。除非性能是核心瓶颈否则应优先保证代码的清晰、正确和可维护性。5.2 浮点数容量与费用大多数教材和竞赛题都假设容量和费用是整数。但在实际问题中它们很可能是浮点数如 3.5 吨/小时 每公里 0.85 元。最大流如果容量是浮点数Ford-Fulkerson 方法可能无法在有限步内终止因为增广量可能无限小。在实践中通常会对容量进行适当的缩放和取整或者使用能处理浮点数的 Push-Relabel 算法。最小费用流费用是浮点数时最短路径计算中的比较操作要特别小心浮点误差。通常需要设置一个很小的误差容忍度eps如 1e-9当两个费用的差绝对值小于eps时即认为相等。更稳妥的做法是如果可能将费用乘以一个倍数如 100转化为整数进行计算。5.3 无解情况的诊断模型运行后如果得不到可行解如最大流无法满足所有需求诊断问题出在哪里至关重要。检查供需总量首先确认总供应是否大于等于总需求。这是最基础的。检查网络连通性确保从超级源点 S 到超级汇点 T 是连通的。有时因为数据缺失某些仓库到某些配送站之间没有边导致网络不连通。寻找最小割最大流最小割定理指出最大流的值等于最小割的容量。求出最大流后可以在剩余网络中从源点 s 出发标记所有能到达的顶点。这些标记的顶点构成集合 S’未标记的构成集合 T’。那么从 S’ 指向 T’ 的所有边的容量之和就是这个网络的一个最小割。这些边就是网络的瓶颈。在实际问题中检查这些边对应的实际约束哪条管道太细哪个枢纽处理能力不足就能找到系统的关键限制因素。引入虚拟边与惩罚成本如前所述对于软约束可以通过引入高成本的“虚拟运输”边来使问题总有解。这样最终方案中如果使用了这些虚拟边就说明在原网络中无法完全满足约束需要付出额外“惩罚”。这不仅能诊断问题还能量化不满足约束的代价。5.4 模型扩展与变体思考基础的最大流和最小费用流模型是基石但现实问题往往更复杂需要灵活扩展。带时间窗的流物流配送中货物必须在特定时间窗口内送达。这需要将时间维度引入模型常用方法是时间展开。将每个物理地点在每个时间点都复制为一个顶点然后用边连接不同时间点的状态以此来表示等待、运输耗时等。多商品流网络中同时流动多种不可混合的货物如石油和天然气共用管道。不同商品对边的占用可能不同且可能存在互斥。这是一个 NP-Hard 问题通常需要借助整数规划或启发式算法。流量收益与固定成本除了变动运输成本使用某条边或某个设施如开启一个仓库可能产生固定成本。这引入了 0-1 决策变量问题就变成了混合整数规划求解难度大大增加。在面对复杂问题时不要试图用一个极度复杂的流网络模型解决所有事情。很多时候将网络流模型作为一个核心组件嵌入到一个更大的优化框架中如先使用流模型进行快速评估再用元启发式算法进行搜索是更可行的工程思路。例如可以先固定仓库选址0-1决策然后对每个选址方案用最小费用流快速计算运营成本最后用遗传算法或模拟退火来搜索最优的选址方案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价