问题描述
看这样一张图,表示一个材料的运输路线。从
s
s
s处生产材料和运入
t
t
t的材料的速率是相同的,从
s
s
s到
t
t
t有多条运输路径,上面标明了每条路径能运输的材料的最大数量(分母)。现在请问
s
s
s在单位时间内生产多少材料能最大化利用这个运输路径的容量?
问题抽象
将s定义为“源”,将t定义为“汇”。
材料的运输路径可以被抽象成一张有向图“
G
G
G”。
从
s
s
s到
t
t
t有一系列中转点,它们的集合(包括
s
s
s和
t
t
t)记为“
V
V
V”。
对于
V
V
V中的任意两个中转点
u
u
u和
v
v
v,我们用
c
(
u
,
v
)
c(u,v)
c(u,v)表示从
u
u
u通往
v
v
v的有向路径的最大容量。用
f
(
u
,
v
)
f(u,v)
f(u,v)表示实际从
u
u
u运往
v
v
v的物品数量。
G
G
G中的一个从
s
s
s运输物资到
t
t
t的方案实例即为“流”,在图中写在分子的位置上。这个流需要满足以下几条性质:
- 容量限制。从一个点流向另一个点的实际流量大于等于 0 0 0且不能超过从这个点通到另一个点的有向边的容量。 0 ≤ f ( u , v ) ≤ c ( u , v ) 0≤f(u,v)≤c(u,v) 0≤f(u,v)≤c(u,v)
- 反对称性。从 u u u到 v v v有一条有向路径,则如果从 u u u到 v v v的流量为 f f f,从 v v v到 u u u的流量被记为 − f -f −f。 f ( u , v ) = − f ( v , u ) f(u,v)=-f(v,u) f(u,v)=−f(v,u)
- 流量守恒。除了源点和汇点,流入一个结点的流量必定等于流出这个节点的流量。 ∀ u , v ∈ V , ∑ t ∈ V f ( u , t ) = ∑ t ∈ V f ( t , v ) \forall u,v \in V,\sum_{t\in V}f(u,t)=\sum_{t\in V}f(t,v) ∀u,v∈V,∑t∈Vf(u,t)=∑t∈Vf(t,v)(为了方便理解,这里 u u u代表流入, v v v代表流出,其实因为 ∀ \forall ∀的任意性只需用一个字母表示即可)。
整个图的流量记为
∣
f
∣
\vert f\vert
∣f∣,则
∣
f
∣
=
∑
v
∈
V
f
(
s
,
v
)
−
∑
u
∈
V
f
(
u
,
s
)
\vert f\vert=\sum_{v\in V}f(s,v)-\sum_{u\in V}f(u,s)
∣f∣=∑v∈Vf(s,v)−∑u∈Vf(u,s)(流出源点的流量减去流入源点的流量)
则本问题的目标就是求得
∣
f
∣
\vert f\vert
∣f∣的最大值。
对于多源多汇问题,我们只需要追加一个“终极源”和“终极汇”即可转化为单源单汇问题。
Ford-Fulkerson算法
最小割
最大流=最小割。
(来自本人真情实感的吐槽:这个地方老师的讲解还有书上的表示我完全没有搞懂,明明很直白的东西偏偏要抽象成人看不懂的鬼话,虽然很严谨但的确很不方便人理解啊!!所以我采用很傻瓜的作图法来解释啥是最小割)
之前在另一篇博文里看到一个很通俗的关于最小割的解释,我尝试用自己的语言和图片也表达得直白一些。
割是什么?
简单地说就是把一大坨顶点分成两片
如上图,顶点集
V
=
{
a
,
b
,
c
,
d
,
e
}
V=\{a,b,c,d,e\}
V={a,b,c,d,e},割
C
1
C_1
C1把这个顶点集分成了
{
a
,
b
,
d
}
、
{
c
,
e
}
\{a,b,d\}、\{c,e\}
{a,b,d}、{c,e}两部分,割
C
2
C_2
C2把这个顶点集分成了
{
a
,
b
,
c
,
d
}
、
{
e
}
\{a,b,c,d\}、\{e\}
{a,b,c,d}、{e}两部分。
割有什么作用?
简单粗暴地说,一个割可以直接把它之前的顶点全体视作是源点,之后的所有顶点全体视作是汇点。这样得到“源一片点”到“汇一片点”的流量就是割经过的所有从“源一片点”流向“汇一片点”的边的容量之和(从汇流回源的直接忽略)。
一个简单的例子
很显然这条路有3种割法:
而这条路的最大流很显然是1。
发现了什么?
我们要找的最大流就是我们能够割出来的所有方案里面得到的割的容量和最少的那一组。
为什么?
因为流量不能大于容量。
我们已经可以通过“割”把源点经过一大片结点再到汇点的过程抽象成“源一片点”(靠近源点的那一大片点)直接到“汇一片点”了(靠近汇点的那一大片点),现在从“源一片点”到“汇一片点”的“抽象容量”就是割经过的边的容量和,很显然,我们所求得的最大流不能超过这个“抽象容量”。而“源一片点”和“汇一片点”的分割有很多很多种,能得到很多很多的“抽象容量”,最大流都不允许超过这些“抽象容量”。因此最大流就是我们能够割出来的所有方案里面得到的割的容量和最少的那一组。
一个稍微复杂一些的例子
回到文章最开头的那一张图,我们来找找这张图的最小割。
对于这个图,它的最小割是23,因此它的最大流也是23。
算法的思想
简单地说,这个算法每一次从一张图中找一条从
s
s
s到
t
t
t的基本路径并把这个路径记录到另一个图中。每条路径的容量都等于这条路径里容量最小的那条边的容量,最后把这些容量加在一起即可。其实可以把这个过程看成是在拟合最小割(每次都加最小边的流量)。
要使找到的基本路径不重复,我们就需要更新这张图,要使我们找到的基本路径是最优的路径,我们可能选了一条边之后还想改选另外一条边,相当于需要进行“反悔”操作。
残存网络
需要被更新的图就是残存网络。一开始这个网络等于我们要找最大流的原图。我们需要从这张图中找一条从
s
s
s到
t
t
t的基本路径并把这个路径记录到另一个图中,这个路径被我们称为增广路径,这个路径的容量是里面的有向边中容量最小的那一个。在找到这条基本路径之后,我们通过下面这个操作先更新图,再给予我们一个反悔的机会(抵消操作)。比如我们找到一条
s
−
>
u
−
>
v
−
>
t
s->u->v->t
s−>u−>v−>t的路径,用
f
f
f来表示每条边的流量(
f
f
f的初值就是
c
c
c),
∣
f
∣
|f|
∣f∣来表示整个路径的流量,下面首先将
f
(
s
,
u
)
−
=
∣
f
∣
,
f
(
u
,
v
)
−
=
∣
f
∣
,
f
(
v
,
t
)
−
=
∣
f
∣
f(s,u)-=|f|,f(u,v)-=|f|,f(v,t)-=|f|
f(s,u)−=∣f∣,f(u,v)−=∣f∣,f(v,t)−=∣f∣(更新原图),然后再给予一个反向的容量
f
(
u
,
s
)
+
=
∣
f
∣
,
f
(
v
,
u
)
+
=
∣
f
∣
,
f
(
t
,
v
)
+
=
∣
f
∣
f(u,s)+=|f|,f(v,u)+=|f|,f(t,v)+=|f|
f(u,s)+=∣f∣,f(v,u)+=∣f∣,f(t,v)+=∣f∣(给予反悔机会)。
前面更新原图的操作应该很好理解。我既然已经找到一条边了算上它的流量了,那我当然要把这个流量给减掉。
后面的反向容量(相当于添加了一条反向边)的操作其实也容易理解的。试想你在操作一个小人走一个迷宫,你通过画线指引这个小人的方向。当你不小心引导这个小人走到了一处死角的时候该怎么办?你会当然会画一根往回走的反向的路把他引导回去。因此增添反向边的操作相当于就是给这个算法留一个后退的余地。当图中的流动可以有更好的分配的时候,它可以通过反向边撤回一个不大好的分配。而如果没有反向边的话,选定即被定死,算法运行结束会有很多容量浪费。
一个例子
还是这张图,我们来找它的增广路径
一段代码
参考最大流问题