目录
-
实验目的
- 掌握最大流算法思想。
- 学会用最大流算法求解应用问题。
-
实验内容与结果
问题描述
一个医院有 n 名医生,现有 k 个公共假期需要安排医生值班。每一个公共假期由若干天(假日)组成,第 j 个假期包含的假日用 Dj 表示,那么需要排班的总假日集合为 D=∪j=1kDj 。例如,“五一”假期由5月1日至5月7日一共7个假日组成。“元旦”假期由1月1日至1月3日一共3个假日组成。
每名医生 i 可以值班的假日集合是 Si ,Si⊆D 。例如,李医生可以值班的假日集合包括“五一”假期中的5月3日、5月4日和“元旦”假期中的1月2日。
设计一个排班的方案使得每个假日都有一个医生值班并且满足下面两个条件:
- 每个医生最多只能值班c 个假日;
- 每个医生在一个假期中只能值班1个假日。例如,安排李医生在“五一”假期中的5月4日值班。
根据上述场景完成下面任务:
- 实例化上述场景中的参数,生成数据。
- 设计一个多项式时间的算法求解上述问题,具体任务如下
- 基于生成的数据,设计一个流网络;
- 解释说明该流网络中最大流与值班问题的解的关系;
- 基于生成的数据,计算出排班的方案。
图的构建
首先可以构建医生与假日之间的关系,将每个医生和假日以节点的形式表示,医生和假日间的路径表示该医生可以在这个假日值班。
加上源点与汇点,构成一张流网络,表示所有可能的排班情况。
但是根据题意,图中不仅仅包含医生和假日的对应关系,还包含了医生——假期,假期——假日之间的关系。因此需要在医生节点和假日节点之间加入中间节点假期。
根据题中的限制条件,需要对每个对应关系上的路径进行流量限制:
- 每个医生最多只能值班c个假日
- 每个医生在每个假期中只能值班一个假日
- 每个假日都有一个医生值班
最终构建流网络如下
算法描述
简单算法
首先对求解最大流问题的思想进行简要介绍,以下图为例,其中s表示源点,t表示汇点,每天路径上的权重表示该路径的容量(Capacity)。
引入另一张图,该图上路径的权重为空闲量(Residual),其中空闲量(Residual)= 容量(Capacity)- 流量(flow),在起始状态,空闲量等于容量。
算法运行时,只需要用右边的Residual Graph,不需要用左边的原图。首先开始第一次迭代,寻找从源点s到汇点t的第一条增广路径(Augmenting Path)。找到第一条路径为s -> v2 -> v4 -> t(注意此处路径不唯一)。此时水可以沿着这条路径流到汇点,由于木桶效应,该路径上只能流过2个单位的水,即瓶颈(BottleNeck Capacity)为2。由于让2个单位的水流过这条路径,因此该路径上的空闲量都应该减2。
此时可以看到s -> v2,v2 -> v4两条路径上的空闲量为0,已经饱和了,不能再流入更多的水,因此可以将这两条路径从图中删除。
此时第一轮迭代结束。
开始第二轮迭代,同样找到一条从源点s到汇点t的路径s -> v1 -> v3 -> t,此时同样只能通过2个单位的水。第二轮迭代完成后的剩余量图如图所示。
进行第三次迭代。
这时候已经找不到一条从源点s到汇点t的路径了,迭代结束。
根据公式:流量(Flow) = 容量(Capacity) – 剩余量(Residual)和最终得到的剩余量图可以得到最终结果。
此时求得最大流为5。因为又迭代结果可知,此时已经不能再流入更多的水了。并且可以注意到,从源点流出的水和流入汇点的水的总量都是相同的。
但是,由这种方法得到的最大解不一定是最大的,有可能小于实际的最大解。这是因为我们在迭代的过程中,对于路径的选取是随机的,因此选取路径的顺序会对求得的结果造成影响。
还是同样的图,如果按照与上述不同的迭代过程则会得出以下结果。
此时可以看到得到的最大流为4,小于第一次查询得到的结果,因此该方法得出的结果可能是不正确的。
引入阻塞流(Blocking Flow)的概念,如果图中不允许更多的水流入到管道中,那么此时的流量成为阻塞流,最大流也是一种阻塞流。因此在上图中,求得的结果4是一种阻塞流,求得的最大流5也是一种阻塞流。
至此,可以对最大流问题的求解有一个大致的思路,求解流程图大致为
Ford-Fulkerson
算法思想
在上述的简单算法中,我们得到的结果可能是错误的,是因为求解的过程与增广路径的选择顺序有关,在路径选择的过程中,一旦选择了一条坏的路径,算法就无法找到正确的最大流。
在Ford-Fulkerson算法中,增加了反向路径(Backward Path),允许对已选择的路径进行撤销的操作。
以同样的例子进行分析,首先根据原图构建剩余量图
找到一条增广路径 s -> v1 -> v4 -> t,这条路径上可以流过3个单位的水。更改剩余量图。
此时,沿着这条路径增加一条反向路径,该路径允许对流过的水进行“撤销”,因此这条路径的权重为流过的水的流量。
进行第二次迭代,找到路径s -> v1 -> v3 -> t,进行同样的处理。
如果此时把反向路径都删除掉,就找不到从源点s到汇点t的增广路径了,此时与简单算法相同。
而在Ford-Fulkerson算法中提供了回退的路径,有了这些反向边我们就可以找到从源点s到汇点t的增广路径了。
因此在第二次迭代中可以找到路径s -> v2 -> v4 -> v1 -> v3 -> t,对剩余量图进行更新。
现在已经找不到从源点s流向汇点t的路径了,迭代结束。此时不再需要反向路径,因此删除反向路径,并根据两幅图得到最终的结果。
此时得出结果为5,因此利用Ford-Fulkerson算法得出的流量是最大的。
该算法与简单算法的不同之处就在于增加了反向路径,允许撤销较差路径的选择。在算法流程中,仅增加了一步就保证了结果的正确性。
伪代码
Ford-Fulkerson(n, m) {
n: number of vertices
m: number of edges
f(e): flow of edge
c(e): capacity of edge
initialize the residual graph
maxFlow = 0
f(e) = 0 for every edge
while(can find a path from source to sink)
flow = min(c(e) - f(e)) for every edge in path p
maxFlow += flow
update f(e) for every edge in path p
add backward path
return maxFlow
}
时间复杂度分析
在Ford-Fulkerson算法中,遇到最坏情况时是很慢的。例如在下图中,稍加观察就可以看出来最大流为200。
而在Ford-Fulkerson算法中,可能经过如下迭代过程。首先在第一次迭代中,找到路径s -> v1 -> v2 -> t。
在第二次迭代中,找到路径 s-> v2 -> v1 -> t。
不难发现,在最坏的情况下需要重复以上两个路径各100次才能得出结果,即一共需要进行200次迭代。在每次迭代中流量仅加了一个单位,因此迭代的次数为最大流的值。
假设最大流为f,顶点之间边的数量为m。在每次迭代过程中需要寻找一条路径,该过程的时间复杂度为O(m),在最坏情况下需要迭代f次,则Ford-Fulkerson算法的时间复杂度为O(m*f)。
Edmonds-Karp
算法思想
Edmonds-Karp算法可以看成是对Ford-Fulkerson算法的优化,在对Ford-Fulkerson算法的时间复杂度分析中,我们可以发现路径的选择对算法的时间效率有着很大的影响,因此我们对其进行优化,在每次路径的选择时选择最短的路径。
还是以这种特殊情况为例。
第一次迭代选择最短的路径s -> v1 -> t。
第二次迭代选择最短的路径s -> v2 -> t。
此时找不到从源点s到汇点t的路径了,迭代结束。
可以看到Edmonds-Karp算法相较于Ford-Fulkerson算法而言,时间效率得到了很大的提高。
伪代码
Edmonds-Karp(n, m) {
n: number of vertices
m: number of edges
f(e): flow of edge
c(e): capacity of edge
initialize the residual graph
maxFlow = 0
f(e) = 0 for every edge
while(can find a shortest path from source to sink using bfs)
flow = min(c(e) - f(e)) for every edge in path p
maxFlow += flow
update f(e) for every edge in path p
add backward path
return maxFlow
}
时间复杂度分析
设边数为m,顶点数为n,每次迭代寻找路径的时间复杂度为O(m),迭代次数最多为m*n(证明过程较为复杂),因此Edmonds-Karp算法的时间复杂度为O(m²*n)。
Dicnic’s
算法思想
引入层次图
首先引入层次图(Level Graph)的概念,以下图为例,层次图的构造过程如下:
此时从源点s出发,走一步可以到达的节点为v1和v2。则在层次图中记录节点v1,v2为第一层,并保留s到第一层之间的边。
第二次从节点v1和v2出发,从v1出发走一步可以到达v2,v3和v4,从v2出发走一步可以到达v4,由于v2已经被加入到层次图中,因此只需要加入v3和v4,记为第二层,并记录第一层到第二层间的所有边。
循环上述步骤,则可以得到最终的层次图,其中每个节点都被标识了它所在的层次,也就是与源点s之间的距离。
注意,层次图是基于剩余量图进行构建的,上述过程因为是第一次迭代生成,因此原图与剩余量图相同。
多路增广
在上述dinic’s算法中,算法找到两条最短路径中的其中一条就会返回了,此时与Edmonds-Karp算法相似,每次只能在一条路径上输送水流。引入多路增广的思想,在找到一条最短路径之后进行回溯,再次尝试寻找其他的最短路径。
例如在以下情况中,找到路径s -> v1 -> v3 -> t之后,回溯到节点v1,尝试路径s -> v1 -> v4 -> t,发现长度也是最短的,则标记该路径,再次进行回溯,发现路径s -> v2 -> v4 -> t也是最短路径。
此时找到了三条最短路径,可以将水流从三条路径同时输送,大大提高了算法的执行效率。
当前弧优化
当使用Dinic’s算法时,找到一条最短路径时会结束查找并返回这条路径,在第二次寻找时,由于这条路径即使已经更新了剩余量的值,但是它仍然是一条最短路径,因此程序会试图再次查找这条路径,造成了不必要的查找。因此引入当前弧优化,在查找时跳过已经查找过的路径,搜索其他路径。
以下图为例,在第一次寻找到了黄色路径后,第二次寻找时直接跳过黄色路径上的边,从其他边开始进行寻找。
Dinic’s算法的流程大致如下所示
伪代码
- Dinic’s
Dinic's
n: number of vertices
m: number of edges
f(e): flow of edge
c(e): capacity of edge
initialize the residual graph
maxFlow = 0
f(e) = 0 for every edge
while(can build a level graph using bfs)
while(can find a shortest path from source to sink using dfs in level graph)
flow = min(c(e) - f(e)) for every edge in path p
maxFlow += flow
update f(e) for every edge in path p
add backward path
return maxFlow
在每次找到一条最短路径后重新用bfs进行层次的划分。
- 当前弧优化
dfs(cur, flow)
if cur == sink
return flow
for i in currentArc[cur] to last edge from cur
d = destination of i
minFlow = min(flow, flow of i);
pathFlow = dfs(d, minFlow)
if pathFlow > 0
update edge i
return pathFlow
return 0
在Dinic’s的基础上,加入数组currentArc对当前遍历的边进行保存,在后续的dfs中跳过已遍历过的边。
- 多路增广
dfs(cur, flow)
if cur == sink
return flow
temp = flow
for i in adjList[cur]
minFlow = min(flow, flow of edge<cur, i>);
pathFlow = dfs(i, minFlow)
if pathFlow != 0
update edge<cur, i>
flow -= pathFlow
if (flow == 0)
return temp
return temp - flow
在Dinic’s的基础上,对dfs的过程进行改动,当每次dfs的过程中找出所有路径,因此在bfs建立层次后只需要调用一次dfs寻找路径即可。
时间复杂度分析
设节点数为n,边数为m。
- Dinic’s:需要给每个节点设置层次,时间复杂度为O(n),可能出现m条最短路径,则需要寻找m次,时间复杂度为O(m),每次寻找最短路径的时间复杂度为O(m)。则该算法的时间复杂度为O(n*m²)。
- Dinic’s + 当前弧优化/Dinic’s + 多路增广:时间复杂度为O(n²*m)。
ISAP
算法思想
在Dinic’s算法中,我们在每次找出最短增广路径并更新剩余量图后,都需要重新对节点的层次进行搜索,而在ISAP算法中,我们只需要在每次寻找最短增广路径的过程中就更新每个节点的层次,就可以免去多余的搜索过程。
在ISAP算法中,用离汇点的距离“高度”代替深度表示层次关系,只需要在预处理时进行一次搜索进行处理即可。除此之外,引入“gap”优化,因为我们寻找路径的时候是按照高度严格递减的顺序进行寻找的,如果某个高度的节点数目为0,则说明当前高度的节点不存在,视作“断层”,此时没有路径可以到达汇点,结束搜索。
在寻找最短路径过程中,和Dinic’s类似,但是当一个节点接收到的从上层传来的流量比自身的容量要大时(由于路径是根据高度严格递减来选取的,因此这种情况有可能发生),提高该节点及路径中该节点前面的节点的高度。
下面对该算法的流程进行举例,首先进行初始化,计算图中每个节点的层次。
可以看到节点6沿右边的路径计算的话高度为3,沿左边路径计算的话高度为4,此处取较小值。
根据路径选择的原则,选取路径s->6->4->5->t。从4传到5时流量为4,而5->t的容量为3,此时不会直接返回,而是将节点5的高度加1。并且,将源点s,节点6,节点4的高度加1。
此时路径s->6->4->5->t的层次不再严格递增,因此不能再次选取。但是因为节点层次的改动,s->6->1->2->3->t这条路径可选了,对本条路径作同样的处理。由于从节点6节点到1的流量为3,因此流向后面节点的流量也是3,则不对后面的节点作处理,但是要对节点6和节点s进行加1处理。
此时出现了断层,高度为4的节点没有了,算法结束。
伪代码
ISAP
dfs(cur, flow)
if cur == sink
return flow
temp = flow
for i in adjList[cur]
pathFlow = dfs(i, min(flow, flow of edge<cur, i>))
if pathFlow != 0
update edge<cur, i>
flow -= pathFlow
if flow == 0
return temp
gap[level[cur]]--
level[cur]++
gap[level[cur]]++
return temp - flow
在ISAP中只需要进行一次bfs对整张图进行层次划分,然后一直dfs直到出现“断层”(某一高度的节点数为0)或源点的高度 >= 节点数时寻找结束,此时两种情况都表示不存在路径能达到汇点。
时间复杂度分析
设节点数为n,边数为m。ISAP的时间复杂度为O(n²*m)。
算法效率测试
回到值班问题,假设有3名医生,2个假期,每个假期有3个假日,每人最多值班2天,根据这些数据可以生成流网络如下。由于在求解过程中,我们并不关心每个节点的实际意义,因此在下图中都以普通节点的形式表示,并给每个节点进行标号。
根据算法的输出结果得到答案:
排班情况为
5.1 | 5.2 | 5.3 | 5.4 | 5.5 | 5.6 |
医生1 | 医生2 | 医生3 | 医生4 | 医生5 | 医生6 |
该排班情况满足每个医生在一个假期中只值班一天,且每个假日只有一个医生值班。
接下来测试不同规模下各算法的时间效率(更改一个变量的数据规模时固定其他变量的值)
- Ford-Fulkerson
医生人数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 308.664 | 444.108 | 593.38 | 739.38 | 906.4 |
假期数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 133.923 | 139.48 | 134.925 | 134.321 | 123.731 |
假日数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 132.229 | 132.061 | 141.296 | 128.718 | 137.522 |
值班天数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 128.133 | 128.778 | 127.895 | 128.804 | 134.371 |
- Edmonds-Karp
医生人数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 402.688 | 1234.05 | 2372.98 | 4044.2 | 6468.54 |
假期数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 395.059 | 1288.05 | 2473.18 | 4460.25 | 6775.39 |
假日数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 146.175 | 181.206 | 231.234 | 280.667 | 378.958 |
值班天数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 123.865 | 128.758 | 128.621 | 134.51 | 139.019 |
- Dinic’s
医生人数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.5951 | 0.9374 | 0.8973 | 1.266 | 1.5043 |
假期数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.7184 | 0.7463 | 0.9493 | 1.1451 | 1.392 |
假日数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.2219 | 0.3381 | 0.3834 | 0.4356 | 0.3809 |
值班天数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.4308 | 0.5737 | 0.3987 | 0.3862 | 0.7015 |
- Dinic’s + 当前弧优化
医生人数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.6123 | 1.1654 | 1.0171 | 1.2579 | 2.0568 |
假期数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.5302 | 0.9307 | 1.0471 | 1.3398 | 1.4642 |
假日数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.2773 | 0.3003 | 0.3607 | 0.4491 | 0.5115 |
值班天数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.3143 | 0.4009 | 0.3536 | 0.5084 | 0.4361 |
- Dinic’s + 多路增广
医生人数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.4547 | 0.8318 | 0.994 | 1.8248 | 1.4499 |
假期数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.5212 | 1.231 | 0.9024 | 1.0336 | 2.3399 |
假日数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.2967 | 0.3176 | 0.317 | 0.5482 | 0.4454 |
值班天数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.3328 | 0.5725 | 0.3939 | 0.3829 | 0.3573 |
- ISAP
医生人数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.8908 | 1.6582 | 1.3466 | 1.8106 | 2.6072 |
假期数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.5235 | 0.7185 | 0.9927 | 1.2273 | 1.5357 |
假日数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.3338 | 0.348 | 0.4232 | 0.4827 | 0.4801 |
值班天数 | 100 | 200 | 300 | 400 | 500 |
运行时间/ms | 0.3297 | 0.552 | 0.3893 | 0.4242 | 0.37 |
固定单一变量值,对以上不同的算法效率进行对比:
在不同算法之间进行对照可以发现,Edmonds-Karp算法是最慢的,其次是Ford-Fulkerson算法,相较而言,其余几种算法的运行时间几乎可以忽略不计。
-
实验总结
在本次实验中,将医生的值班问题抽象为了图的最大流问题进行求解。在求解过程中,采用了不同的算法,并且在保证算法正确性的基础上采用了不同的优化策略,比较了不同算法之间的时间效率。