目录
- 一、引言
- 1.1 网络流问题
- 1.2 “流”的定义
- 1.3 “割”的定义
- 二、最大流最小割
- 2.1 最大流
- 2.2 最小割
- 2.3 最大流最小割定理
- 2.4 最大流最小割定理证明
- 三、Ford–Fulkerson算法
- 3.1 增广路径
- 3.2 剩余图
- 3.3 算法代码
- 3.4 FordFulkerson Demo
一、引言
1.1 网络流问题
网络流问题是图论中的一个重要问题,通常用于描述在网络中物质、信息或资源的流动。它通常建模为一个图,由节点和边构成。
网络流问题包括两个主要方面:网络的结构和流量限制。一般情况下,网络流问题被建模为一个有向图,其中节点表示资源的位置,边表示资源在节点之间的流动路径。每条边都有一个容量限制 (Capacity),表示该路径上能够通过的最大流量。此外,还有源节点 (Source) 和汇点 (Sink) ,分别代表资源的产生地和消耗地。
形式上,我们说一个流网络是具有下述特征的有向图 G=(V,E)
- 每条边
e
关联一个容量,它是一个非负的数,把它记作 c e c_e ce - 存在单一源点 s ∈ V s\in V s∈V
- 存在单一汇点 t ∈ V t\in V t∈V
s
与t
之外的其他结点叫做内部结点
网络流问题有多种形式,其中最经典的是最大流 (max-flow)
和最小割 (min-cut)
问题。
为了使网络流处理更加方便,我们引入三个假设来简化问题:
- 没有边进入源点
s
,且没有边离开汇点t
- 每个结点至少存在一条边与之连接
- 所有的容量都是整数
1.2 “流”的定义
“网络运送交通” 或者 “流” 的含义是什么。我们说一个 s-t 流
是一个函数 f
,它把每条边e
映射到一个非负实数:
f
:
E
→
R
+
f:E\rightarrow R^+
f:E→R+ 值
f
(
e
)
f(e)
f(e) 直观上表示由边e
携带的流量。一个流必须满足下面两个性质:
- (容量条件) 对每个 e ∈ E e\in E e∈E ,我们有 0 ≤ f ( e ) ≤ c e 0\le f(e)\le c_e 0≤f(e)≤ce
- (守恒条件) 除了
s
与t
之外,对每个结点 v v v ,我们有 ∑ e 进入 v f ( e ) = ∑ e 从 v 出来 f ( e ) \sum_{e进入v}f(e)=\sum_{e从v出来}f(e) ∑e进入vf(e)=∑e从v出来f(e)
即一条边上的流不能超过这条边的容量
除了源点和汇点以外,对每个其他结点,进人的流量必须等于离开的流量
一个流 f f f 的值,记作 v ( f ) v(f) v(f) ,定义成在源点产生的流量: v ( f ) = ∑ e 从 s 出来 f ( e ) = f o u t ( s ) v(f)=\sum_{e从s出来}f(e)=f^{out}(s) v(f)=∑e从s出来f(e)=fout(s)
1.3 “割”的定义
考虑把图的结点
v
v
v 分成两个集合:A
与 B
,使得
s
∈
A
s\in A
s∈A,
t
∈
B
t\in B
t∈B。
对于图上的所有的流一定在某个地方从 A
跨到 B
。
形式上,我们说一个 s-t
割是结点集合
v
v
v 的一个划分 (A,B)
,使得
s
∈
A
s\in A
s∈A,
t
∈
B
t\in B
t∈B
一个割(A,B)
的容量记为:
c
(
A
,
B
)
c(A,B)
c(A,B) ,只不过是从 A
出来的所有边的容量之和:
c
(
A
,
B
)
=
∑
e
从
A
出来
c
e
c(A,B)=\sum_{e从A出来}c_e
c(A,B)=∑e从A出来ce
二、最大流最小割
2.1 最大流
给定一个流网络,一个自然的目标就是安排交通以使得有效容量尽可能得到有效的使用。于是,我们将在下面考虑的基本算法问题是:给定一个流网络,找出一个具有最大值的流,即在网络中找到一条从源节点到汇节点的路径,使得沿该路径的流量总和最大化,同时满足每条边的流量限制。
一个最简单的论断是:任何 s-t 流
f
f
f 的值
v
(
f
)
v(f)
v(f) 至多是
C
=
∑
e
从
s
出来
c
e
C=\sum_{e从s出来}c_e
C=∑e从s出来ce 。
如下图所示,我们可以快速得到
v
(
f
)
=
10
+
5
+
15
=
30
v(f)=10+5+15=30
v(f)=10+5+15=30 作为网络流的上界。
但大多时候这个上界是非常弱的,可以使用割的概念来发展一个更一般的手段来设置最大流值的上界。
2.2 最小割
假设我们把一张网络流图的结点划分成两个子集:A
与B
,使得
s
∈
A
s\in A
s∈A 且
t
∈
B
t\in B
t∈B 。那么从到的任何流直观上必须在某个点从 A
穿到 B
,因此用完了从 A
到 B
的某些边的容量。这提醒我们,这个图的每个这样的“割”对最大的流值设立了一个界。也就是说最小割表示从源节点到汇节点的最小流量限制,当这个限制被破坏时,网络将被切割成两个部分。
2.3 最大流最小割定理
从上图不难看出,最大流和最小割冥冥之中存在很强的相关性。先说结论,其实最大流和最小割是等价的。
定理 1:令 f f f 是任何 s-t 流,且 (A,B) 是任意 s-t 割。那么 v ( f ) = f o u t ( A ) − f i n ( A ) v(f)=f^{out}(A)-f^{in}(A) v(f)=fout(A)−fin(A)
这个论断比起之前那个简单的上界要强的多。它是说,通过观察跨过一个割送出的流量,我们恰好可以计算流的值,即离开 A 的流的总量 减去 “返回”进入 A 的总量
同理也可以得到 定理 1 的另外一个版本: v ( f ) = f i n ( B ) − f o u t ( B ) v(f)=f^{in}(B)-f^{out}(B) v(f)=fin(B)−fout(B)
定理 2:令 f f f 是任何 s-t 流,且 (A,B) 是任意 s-t 割。那么 v ( f ) ≤ c ( A , B ) v(f) \le c(A,B) v(f)≤c(A,B)
证明: v ( f ) = f o u t ( A ) − f i n ( A ) ≤ f o u t ( A ) = ∑ e 从 A 出来 f ( e ) ≤ ∑ e 从 A 出来 c ( e ) = c ( A , B ) v(f)=f^{out}(A)-f^{in}(A)\le f^{out}(A)=\sum_{e从A出来}f(e)\le \sum_{e从A出来}c(e)=c(A,B) v(f)=fout(A)−fin(A)≤fout(A)=∑e从A出来f(e)≤∑e从A出来c(e)=c(A,B)
定理2 说的是每个流的值是以每个割的容量为上界的,换句话说,如果我们展示了 G 中任何具有某个值 c ∗ c^* c∗ 的 s-t 割,由定理2 我们立刻就知道 G 中不可能有任何值大于 c ∗ c^* c∗ 的 s-t 流
令 f ˉ \bar f fˉ 为 G 中任何流的最大可能值; ( A ∗ , B ∗ ) (A^*,B^*) (A∗,B∗) 为任何割的最小容量。
Max-flow Min-cut Theorem:
如果 f ˉ \bar f fˉ 是使得在剩余图 G f G_f Gf 中没有 s-t 路径(无增广路径)的一个 s-t 流
那么在 G 中存在一个割 ( A ∗ , B ∗ ) (A^*,B^*) (A∗,B∗)
使得 v ( f ˉ ) = c ( A ∗ , B ∗ ) v(\bar f)=c(A^*,B^*) v(fˉ)=c(A∗,B∗)
因此, f ˉ \bar f fˉ 有 G 中任何流的最大值,且 ( A ∗ , B ∗ ) (A^*,B^*) (A∗,B∗) 有 G 中任何 s-t 割的最小容量.
2.4 最大流最小割定理证明
上述定理声称存在着满足某条所要求性质的割;我们的任务就是找出这样一个割。
令
A
∗
A^*
A∗ 表示
G
f
G_f
Gf中存在一条 s-v 路径的所有结点
v
v
v 的集合
令
B
∗
B^*
B∗ 是所有其他结点的集合:
B
∗
=
V
−
A
∗
B^*=V-A^*
B∗=V−A∗
- 显然 ( A ∗ , B ∗ ) (A^*,B^*) (A∗,B∗) 的确是 s-t 割,它是结点集合 V V V 的划分,且源点 s ∈ A ∗ s\in A^* s∈A∗ ; 汇点 t ∈ B ∗ t \in B^* t∈B∗
如下图所示
- 假设
e
=
(
u
,
v
)
e=(u,v)
e=(u,v) 是 G 中一条边,并且使得
u
∈
A
∗
u\in A^*
u∈A∗ 且
v
∈
B
∗
v\in B^*
v∈B∗ 。
设 f ( e ) = c e f(e)=c_e f(e)=ce ,即该条边的流等于边的容量。- 如果
f
(
e
)
≠
c
e
f(e)\ne c_e
f(e)=ce ,
e
e
e 将是剩余图
G
f
G_f
Gf 中一条前向边,由于
u
∈
A
∗
u\in A^*
u∈A∗,设在
G
f
G_f
Gf 中存在一条 s-u 的路径
再把 e 接到这条路径上,我们将得到 G f G_f Gf 中一条 s-v 路径,与我们假设 v ∈ B ∗ v\in B^* v∈B∗ 矛盾
- 如果
f
(
e
)
≠
c
e
f(e)\ne c_e
f(e)=ce ,
e
e
e 将是剩余图
G
f
G_f
Gf 中一条前向边,由于
u
∈
A
∗
u\in A^*
u∈A∗,设在
G
f
G_f
Gf 中存在一条 s-u 的路径
-
假设 e ′ = ( u ′ , v ) e'=(u',v) e′=(u′,v) 是 G 中的一条边,并且使得 u ′ ∈ B ∗ u'\in B^* u′∈B∗ 且 v ′ ∈ A ∗ v'\in A^* v′∈A∗ 。
设 f ( e ) = 0 f(e)=0 f(e)=0 ,即该条边的流为0。- 如果 f ( e ′ ) ≠ 0 f(e')\ne 0 f(e′)=0 , e ′ e' e′ 将在剩余图 G f G_f Gf 中产生一条后向边 e ′ ′ = ( v ′ , u ′ ) e''=(v',u') e′′=(v′,u′),由于 v ′ ∈ A ∗ v'\in A^* v′∈A∗,设在 G f G_f Gf 中存在一条 s-v’ 的路径。再把 e ′ ′ e'' e′′ 接到这条路径上,我们将得到 G f G_f Gf 中一条 s-u’ 路径,与我们假设 u ′ ∈ B ∗ u'\in B^* u′∈B∗ 矛盾
-
因此所有从 A ∗ A^* A∗ 出来的边完全充满了流,而所有进入 A ∗ A^* A∗ 的边则完全没有用过 v ( f ˉ ) = f o u t ( A ∗ ) − f i n ( A ∗ ) = ∑ e 从 A ∗ 出来 f ( e ) − ∑ e 进入 A ∗ f ( e ) = ∑ e 从 A ∗ 出来 c e − 0 = c ( A ∗ , B ∗ ) v(\bar f)=f^{out}(A^*)-f^{in}(A^*)=\sum_{e从A^*出来}f(e)-\sum_{e进入A^*}f(e)\\\quad\\=\sum_{e从A^*出来}c_e-0=c(A^*,B^*) v(fˉ)=fout(A∗)−fin(A∗)=e从A∗出来∑f(e)−e进入A∗∑f(e)=e从A∗出来∑ce−0=c(A∗,B∗)
三、Ford–Fulkerson算法
Ford–Fulkerson Algorithm
(FFA) 是一种贪婪算法,用于计算流网络中的最大流。 它有时被称为“方法”而不是“算法”,因为在剩余图中查找增广路径的方法尚未完全指定,或者在具有不同运行时间的多个实现中指定。
它由 L. R. Ford Jr. 和 D. R. Fulkerson 于 1956 年出版。“Ford–Fulkerson”这个名称通常也用于 Edmonds–Karp 算法,它是 Ford–Fulkerson 方法的完全定义的实现。 该算法背后的思想如下:只要存在从源(起始节点)到汇点(结束节点)的路径,并且路径中所有边上都有可用容量,我们就沿着其中一条路径发送流量。 然后我们找到另一条路,依此类推。 具有可用容量的路径称为增广路径。
3.1 增广路径
增广路径(Augmenting Path)是在网络流问题中,从源节点到汇节点的一条路径,沿途的边上还有剩余容量可以增加流量的路径。增广路径具有以下特点:
- 起始于源节点
- 终止于汇节点
- 剩余容量大于0
在解决最大流问题时,寻找增广路径是Ford-Fulkerson算法的核心步骤。FFA算法依赖于不断寻找增广路径来增加流量,直到无法找到增广路径为止,从而找到最大流。
3.2 剩余图
剩余图(Residual Graph)是在Ford-Fulkerson算法中用于反映当前流量状态的图。它基于原始网络图构建,用于指示在增广路径上增加流量的可能性。
剩余图的构建方式如下:
- 正向边:对于每条原始图中的边,其残余容量表示该边上还能增加的流量。如果某条边上的流量小于其容量,那么在剩余图中就会有一条边,其容量等于该边的容量减去当前流量。
- 反向边:为了支持反向流,即使在原始图中不存在反向的边,也会在剩余图中创建反向边。这些反向边的容量等于原始图中对应边上的流量,因为可以减少流量或者撤销之前的流。
3.3 算法代码
import collections
class Graph:
"""
This class represents a directed graph using
adjacency matrix representation.
"""
def __init__(self, graph):
self.graph = graph # residual graph
self.row = len(graph)
def bfs(self, s, t, parent):
"""
Returns true if there is a path from
source 's' to sink 't' in residual graph.
Also fills parent[] to store the path.
"""
# Mark all the vertices as not visited
visited = [False] * self.row
# Create a queue for BFS
queue = collections.deque()
# Mark the source node as visited and enqueue it
queue.append(s)
visited[s] = True
# Standard BFS loop
while queue:
u = queue.popleft()
# Get all adjacent vertices of the dequeued vertex u
# If an adjacent has not been visited, then mark it
# visited and enqueue it
for ind, val in enumerate(self.graph[u]):
if (visited[ind] == False) and (val > 0):
queue.append(ind)
visited[ind] = True
parent[ind] = u
# If we reached sink in BFS starting from source, then return
# true, else false
return visited[t]
# Returns the maximum flow from s to t in the given graph
def edmonds_karp(self, source, sink):
# This array is filled by BFS and to store path
parent = [-1] * self.row
max_flow = 0 # There is no flow initially
# Augment the flow while there is path from source to sink
while self.bfs(source, sink, parent):
# Find minimum residual capacity of the edges along the
# path filled by BFS. Or we can say find the maximum flow
# through the path found.
path_flow = float("Inf")
s = sink
while s != source:
path_flow = min(path_flow, self.graph[parent[s]][s])
s = parent[s]
# Add path flow to overall flow
max_flow += path_flow
# update residual capacities of the edges and reverse edges
# along the path
v = sink
while v != source:
u = parent[v]
self.graph[u][v] -= path_flow
self.graph[v][u] += path_flow
v = parent[v]
return max_flow