第 48 课 - 网络流基础
学习目标
通过本课学习,你将能够:
- 理解网络流的核心概念,包括源点、汇点、容量和流量。
- 掌握最大流问题的定义及其与增广路径的关系。
- 理解并实现基础的 Ford-Fulkerson 算法(使用 DFS 寻找增广路径)来计算最大流。
- 初步了解最大流最小割定理,并认识其重要性。
核心概念
1. 什么是网络流?
想象一个供水管道网络。每条管道都有其最大输水能力(容量)。我们有一个水源(源点 s)和一个需要供水的目的地(汇点 t)。我们希望找到一个从源点到汇点的送水方案,使得总输水量最大,同时保证任何一条管道中的实际流量不超过其容量。这就是网络流(Network Flow) 问题。
- 有向图
G = (V, E): 代表网络,V是顶点集合,E是有向边(管道)集合。 - 容量函数
c(u, v): 对每条边(u, v) ∈ E,c(u, v) ≥ 0表示该边的最大通过能力。如果(u, v) ∉ E,则c(u, v) = 0。 - 流函数
f(u, v): 表示边(u, v)上的实际流量。它必须满足:- 容量约束:
0 ≤ f(u, v) ≤ c(u, v) - 流守恒: 对于除源点
s和汇点t的任何中间节点v,所有流入v的流量之和等于所有从v流出的流量之和。
- 容量约束:
最大流问题:在满足上述约束的前提下,找到一个从 s 到 t 的流 f,使得从 s 流出的总流量(或等价地,流入 t 的总流量)最大化。这个最大总流量称为最大流值。
2. 增广路径与残量网络
解决最大流问题的一个核心思想是增广路径(Augmenting Path)。它是在残量网络中一条从 s 到 t 的路径。
-
残量网络
Gf: 给定一个当前流f,残量网络Gf是一个与原图结构相似的图,它包含了所有还能增加流量的边。- 对于原图中的每条边
(u, v),如果f(u, v) < c(u, v),则Gf中包含一条正向边(u, v),其残量容量为c(u, v) - f(u, v)。 - 关键的一点是,
Gf中也包含反向边(v, u),其残量容量为f(u, v)。这相当于给了算法一个“撤销”之前流量分配的机会,允许流量在后续迭代中被重新调整,这是算法能够找到最优解的关键。
- 对于原图中的每条边
-
增广路径: 在残量网络
Gf中任何一条从s到t的路径P。沿着这条路径,我们可以增加min_{(u,v) ∈ P} (c_f(u, v))的流量,这个值称为路径P的瓶颈容量。
代码示例
下面是一个使用 Python 实现的 Ford-Fulkerson 算法(采用 DFS 寻找增广路径)。我们将用一个邻接字典来表示网络,其中键是节点,值是其邻居及对应的容量字典。
class MaxFlow:
def __init__(self, graph):
"""
初始化最大流求解器。
:param graph: 邻接表表示的图。例如:{'s': {'A': 10, 'B': 5}, 'A': {'B': 15, 't': 10}, ...}
"""
self.graph = graph # 原始容量图
self.flow = {} # 存储当前流,格式与graph相同,但存储流量值
self._init_flow()
def _init_flow(self):
"""初始化流量图,所有边的流量初始为0。"""
for u in self.graph:
self.flow[u] = {}
for v in self.graph[u]:
self.flow[u][v] = 0
# 确保反向边的流量字段存在,即使原始图中没有这条边
if v not in self.flow:
self.flow[v] = {}
if u not in self.flow[v]:
self.flow[v][u] = 0
def ford_fulkerson(self, source, sink):
"""
使用Ford-Fulkerson算法计算从source到sink的最大流。
:param source: 源点
:param sink: 汇点
:return: 最大流值
"""
max_flow = 0
parent = {} # 用于在DFS中记录路径
while self._dfs(source, sink, parent):
# 找到一条增广路径,计算瓶颈容量
path_flow = float('inf')
v = sink
while v != source:
u = parent[v]
# 瓶颈容量是路径上残量容量的最小值
residual_capacity = self._get_residual_capacity(u, v)
path_flow = min(path_flow, residual_capacity)
v = u
# 沿路径更新流量
v = sink
while v != source:
u = parent[v]
# 增加正向边流量
self.flow[u][v] += path_flow
# 减少反向边流量(等价于增加反向边的残量容量)
self.flow[v][u] -= path_flow
v = u
max_flow += path_flow
parent.clear() # 清空路径记录,准备下一次DFS
return max_flow
def _dfs(self, source, sink, parent):
"""
深度优先搜索寻找一条从source到sink的增广路径(在残量网络中)。
:return: 是否找到路径
"""
visited = set()
stack = [source]
visited.add(source)
while stack:
u = stack.pop()
# 遍历所有邻接节点(包括正向和反向边)
for v in self.graph.get(u, {}):
# 检查正向边的残量容量
residual = self._get_residual_capacity(u, v)
if v not in visited and residual > 0:
parent[v] = u
if v == sink:
return True # 找到增广路径
visited.add(v)
stack.append(v)
# 注意:为了简化,这个DFS没有显式遍历所有可能的反向边。
# 实际上,我们的残量网络是通过`_get_residual_capacity`函数动态计算的,
# 它已经考虑了原始图和当前流,隐含了反向边的概念。
return False
def _get_residual_capacity(self, u, v):
"""计算边(u, v)在残量网络中的容量。"""
# 原始图的容量(如果存在)
original_capacity = self.graph.get(u, {}).get(v, 0)
# 当前流的流量
current_flow = self.flow.get(u, {}).get(v, 0)
# 残量容量 = 原始容量 - 当前流
return original_capacity - current_flow
# 使用示例
if __name__ == "__main__":
# 定义一个示例网络图(邻接表)
# s -> A (10), s -> B (5)
# A -> B (15), A -> t (10)
# B -> t (10)
graph = {
's': {'A': 10, 'B': 5},
'A': {'B': 15, 't': 10},
'B': {'t': 10},
't': {} # 汇点没有出边
}
solver = MaxFlow(graph)
source = 's'
sink = 't'
max_flow_value = solver.ford_fulkerson(source, sink)
print(f"从 {source} 到 {sink} 的最大流值为: {max_flow_value}")
# 输出应为: 从 s 到 t 的最大流值为: 15
# 解释:路径1: s->A->t (流量10),路径2: s->B->t (流量5),总流量15。
# 注意:虽然A->B的容量是15,但s->A只有10,所以无法充分利用。
实践练习
练习 1:基础计算
在上面的代码示例网络中,手动模拟一次算法的执行过程。写出前两条增广路径以及它们的瓶颈容量。最终的最大流值是否与代码输出一致?
练习 2:设计自己的网络
创建一个新的网络流图,至少包含 5 个节点(包括源点和汇点),使得最大流值能够达到某个你设定的值(例如 20)。用代码验证你的设计。 提示:确保你设计的图中,源点有足够容量的出边,汇点有足够容量的入边,并且中间路径相互连接。
练习 3:理解最大流最小割
研究上面代码运行结束后,流量分布(self.flow 字典)。找出一个割(一组边,移除它们后 s 和 t 不再连通),使得割中边的容量之和等于你计算出的最大流值。这个割就是最小割。尝试写出你的割。
常见错误
- 忽略反向边的作用:这是初学者最常见的错误。在残量网络中,反向边
(v, u)的容量等于当前流f(u, v)。它允许算法在后续迭代中将u -> v的部分流量“退回”,从而调整之前的非最优分配。如果实现时没有正确处理反向边,算法可能无法找到最大流。 - 错误的 DFS 实现:DFS 在寻找增广路径时,需要遍历所有邻接边,并正确计算残量容量。常见的错误包括:没有考虑所有可能的边(特别是当图是用邻接矩阵表示时,需要遍历所有节点),或者在更新流量时忘记同时更新正向和反向边的流量。
- 误解“增广路径”的含义:增广路径存在于残量网络中,而不是原始容量图中。残量网络是动态变化的,每进行一次流量更新后都需要重新计算。在代码中,这通常通过实时计算
original_capacity - current_flow来实现,而不是维护一个独立的残量图数据结构。 - 无法终止:如果容量值是无理数,Ford-Fulkerson 算法可能不会终止。在实际编程中,我们通常假设容量为整数或有理数。对于整数容量,算法保证终止,且最大迭代次数不超过最大流值。
小结
- 网络流是一个在有向图中,满足容量约束和流守恒条件下,最大化从源点到汇点流量的问题。
- 残量网络和增广路径是解决最大流问题的关键工具。残量网络反映了当前状态下还可以增加(或减少)的流量潜力。
- Ford-Fulkerson 算法的核心思想是:不断地在残量网络中寻找增广路径,并沿着该路径增加流量,直到找不到为止。其中,
反向边的概念至关重要,它使得流量可以被重新分配。 - 使用深度优先搜索(DFS)寻找增广路径的 Ford-Fulkerson 方法,时间复杂度为
O(E * max_flow),对于大整数流可能较慢。更高效的算法如 Edmonds-Karp(使用 BFS)或 Dinic 算法会在后续课程中探讨。 - 最大流最小割定理是网络流理论中最深刻的结论之一:在一个网络中,最大流的值等于最小割的容量。这个定理将流问题与割问题(一个图划分问题)联系起来,在理论和应用上都非常重要。
- 掌握了网络流基础后,你将能够解决许多看似不同的问题,如二部图匹配(下一课内容)、项目选择、棒球比赛淘汰赛等,它们都可以转化为网络流模型。