文章目录
前言
学校老师在讲流网络的时候进度很快,流网络的实验也更换了新的情景,网上找不到实现思路。
这篇文章希望让大家更好的理解流网络,以及如何使用流网络解决现实生活中的问题
一、问题描述
1996 年 9 月 10 日,《旧金山纪事报》的体育版上登载了《巨人队正式告别 NL 西区比赛》一文,宣布了旧金山巨人队输掉比赛的消息。当时,圣地亚哥教士队凭借 80 场胜利暂列西区比赛第一,旧金山巨人队只赢得了 59 场比赛,要想追上圣地亚哥教士队,至少还得再赢 21 场比赛才行。然而,根据赛程安排,巨人队只剩下 20 场比赛没打了,因而彻底与冠军无缘(摘自http://www.matrix67.com/blog/archives/5190)。
有趣的是,报社可能没有发现,其实在两天以前,也就是 1996 年 9 月 8 日,巨人队就已经没有夺冠的可能了。那一天,圣地亚哥教士队还只有 78 场胜利,与洛杉矶道奇队暂时并列第一。此时的巨人队仍然是 59 场胜利,但还有 22 场比赛没打。因而,表面上看起来,巨人队似乎仍有夺冠的可能。然而,根据赛程安排,圣地亚哥教士队和洛杉矶道奇队互相之间还有 7 场比赛要打,其中必有一方会获得至少 4 场胜利,从而拿到 82 胜的总分;即使巨人队剩下的 22 场比赛全胜,也只能得到 81 胜。由此可见,巨人队再怎么努力,也不能获得冠军了。
在美国职业棒球的例行赛中,每个球队都要打 162 场比赛(对手包括但不限于同一分区里的其他队伍,和同一队伍也往往会有多次交手),所胜场数最多者为该分区的冠军;如果有并列第一的情况,则用加赛决出冠军。在比赛过程中,如果我们发现,某支球队无论如何都已经不可能以第一名或者并列第一名的成绩结束比赛,那么这支球队就提前被淘汰了(虽然它还要继续打下去)。从上面的例子中可以看出,发现并且证明一个球队已经告败,有时并不是一件容易的事。
关于这个事情有一个有趣的故事,下面是一段对话:
“看到上周报纸上关于爱因斯坦的那篇文章了吗?……有记者请他算出三角比赛的数学公式。你知道,一支球队赢得了很多剩余的比赛,其他球队则赢这个赢了那个。这个比赛到底有多少种可能性?哪个球队更有优势?”
“他到底知道吗?”
“显然他知道的也不多。上周五他选择道奇队没有选巨人队。”
上面的表是四个球队的比赛情况,现在的问题是哪些球队有机会以最多的胜利结束这个赛季?可以看到蒙特利尔队因最多只能取得 80 场胜利而被淘汰,但亚特兰大队已经取得 83 场胜利,蒙特利尔队因为wi + ri < wj 而被淘汰。费城队可以赢83场,但仍然会被淘汰。 。 。如果亚特兰大输掉一场比赛,那么其他球队就会赢一场。所以答案不仅取决于已经赢了多少场比赛,还取决于他们的对手是谁。
请利用最大流算法给出上面这个棒球问题的求解方法。
二、实验内容
1.解释为什么最大流能解决这个问题。
为了说明这一点,我们展示一组虚构的数据(这是在 1996 年 8 月 30 日美国联盟东区比赛结果的基础上略作修改得来的),如下表所示。
有没有什么通用的方法,能够根据目前各球队的得分情况和剩余的场次安排,有效地判断出一个球 队是否有夺冠的可能? 1966 年, Schwartz 在一篇题为 Possible winners in partially completed tournaments 的论文中指出,其实刚才提出的问题,可以归结为一个简单而巧妙的网络流模型。
假设图 1 是一个交通网示意图,其中 s 点是出发点(或者说入口), t 点是终点(或者说出口),
其余所有的点都是交叉路口。点与点之间的连线代表道路,所有道路都是单行道,汽车只能沿着箭头方向行驶。
由于道路的宽度、限速不同等原因,每条道路都有各自的最大车流量限制,我们已经把它们标在了图上。例如,
道路 b → c 的最大车流量为 6 ,这就表示当你站在这条道路上的任意一点时,单位时间内最多可以有 6 辆
汽车经过你所在的位置。假设在 s 点处源源不断地有汽车想要到达 t 点,这些汽车已经在 s 点处排起了长队。
那么,应该怎样安排每条道路的实际流量,才能让整个交通网络的总流量最大化,从而最大程度地缓解排队压力呢?
其中一种规划如图 2 所示,各道路上标有实际流量和最大流量,此时整个交通网络的流量为 6 。
由 s 点出发的汽车平分两路,这两条路的实际流量均为 3 ,分别驶入 a 路口和 b 路口。在 b 路口处还有
另外一条驶入的路,流量为 2 。从 s → b 和 a → b 这两条路上来的车合流后驶入 b → c ,因而 b → c
的实际流量就是 5 。这 5 个单位的车流量是 c 路口的驶入汽车的唯一来源,这些车分为两拨,其中 1 个单位
的车流量进入 c → a 路,另外 4 个单位的车流量直接流向了终点。 a 路口的情况比较复杂,其中有两条路是
驶入的,实际流量分别为 3 和 1 ;有两条路是驶出的,实际流量分别为 2 和 2 。
给定一个交通网络图,给出图中每条道路允许的最大流量,再指定一个点作为源点(通常用 s 表示),指定
一个点作为汇点(通常用 t 表示)。如果为每条道路设定一个实际流量(通常可以假设流量值为整数),使得每
条道路的实际流量都不超过这条道路的最大流量,并且除了s点和t点之外,其他每个点的总流入量都等于总流出量,
我们就说这是一个网络流。由于制造流量的只有 s 点,消耗流量的只有 t 点,其他点的出入都是平衡的,因此
很容易看出,在任意一个网络流中, s 点的总流出,一定等于 t 点的总流入。我们就把这个数值叫做网络流的
总流量。我们通常关心的是,如何为各条道路设定实际流量,使得整个图的总流量最大。
图 2 的流量显然还没有达到最大,因为我们还可以找出一条从 s 到 t 的路径,使得途中经过的每条道路的流量都还没满。例如, s → a → b → c → t 就是这样的一条路径。把这条路径上的每条道路的实际流量都加 1 ,显然能够得到一个仍然合法,但总流量比原来大 1 的网络流。新的网络流如图 3 所示。
我们还能进一步增加流量吗?还能,但是这一次就不容易看出来了。考虑路径 s → b → a → c → t ,注
意这条路径中只有 s → b 段和 c → t 段是沿着道路方向走的,而 b → a 段和 a → c 段与图中所示的箭头
方向正好相反。现在,我们把路径中所有与图中箭头方向相同的路段的实际流量都加 1 ,把路径中所有与图中箭
头方向相反的路段的实际流量都减 1 。于是,整个网络变成了图 4 的样子。此时你会发现,这番调整之后, s
点的流出量增加了 1 个单位, t 点的流入量增加了 1 个单位,其他所有点的出入依旧平衡。因此,新的图仍然
是一个合法的网络流,并且流量增加了 1 个单位。
现在,我们有了两种增加网络流流量的通用模式,考虑到前者实际上是后者的一个特例,因而它们可以被归结
为一种模式。首先,从 s 点出发,寻找一条到 t 点的路径,途中要么顺着某条流量还没满的(还能再加流量的)
道路走一步,要么逆着某条流量不为零的(能够减少流量的)道路走一步。我们把这样的路径叫做“增广路径”。
找到一条增广路径之后,增加或者减少沿途道路的流量,从而在保证网络流仍然合法的前提下,增加网络流的总
流量。
1956 年,美国数学家 Lester Ford, Jr. 和 Delbert Fulkerson 共同发表了一篇题为 Maximal flow through a network 的论文,论文中指出,为了找出一个网络中的最大流量,我们只需要用到上面这种流量改进模式。换句话说,如果不能用上述模式增加某个网络流的流量,即如果图中不存在增广路径,那么此时的流量就一定达到最大值了。
例如,在图 4 中,网络流的流量已经达到了 8 个单位,但我们再也找不到增广路径了。这就说明,图 4 中的流量已经不能再改进,流量最大就是 8 了。
假设现在有这么一个网络流,它里面不存在任何增广路径,这就意味着,从 s 点出发,沿着尚未满流的道路走,或者逆着尚有流量的道路走,是无法走到 t 点的。我们把从 s 点出发按此规则能够走到的所有点组成的集合记作 U 。根据集合 U 的定义,任何一条从 U 内走到 U 外的道路一定都已经满流了,任何一条从 U 外走进 U 内的道路流量一定都为零,否则的话集合 U 都还能进一步扩大。例如,在图 4 中,集合 U 就是 {s, a, b} 。驶出 U 的道路有两条,分别是 a → t 和 b → c ,它们都已经满流了;驶入 U 的道路只有 c → a ,它的流量一定为零。我们不妨把所有驶出 U 的道路都涂成红色,把所有驶入 U 的道路都涂成蓝色。
现在,保持集合 U 的范围和道路的颜色不变,修改图中各道路的实际流量,使之成为任意一个合法的网络流。下面这个重要的结论始终成立:红色道路里的总流量,减去蓝色道路里的总流量,总是等于整个网络流的流量。比如,把图 4 中的网络流改成图 2 或者图 3 的样子,那么道路 a → t 的流量加上道路 b → c 的流量,再减去道路 c → a 的流量,一定都等于整个网络流的总流量。为什么?其实道理很简单,别忘了,制造流量的只有 s 点,消耗流量的只有 t 点,其他点只负责转移流量,因而不管网络流长什么样,如果从 U 里边流出去的流量比从外边流入 U 的流量更多,多出来的部分就一定是 s 制造的那些流量。
对于任意一个网络流,这些红色道路的总流量减去这些蓝色道路的总流量,就可以得出整个网络流的总流量,这实际上给出了网络流的流量大小的一个上限——如果在某个网络流中,所有的红色道路都满流,并且所有蓝色道路都无流量,那么流量值便达到上限,再也上不去了。然而,这个上限刚才已经实现了,因而它对应的流量就是最大的了。至此,我们便证明了 Ford 和 Fulkerson 的结论。
根据这一结论,我们可以从零出发,反复寻找增广路径,一点一点增加流量,直到流量不能再增加为止。这种寻找最大流的方法就叫做 Ford–Fulkerson 算法。
回到我们这个题目
一支队伍必然落败,意即这支队伍在最好的局面下也拿不到第一。让我们来分析一下底特律可能的最好局面。显然,对于底特律来说,最好的局面就是,剩余 27 场比赛全都赢了,并且其他四个队在对外队的比赛中全都输了。这样,底特律将会得到 76 胜的成绩,从而排名第一。但是,麻烦就麻烦在,剩下的四个队内部之间还会有多次比赛,其中必然会有一些队伍获胜。为了让底特律仍然排在第一,我们需要保证剩下的四个队内部之间比完之后都不要超过 76 胜的成绩。换句话说,在纽约、巴尔的摩、波士顿、多伦多之间的 3 + 8 + 7 + 2 + 7 + 0 = 27 场比赛中,纽约最多还能胜 1 次,巴尔的摩最多还能胜 4 次,波士顿最多还能胜 7 次,多伦多最多还能胜 16 次。只要这 27 场比赛所产生的 27 个胜局能够按照上述要求分给这四个队,底特律就有夺冠的希望。
事实上,在图 5 所示的网络中,可能的最大流量是 26 (其中一种网络流方案如图 6 所示),没有达到 27 ,因而底特律也就必败无疑了。类似地,我们也可以为其他队伍建立对应的网络,依次计算每个队伍的命运,从而完美解决了棒球赛淘汰问题。
2.尽可能实验优化的最大流算法。
Ford-Fulkerson方法
1.初始化:
为每个边分配一个容量,表示这条边最多能通过多少单位的流。
设置从源点 s 到汇点 t 的初始流为零。
2.寻找增广路径:
使用广度优先搜索(BFS)或深度优先搜索(DFS)等方法来寻找从源点 s 到汇点 t 的路径,在这条路径上还能增加流量。
每条边上的剩余容量必须大于零,即还有空间可以用来传输更多的流。
3.更新流:
对于找到的每条增广路径,确定最小的剩余容量 min_cap,这是沿着该路径可以增加的最大流量。
更新这条路径上所有边的流量和反向边的流量。对于正向边,增加流量;对于反向边,减少流量(或者创建反向边并设置其流量为增加的流量)。
4.重复步骤 2 和 3:
直到找不到任何可以进一步增加流量的增广路径为止。
5.输出结果:
当不再存在任何增广路径时,当前的流就是最大流。*
Edmonds-karp算法
1.初始化:
创建一个流网络图 G(V, E),其中 V 是顶点集,E 是边集。
为每条边 e = (u, v) 分配一个非负整数容量 c(e)。
初始化从源点 s 到汇点 t 的流为零。
2.构建残余网络:
构建一个残余网络 G_f,初始时与原网络相同。
3.寻找增广路径:
使用广度优先搜索 (BFS) 在残余网络 G_f 中查找从 s 到 t 的路径 P。
BFS 会按照最短路径顺序寻找增广路径,这里“最短”是指路径中边的数量最少。
4.更新流:
如果找到了一条从 s 到 t 的路径 P,则计算该路径上的最小剩余容量 bottleneck§。
更新路径 P 上的每条边及其反向边的流量。对于正向边 (u, v),增加流量;对于反向边 (v, u),如果存在则减少流量或创建新的反向边并设置流量为增加的流量。
5.重复步骤 3 和 4:
重复上述过程直到没有从 s 到 t 的增广路径。
6.输出结果:
最终得到的流就是最大流。
性能分析:
Edmonds-Karp 算法的时间复杂度是 O(VE^2),其中 V 是顶点数量,E 是边的数量。
这是因为每次 BFS 都会在残余网络中找到一条最短路径,而这样的路径最多只能被增广 V 次,每次增广操作的时间复杂度为 O(E)。
实现细节:
1.在实际实现中,通常会使用邻接矩阵或邻接表来存储图的数据结构。
2.BFS 可以通过队列来实现,以确保按顺序处理顶点。
3.我们要一直bfs,直到没有增广路径
4.我们可以通过边bfs边建立并查集,方便后面查找最短增广路径。搜索到汇点之后,沿着汇点一路沿着并查集的生成树走到源点即是路径
5.最小流量可以使用一个数组 min_flow[x] 表示从源点到x点的最小流量,假设x点有邻居y,那么y最小流量的推导为:min_flow[y] = min(xy边的权值, min_flow[x])
Edmonds-Karp 算法的时间复杂度是 O(VE^2),其中 V 是顶点数量,E 是边的数量。
这是因为每次 BFS 都会在残余网络中找到一条最短路径,而这样的路径最多只能被增广 V 次,每次增广操作的时间复杂度为 O(E)。
Dinic算法
Dinic 算法是一种改进版的最大流算法,它在 Edmonds-Karp 算法的基础上进行了优化,特别适合处理密集图。Dinic 算法的主要特点是使用层次图 (Level Graph) 和块推进 (Block-Advance) 技术来寻找增广路径。
1.初始化:
创建一个流网络图 G(V, E),其中 V 是顶点集,E 是边集。
为每条边 e = (u, v) 分配一个非负整数容量 c(e)。
初始化从源点 s 到汇点 t 的流为零。
2.构建残余网络:
构建一个残余网络 G_f,初始时与原网络相同。
3.构建层次图:
使用广度优先搜索 (BFS) 来构建一个层次图,其中每个顶点都有一个距离源点的层级。
层次图保证了从源点 s 到汇点 t 的每条路径都是递增的。
4.寻找增广路径:
使用深度优先搜索 (DFS) 在层次图中从源点 s 开始寻找增广路径。
DFS 只能在当前层或下一层中移动,以保证路径的单调性。
5.更新流:
对于找到的每条增广路径,确定最小的剩余容量 bottleneck,并更新这条路径上所有边的流量。
对于正向边 (u, v),增加流量;对于反向边 (v, u),如果存在则减少流量或创建新的反向边并设置流量为增加的流量。
重复步骤 3 至 5:
重复上述过程直到 BFS 不再能够构建一个新的层次图,这意味着没有从 s 到 t 的增广路径。
输出结果:
最终得到的流就是最大流。
性能分析:
因为dinic的dfs按照层次来递归,因为每次扫描出的层数是递增的,而且不超过n,所以外循环bfs最多进行n次,而因为每次找增广路都有一条边作为“瓶颈”,一共有e条边,就有e个瓶颈,e条路径,要进行e次dfs(这里的dfs需要访问控制数组vis,每个节点最多访问一次),每次dfs复杂度为O(e),总体复杂度为O(ne2)
具体算法讲解可以上b站搜:13-4: Dinic’s Algorithm 寻找网络最大流
ISAP
ISAP (Improved Shortest Augmenting Path) 算法是基于 Dinic 算法的一种改进版本,它旨在提高最大流问题求解的速度。ISAP 算法的核心思想是通过维护一个“当前弧”(current arc)的概念来减少搜索次数,并且通过不断更新顶点的距离标签来避免不必要的搜索,从而加速寻找增广路径的过程。
1.初始化:
创建一个流网络图 G(V, E),其中 V 是顶点集,E 是边集。
为每条边 e = (u, v) 分配一个非负整数容量 c(e)。
初始化从源点 s 到汇点 t 的流为零。
初始化每个顶点的距离标签 dist[v] 为无穷大,dist[s] = 0。
初始化每个顶点的“当前弧” current_arc[v] 为其出边的第一个边。
2.构建残余网络:
构建一个残余网络 G_f,初始时与原网络相同。
3.寻找增广路径:
使用深度优先搜索 (DFS) 在残余网络 G_f 中从源点 s 开始寻找增广路径。
DFS 只能在当前距离标签或更远的顶点中移动。
如果 DFS 到达汇点 t 或者所有可达的边都被访问过,则返回找到的最小容量 bottleneck。
4.更新流:
对于找到的每条增广路径,确定最小的剩余容量 bottleneck,并更新这条路径上所有边的流量。
对于正向边 (u, v),增加流量;对于反向边 (v, u),如果存在则减少流量或创建新的反向边并设置流量为增加的流量。
5.更新距离标签:
如果一个顶点 v 的所有出边都被访问过并且没有发现增广路径,那么更新 v 的距离标签 dist[v] 为其邻居中距离标签的最小值加一。
6.如果更新后的距离标签小于 v 的前驱顶点的标签,则将前驱顶点的“当前弧”指向下一条边。
重复步骤 3 至 5:
重复上述过程直到 BFS 不再能够构建一个新的层次图,这意味着没有从 s 到 t 的增广路径。
7.输出结果:
最终得到的流就是最大流。
代码实现
// 包含必要的头文件
#include <iostream>
#include<vector>
#include<queue>
using namespace std;
// 定义inf常量,表示无穷大
#define inf 1145141919
typedef struct edge
{
int st, ed, val, pair; // 起点, 终点, 还能通过多少流量, 反向边下标
edge() {}
edge(int a, int b, int c, int d) { st = a; ed = b; val = c; pair = d; }
}edge;
// 定义顶点数、初始边数、源、目和答案等变量
int n, e, src, dst, ans; // 顶点数, 初始边数, 源, 目, 答案
vector<vector<int>> adj; // adj[x][i]表示从x出发的第i条边在边集合中的下标
vector<edge> edges; // 边集合
vector<edge> edges_; // 原始数据 因为每次边要增广所以重复计算时要初始化边
vector<int> min_flow; // min_flow[x]表示从起点到x的路径中最细流
vector<int> father; // 生成树
vector<int> level; // 层次
vector<int> cur_arc; // 当前弧优化数组
vector<int> dis_cnt; // 距离计数器
/* ---------------------------------------------------------------------------- */
/*
* @function bfs_augment : bfs找最短增广路径
* @param : ----
* @return : 如果找到则return true否则false
* @pexlain : father建生成树 min_flow更新节点最细流
*/
bool bfs_augment()
{
for (int i = 0; i < n; i++) father[i] = -1;
for (int i = 0; i < n; i++) min_flow[i] = inf; // inf
father[src] = src;
queue<int> q; q.push(src);
while (!q.empty())
{
int x = q.front(), y; q.pop();
if (x == dst) return true;
for (int i = 0; i < adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
y = e.ed;
if (father[y] != -1 || e.val == 0) continue;
father[y] = x;
min_flow[y] = min(e.val, min_flow[x]);
q.push(y);
}
}
return false;
}
/*
* @function graph_update : 根据bfs_augment的生成树father[]更新图
* @param : ----
* @return : ----
* @explain : 需要在bfs_augment之后使用
*/
void graph_update()
{
int x, y = dst, flow = min_flow[dst], i;
//cout<<"更新流量: "<<flow<<" 路径: ";
ans += flow;
vector<int> path;
while (y != src) // 沿着生成树找起点并沿途更新边
{
path.push_back(y);
x = father[y];
for (i = 0; i < adj[x].size(); i++) if (edges[adj[x][i]].ed == y) break;
edges[adj[x][i]].val -= flow;
edges[edges[adj[x][i]].pair].val += flow; // 更新另一半的边
y = x;
}
/*
path.push_back(y);
for(int i=path.size()-1; i>=0; i--) cout<<path[i]<<" "; cout<<endl;
*/
}
/*
* @function EK : Edmonds-Karp算法求最大流
*/
void EK()
{
ans = 0;
while (1)
{
if (!bfs_augment()) break;
graph_update();
// 打印图信息
for (int i = 0; i < edges.size(); i++)
cout << edges[i].st << " -> " << edges[i].ed << " val=" << edges[i].val << endl;
cout << endl;
}
cout << "最大流:" << ans << endl;
}
/* ---------------------------------------------------------------------------- */
/*
* @function bfs_level : 层次遍历标记节点层数
* @param : ----
* @return ;如果找到终点return true 否则false
* @explain : ----
*/
bool bfs_level()
{
for (int i = 0; i < n; i++) level[i] = -1;
level[src] = 0; // 起始节点第0层
queue<int> q; q.push(src);
int lv = 0; // bfs层次数
while (!q.empty())
{
lv++;
int qs = q.size();
for (int sq = 0; sq < qs; sq++)
{
int x = q.front(), y; q.pop();
if (x == dst) return true;
for (int i = 0; i < adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
y = e.ed;
if (level[y] != -1 || e.val == 0) continue;
level[y] = lv;
q.push(y);
}
}
}
return false;
}
/*
void dfs_dinic(int x, list<int>& path)
{
father[x] = 1; // father充当访问控制数组
for(int i=0; i<adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
int y = e.ed;
if(father[y]!=-1 || e.val==0 || level[y]!=level[x]+1) continue;
path.push_back(adj[x][i]); // path记录经过的边的下标
min_flow[y] = min(e.val, min_flow[x]);
// 找到则更新所有路径上的边
if(y==dst)
{
for(auto it=path.begin(); it!=path.end(); it++)
edges[*it].val-=min_flow[y], edges[edges[*it].pair].val+=min_flow[y];
ans += min_flow[y];
}
dfs_dinic(y, path);
path.pop_back();
}
}
*/
/*
* @function dfs_dinic1 : 普通Dinic算法 往x节点塞入flow流量 并且尝试分配下去
* @param x : 当前节点
* @param flow : 要向x分配多少流量(最大可用值)
* @return : 节点x实际向下分配了多少流量
* @explain : 一旦找到立即返回 一次找一条
* : 使用father数组作为访问控制visit数组
*/
int dfs_dinic1(int x, int flow)
{
father[x] = 1;
if (x == dst) return flow;
for (int i = 0; i < adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
int y = e.ed;
if (e.val == 0 || level[y] != level[x] + 1 || father[y] == 1) continue;
int res = dfs_dinic1(y, min(flow, e.val));
edges[adj[x][i]].val -= res, edges[edges[adj[x][i]].pair].val += res;
if (res != 0) return res; // 找到直接返回
}
return 0; // 没找到则返回0
}
/*
* @function Dinic1 : 普通Dinic算法求最大流
* @explain : 用level数组做层次标记数组 层次逐渐增加
*/
void Dinic1()
{
ans = 0;
while (bfs_level())
{
while (1)
{
for (int i = 0; i < n; i++) father[i] = 0;
int res = dfs_dinic1(src, inf);
if (res == 0) break; // 找不到增广路 需要重新bfs更新层次
ans += res;
}
/*
// 打印图信息
for(int i=0; i<edges.size(); i++)
cout<<edges[i].st<<" -> "<<edges[i].ed<<" val="<<edges[i].val<<endl;
cout<<endl;
*/
}
//cout<<"最大流:"<<ans<<endl;
}
/*
* @function dfs_dinic2 : Dinic + 当前弧优化
* @param x : 当前节点
* @param flow : 要向x分配多少流量(最大可用值)
* @return : 节点x实际向下分配了多少流量
* @explain : 一旦找到立即返回 一次找一条
* : 使用father数组作为访问控制visit数组
*/
int dfs_dinic2(int x, int flow)
{
father[x] = 1;
if (x == dst) return flow;
for (int& i = cur_arc[x]; i < adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
int y = e.ed;
if (e.val == 0 || level[y] != level[x] + 1 || father[y] == 1) continue;
int res = dfs_dinic2(y, min(flow, e.val));
edges[adj[x][i]].val -= res, edges[edges[adj[x][i]].pair].val += res;
if (res != 0) return res; // 找到直接返回
}
return 0; // 没找到则返回0
}
/*
* @function Dinic2 : Dinic + 当前弧优化
* @explain : 用level数组做层次标记数组 层次逐渐增加
*/
void Dinic2()
{
ans = 0;
while (bfs_level())
{
// 当前弧重置
for (int i = 0; i < n; i++) cur_arc[i] = 0;
while (1)
{
for (int i = 0; i < n; i++) father[i] = 0; // 每次dfs更新visit数组
int res = dfs_dinic2(src, inf);
if (res == 0) break; // 找不到增广路 需要重新bfs更新层次
ans += res;
}
/*
// 打印图信息
for(int i=0; i<edges.size(); i++)
cout<<edges[i].st<<" -> "<<edges[i].ed<<" val="<<edges[i].val<<endl;
cout<<endl;
*/
}
//cout<<"最大流:"<<ans<<endl;
}
/*
* @function dfs_dinic1 : Dinic + 多路增广
* @param x : 当前节点
* @param flow : 要向x分配多少流量(最大可用值)
* @return : 节点x实际向下分配了多少流量
* @explain : 一次找完所有路径
*/
int dfs_dinic3(int x, int flow)
{
if (x == dst) return flow;
int temp_flow = flow; // 记录能分配的最大值
for (int i = 0; i < adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
int y = e.ed;
if (e.val == 0 || level[y] != level[x] + 1) continue;
int res = dfs_dinic3(y, min(flow, e.val));
edges[adj[x][i]].val -= res, edges[edges[adj[x][i]].pair].val += res;
flow -= res; // 更新可用流量
if (flow == 0) return temp_flow; // 如果分完了就结束
}
return temp_flow - flow; // 返回实际分配的
}
/*
* @function Dinic1 : Dinic算法 + 多路增广
* @explain : 用level数组做层次标记数组 层次逐渐增加
*/
void Dinic3()
{
ans = 0;
while (bfs_level())
{
ans += dfs_dinic3(src, inf); // dfs层次图以更新边
/*
// 打印图信息
for(int i=0; i<edges.size(); i++)
cout<<edges[i].st<<" -> "<<edges[i].ed<<" val="<<edges[i].val<<endl;
cout<<endl;
*/
}
//cout<<"最大流:"<<ans<<endl;
}
/* ---------------------------------------------------------------------------- */
/*
* @function dfs_ISAP : 在层次图中向下分配流量 往x节点塞入flow流量 并且尝试分配下去
* @param x : 当前节点
* @param flow : 要向x分配多少流量(最大可用值)
* @return : 节点x实际向下分配了多少流量
* @explain : 和Dinic类似 只是边bfs边更新层次
*/
bool ISAP_flag = false;
int dfs_ISAP(int x, int flow)
{
if (x == dst) return flow;
int temp_flow = flow; // temp_flow保存这个点拥有的流量
for (int i = 0; i < adj[x].size(); i++)
{
edge e = edges[adj[x][i]];
int y = e.ed;
if (level[x] != level[y] + 1 || e.val == 0) continue;
int res = dfs_ISAP(y, min(e.val, flow));
edges[adj[x][i]].val -= res, edges[edges[adj[x][i]].pair].val += res; // 退栈时更新图
flow -= res; // 分配了res流量给某个分支
if (flow == 0) return temp_flow; // 分配完了则返回
}
dis_cnt[level[x]]--; // 计算新距离计数
if (dis_cnt[level[x]] == 0) ISAP_flag = true; // 出现断层就提前结束
level[x]++; // 已经分配完所子节点 不存在和刚一样长度增广路径 路径长度严格连续增加
dis_cnt[level[x]]++; // 计算新距离计数
return temp_flow - flow; // 返回已经分配的流量数目
}
/*
* @function ISAP : ISAP
* @explain : 用level数组做距离标记数组,距离逐渐减少
: 引入当前弧优化和gap优化
*/
void ISAP()
{
bfs_level();
// 层次数组变为距离数组
int mlv = *max_element(level.begin(), level.end());
for (int i = 0; i < n; i++) level[i] = mlv - level[i], dis_cnt[i] = 0;
// 距离计数数组计算初始值
for (int i = 0; i < n; i++) dis_cnt[level[i]]++;
ans = 0; ISAP_flag = false;
while (level[src] < n && !ISAP_flag)
{
ans += dfs_ISAP(src, inf);
if (ISAP_flag) break;
}
打印图信息
//for(int i=0; i<edges.size(); i++)
// cout<<edges[i].st<<" -> "<<edges[i].ed<<" val="<<edges[i].val<<endl;
//cout<<endl;
/* cout<<"最大流:"<<ans<<endl;*/
}
/*
* @function add_edge : 添加一条边及其反向边
* @param st : 正向边起点
* @param ed : 正向边终点
* @param val : 边的权值(最大允许流量
*/
void add_edge(int st, int ed, int val)
{
int ii = edges_.size();
edges_.push_back(edge(st, ed, val, ii + 1));
edges_.push_back(edge(ed, st, 0, ii));
adj[st].push_back(ii); adj[ed].push_back(ii + 1);
}
/*
* @function re_graph : 将图恢复为默认生成的图
*/
void re_graph()
{
for (int i = 0; i < e; i++) edges[i] = edges_[i];
}
void cin_graph()
{
cin >> n >> e >> src >> dst;
adj.resize(n);
father.resize(n);
min_flow.resize(n);
level.resize(n);
cur_arc.resize(n);
dis_cnt.resize(n + 1);
for (int i = 0; i < e; i++)
{
int st, ed, limit; cin >> st >> ed >> limit;
add_edge(st, ed, limit);
}
e = edges_.size();
edges.resize(e);
}
int main()
{
cin_graph();
re_graph(); // 打印图信息
for (int i = 0; i < edges.size(); i++)
cout << edges[i].st << " -> " << edges[i].ed << " val=" << edges[i].val << endl;
cout << endl;
double t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0;
clock_t st, ed;
re_graph();
st = clock();
EK();
ed = clock();
t1 += (double)(ed - st) / CLOCKS_PER_SEC;
re_graph();
st = clock();
Dinic1();
ed = clock();
t2 += (double)(ed - st) / CLOCKS_PER_SEC;
re_graph();
st = clock();
Dinic2();
ed = clock();
t3 += (double)(ed - st) / CLOCKS_PER_SEC;
re_graph();
st = clock();
Dinic3();
ed = clock();
t4 += (double)(ed - st) / CLOCKS_PER_SEC;
re_graph();
st = clock();
ISAP();
ed = clock();
t5 += (double)(ed - st) / CLOCKS_PER_SEC;
cout << t1 << endl;
cout << t2 << endl;
cout << t3 << endl;
cout << t4 << endl;
cout << t5 << endl;
return 0;
}
/*测试数据:
11 19 0 10
0 1 3
0 2 8
0 3 7
0 4 2
0 5 7
1 6 999
1 7 999
2 6 999
2 8 999
3 6 999
3 9 999
4 7 999
4 8 999
5 7 999
5 9 999
6 10 1
7 10 4
8 10 7
9 10 16
*/
总结
以上就是今天要讲的内容,本文仅仅简单介绍了最大流解决问题的实现思路,而具体的代码方法的选择和实现可以结合其他文章加强理解