最大流之 Dinic 算法
今天要讲的是 Dinic 算法!之前我们讲过使用 EK 算法来解决最大流问题,可是还有没有优化的余地呢?就让我们来探究一下 Dinic 的奥妙之处。
前置知识:最大流之入门知识与原理 && 最大流之 Ford - Fulkerson 算法与 EK 算法。
问题引入
我们知道,EK 算法是在不断用最短路径贪心地寻找增广路,然而这样做有一个致命的缺点。请看这幅图:
点“……”代表许多许多点,我们暂且假设从点“^_^”到点“awa”的这一条长链的点的个数为
EK 的本质是 BFS。可以想象到,在执行 EK 算法时,会从源点经过非常长的一条链并选择一条“awa”->“T”的路径。接着,我们会继续尝试其他分支,并且每次都需要经过这个无比长的链!然后 EK 就像被耍了一样,发现每次更新的答案都一样……如果汇点处的分支更多,时间复杂度将会超乎想象,因为每个分支都会让 BFS 经过一次这一条链。
相信你很快就发现了 EK 的缺陷:每次都重新搜,容易把一堆东西重复搜。怎么优化呢?我们想到:DFS 可以回溯,我们可以让 DFS 一次性搜索完一堆点而不用重新从源点开始搜!但是我们需要一种高效的 DFS 方法,否则将会退化为纯暴搜 Ford - Fulkerson 呀。
于是现在给出一种限制方法:分层。我们先通过 BFS 将网络分层,离源点越近,层数越小;离源点越远,层数越大。在 DFS 时,我们规定只能从前一层搜向后一层。这样就不能让同层的点互相搜了,同时也在步步逼近汇点,是不是限制住了“乱搜”的 DFS?在一次 DFS 结束后,我们就继续用 BFS 重新分层,直到再也分不了为止。如此一来,我们就完成了初步的算法框架设计。
代码实现
代码分为 BFS,DFS 和 Dinic 三个函数。
Dinic 函数的实现
Dinic 函数是主要的执行函数,代码短,结构简单。
Dinic 算法的本质依然是不断地寻找增广路。在函数中,我们不断地 BFS 直到源点和汇点不连通。BFS 是 DFS 中找增广路的铺垫。因此,BFS 和 DFS 函数都不只会执行一次。将所有 DFS 中找到的增广流量加起来,就是答案。
ll dinic() {
ll maxflow = 0;
while (bfs()) maxflow += dfs(s, 1e18);
// 我们可以暂时不用看bfs和dfs中的参数
return maxflow;
}
我们可以通过 Dinic 函数大致了解最大流的框架。
BFS 函数的实现
我们定义
值得注意的是,如果一条边的剩余流量为
具体实现如下,可以仔细阅读注释理解。
int level[N];
// 定义我们所需要的变量,数组
bool bfs() { // bfs函数的主要作用是:分层
fill(level + 1, level + 1 + n, 0); // 初始化为0
queue<int> q; // 定义队列
q.push(s);
level[s] = 1; // 源点的level值必须设为1
while (!q.empty()) {
ll cur = q.front(); q.pop();
for (ll i = 0; i < nbr[cur].size(); i++) if (!level[nbr[cur][i].to] && nbr[cur][i].w) {
// 没有标记过并且这条边有流量(不然每次的分层结果都一样了呀)
ll nxt = nbr[cur][i].to, w = nbr[cur][i].w;
q.push(nxt); // 将新的点存入队列,继续bfs
level[nxt] = level[cur] + 1;
if (nxt == t) return 1; // 如果已经到达了汇点,就可以结束了
}
}
return 0; // 否则bfs失败
}
是不是很简单?我们可以发现,BFS 函数的返回值是 bool 类型的,这个返回值代表着当前的网络中源点和汇点是否连通。如果源点都流不到汇点了,那么也就不可能再有新的流量(增广路)了。
DFS 函数的实现
DFS 函数当然符合普通 DFS 的结构。我们来逐一攻破:
-
边界条件
如果当前已经到达了汇点,就成功找到了一条增广路。此时我们需要返回当前的流量。
-
递归调用
当 DFS 函数执行到点
cur 时,假设来到点cur 的有flow 的流量。接下来需要分流。也就是说,对于每个cur 的邻接点,都需要将一些流量分给这些点。同时,不能违背了之前的约定:下一个邻接点必须比cur 的层数严格大1 ,也就是不能在同一层中搜索,否则就又退化为 Ford - Fulkerson 算法了。此时cur\to nxt 的有向边的剩余流量不能为0 ,与 BFS 同理。我们会在分流的过程中将
flow 分出去,所以cur 点的剩余流量在不断减少。我们将当前剩余的流量设为rest ,初始时rest=flow 。那么递归到下一个点nxt 时,nxt 有多少流量能从边cur\to nxt 流出呢?这取决于当前我有多少流量和cur\to nxt 这条边的流量上限,那么流过去的流量就是这两个值之中较小者的值。若cur\to nxt 这条边的流量上限为w ,那么在点nxt 处就有flow_{nxt}=\min(rest,w) 。如果在某一时刻rest=0 ,那么接下来的点就都没有流量可分了,可以直接跳出循环。这是第一个剪枝。接着,递归结束的点
nxt 带来的返回值是什么呢?当然是nxt 这条分支最后到汇点的流量啦!在nxt\to T (T 是汇点)的路径中,流量有可能越来越小,此时我们将最后nxt\to T 的流量设为inc 。若inc=0 ,没有流量,那么从nxt 处找不到增广路,在这一次增广中也不可能再找到了。于是,我们可以让所有点都不再经过这个点,也就是令level_{nxt}=0 。这是第二个剪枝。 -
回溯处理
我们知道,在 EK 算法中,有一个重要的步骤:更新网络。我们在找到一条增广路后,一定要减少正向边的流量并增加对应的反向边的流量。但在 Dinic 算法中,由于算法本身就是用 DFS 实现的,所以这项操作本能地非常容易实现,在 DFS 中直接加减流量就可以了,回溯时自然会找到一条路径。同时,我们要记得更新
rest ,rest\gets rest-inc (\gets 表示赋值),也就是rest 要减去inc 。 -
返回值处理
那么,函数最后的返回值是什么呢?换句话说,点
cur 到底流出了多少流量?显然是flow-rest 。比如一开始有100 的流量,最后流啊流只剩10 的流量没有流出去,那么就流出去了(100-10=90) 的流量。
相信你看了这么多后,脑袋是晕乎乎的。没错,这个时候就要配合代码食用!!
ll dfs(ll cur, ll flow) { // dfs函数的主要作用是:找增广路
if (cur == t) return flow; // 如果到了汇点,就不用继续往下走了
ll rest = flow; // rest记录剩下的流量
for (ll i = 0; i < nbr[cur].size() && rest; i++) { // “&& rest” 就是我们提到的第一个剪枝
ll nxt = nbr[cur][i].to, w = nbr[cur][i].w;
if (w && level[nxt] == level[cur] + 1) { // 这条边有流量且层数严格递增
ll inc = dfs(nxt, min(rest, w)); // 递归调用,inc就是点nxt出最终流到汇点的流量
if (!inc) level[nxt] = 0; // 这是第二个剪枝
nbr[cur][i].w -= inc;
nbr[nxt][nbr[cur][i].bk].w += inc;
// 直接在dfs中更新网络
rest -= inc; // 别忘了rest
}
}
return flow - rest; // 流出的流量等于最初的流量减去最后的流量
}
复杂度分析
时间复杂度:
证明比较复杂,咱们不需要太过深入了解~
与 EK 算法的比较
直观上看,Dinic 和 EK 的复杂度似乎没有差得太多。然而实际上,Dinic 算法在一般情况下都比 EK 算法要快!这是因为边的数量
不过有些人认为 EK 算法代码更短,更好写,不容易出错。那么究竟什么时候 EK 算法效率更优呢?答案是:层数较小的图。比如二分图最大匹配问题,我们知道可以通过设置超级源点和超级汇点并使用最大流解决。此时整张图只有
此时 EK 算法的贪心可以很快找到一条增广路,那么就能快速更新答案。在实际做题时,我们也会遇到很多建模如上图的题,此时用 EK 算法较好;否则,还是用 DInic 算法比较谨慎,不然容易超时。
例题
- P3376 【模板】网络最大流
- P2774 方格取数问题(P4474 王者之剑)
主要是要把模板重新打一遍,增强熟练度,尽量排除一些容易犯的错误。在刚学习 Dinic 后,一定不要复制模板,要自己老老实实打!
后面的两道题作为挑战,可以先思考一下,会在后面的习题篇里详细讲解。
当前弧优化
想象一下,如果在多次增广后一些边的流量已经是
如果
核心代码改动有两处:
-
BFS 函数
清空
tmp 。fill(tmp + 1, tmp + 1 + n, 0); -
DFS 函数
枚举邻接点从
tmp_{cur} 开始,每次更新tmp_{cur} 。for (ll i = tmp[cur]; i < nbr[cur].size() && rest; i++)循环内更新
tmp :tmp[cur] = i;
这样就可以进一步优化我们的运行效率了。
后记
这一篇文章是 Dinic 算法的介绍与入门,希望能对你有帮助。后面还打算补一个习题篇,毕竟最大流题目的解题技巧比光秃秃的模板还是更重要。希望你能好好消化这些知识,那我们下次再见!