网络流学习笔记 - 最大流最小割
字数统计:12333字,好了现在 12345 字了。
终于有时间来学学这个东西了,再不学就落后时代了(
前置知识:对 dfs 和 bfs 有一定了解,而且掌握了最短路算法。(实际上 spfa 和 dijkstra 都可以)
因为我前几次沪粤联赛打得不好就是因为 C 是网络流题,所以就来学学。
现在我们讲最大流和最小割。
网络流,英文 Network Flow
。Network
就是网络,Flow
是流的意思。
这个“流”可以是流水(边就是管道),可以是流电(边就是光纤),也可以是流“人”(边可以是航班也可以是高铁等等)以及其他的一些东西。
为什么说是“网络”而不是“图”呢?因为这个“网络”中的边有不同之处。
首先,边一定是有向的。
网络中的边也是有边权的,但是这不是普通的边权,而是容量,指的是一条边上面最多能通过的流的数量总量。
而且,中途的一个结点进入了多少流量,最终就必须要想办法把这些流量给走出去。一点也不能多,一点也不能少。
最大流 max flow
的意思就是从源点到汇点,最多能通过的流的数量。(允许流量走多条不相同的路径)
很显然最小流没有意义,就是 0 0 0。
解释一下一些术语:
源点:就是起点的形象表述。因为可以想象一条流水一定有源头。
汇点:就是终点的形象表述。因为一条流水最终可能会汇入大海。
例如这个图,最左边的点是源点,最右边的点是汇点,则从源点到汇点最多可以容纳 8 8 8 个流。当然这是最简单的情况,还有复杂的。
假设源点为 4 4 4,汇点为 3 3 3,那么这个图就可以容纳 50 50 50 个流: 4 → 2 → 3 4 \to 2 \to 3 4→2→3 可以容纳 20 20 20 个流量, 4 → 3 4\to 3 4→3 可以 20 20 20 个,而 4 → 2 → 1 → 3 4 \to 2 \to 1 \to 3 4→2→1→3 是 10 10 10 个。显然这样就不是再路径上面取最小值那么简单了。
请注意,源点和汇点可以有很多。
当然,除了最大流以外,还有其他将来我们要学会求解的东西。当然这些东西后面才会讲到,这里先简单阐述一下它们的定义。
最小割 min cut
:选择总权值和最小的一些边,使得如果断开这些边,从源点到汇点不存在任何的流。
有上下界的最大流:就是相当于在边的上面有通过流的总数的上界和下界。 我们原本就有了上界,但是现在增加了下界,也就是说通过的流的总数不能太少。这个时候的最大值和最小值都要有所考虑。
最小费用流(min cost max flow
): 一般都默认是最小费用最大流,但是也有时候不仅仅是“最大”,而可以是“最优”。这个时候类似上下界最大流,还加了一个代价限制,导致题目难了许多。
最大流
前面铺垫了很多,现在终于可以讲一下到底怎么做了。
前面我们说到源点和汇点有很多个,这个时候我们就可以建两个虚点,一个是超源点,一个是超汇点。
建完虚点之后可以这么处理:将超源点想所有的源点连一条边权正无穷的边,所有的汇点向超汇点连一条边权正无穷的边。
很容易理解,正确性就不讲了。所以现在我们就变成了一个源点 s s s 和一个汇点 t t t。
这个问题在生活中运用很多,在旅游规划、网络线路规划都有体现出来。
很容易发现依靠我们以前的图上算法都不能解决这个问题( d p dp dp 也无法解决,当时的沪粤联赛的那道题我写了 DAG d p dp dp 挂了 64 64 64 分)。
Ford - Fulkerson 算法
开始进入正题,这个东西到底该怎么求?
冷知识:这里的 Ford 就是那个发明 Bellman_Ford 算法的人。
为了表述方便,先来讲解几个以后需要用到的概念(这段东西必须要掌握,要不然后面看不懂)。
增广路径 augment path
:这个东西我们应该不陌生了,二分图里面就出现过这个东西。
但是这个时候增广路的意思改变了一下:当前的某一条流量 > 0 >0 >0 的 s → t s \to t s→t 的路径。
而这个算法本身就是在找增广路。
首先我们有一种比较直观的思路:
假设我们目前找到了一条增广路,其流量为 3 3 3:
很容易发现,这个时候我们可以把这个流量是 3 3 3 的提出来然后计入答案了。变成这个样子:
我们可以重新改一下边权,然后继续找增广路,在最后如果发现没有一条流 > 0 >0 >0 的增广路就结束了,我们就找到了最大流。
这个时候又出现了一个问题:
- 你这样做,真的可以最后得到正确的最大流吗?
这个问题是很容易就可以提出来的:你随便找到一条增广路径你就把它消耗了,有没有可能后面会存在流量更大的增广路径不能用了呢?
然后我们就会发现这个方法是假的。
举个例子:
例如这个图,如果我们先走了中间这条增广路径,图就会变成这样:
但是我们就会发现此时没有增广路了。所以答案为 3 3 3,是这样吗?
如果我们走的是最上面的和最下面的路径,答案就是 6 6 6 了。这告诉我们这个方法是假的。
不要气馁,思考解决方案。那么怎么办呢?
因为我们的假做法是贪心的,所以考虑反悔贪心。我们考虑对边的使用反悔。
因为一条边的总流量是不变的,所以可以将提出来的流量总数作为反悔贪心的反悔筹码。 提出来的流量总数就是图中的绿字。
这个时候我们可以这样处理:将还剩下的流量总数存在正向的边上,把提取出来的流量总数存在反向的边上。
而我们这个时候允许让增广路走正向的边或者是反向的边。想想,这个时候会发生什么?
-
如果走的是正向的边,那么最终一起提取出来的流量(很显然是增广路上面的边权最小值)会被正向的边权减去,而反向的边权加上这个值。这种就相当于减少了当前的总量,增加了反悔的余量。是正常的贪心操作。
-
如果走的是反向的边,那么最终一起提取出来的流量(很显然是增广路上面的边权最小值)会被反向的边权减去,而正向的边权加上这个值。这种就相当于增加了当前的总量,减少了反悔的余量。这就是另一种反悔贪心操作。
所以发现如果建正向反向边的话就可以非常自然地处理贪心和反悔操作。 个人感觉还是很妙的。也可以感性理解一下,这个算法是正确的(因为允许反悔)。
注意我们这个时候找到一条增广路径就直接让答案增加这条增广路径的边权最小值即可。
例如我们现在的有一条边 ( u , v ) (u,v) (u,v) 的容量是 6 6 6,而我们提取出来了 4 4 4,还剩下 2 2 2 的容量。则我们可以设 ( u , v ) (u,v) (u,v) 的容量为 2 2 2,而 ( v , u ) (v,u) (v,u) 的容量为 4 4 4。
很容易发现我们这样就可以使得上面的例子满足要求了,因为我们可以:
走红色的路径,就可以获得一条新的增广路径。
然后再看看,发现上面的三条边和下面的三条边的正向边都变成了 0 0 0,而中间的正向边变成了 3 3 3,和走最上面和最下面的路径是等价的!!这就是反悔贪心的威力。
一直这样找,每一次更新反边,就可以最终得到答案。
最大流最小割定理
最小割就是从图里面切断一些边使得图不存在增广路,这些边的最小权值。而最大流就是图中所流通的最大流量。
很容易想到二分图中差不多的定义:最大流对应二分图中的最大匹配,最小割对应二分图中的最大独立集。
在二分图中我们有 Konig \text{Konig} Konig 定理,也就是 最大匹配 = 最大独立集。
那么在更加复杂的有向图里面还存不存在这个定理呢?是存在的。
直接说定理,稍后再讲正确性:最大流最小割定理:任意有向图中的最大流等于最小割。
最大流最小割定理证明
设源点为 s s s,汇点为 t t t,源点及其某些能够到达的点构成 U U U 集合,汇点及其某些能够到达它的点构成 V V V 集合。
Note: U U U 和 V V V 都可以放弃加入一些结点,但是一定要保证每一个结点要么在 U U U 要么在 V V V,其中 s s s 一定在 U U U 里面, t t t 一定在 V V V 里面。
并设 E ( U , V ) E(U,V) E(U,V) 表示一端在 U U U 一端在 V V V 的边的集合。很显然一共分两种边,一种是前者连向后者,一种是后者连向前者。
容易发现,从 U U U 到 V V V 的三条边恰好就是一个割。
扩展到一般情况,我们会发现:对于任意的 U U U 和 V V V,一定存在恰好一种割的方案与之对应,就是从 U U U 集合到 V V V 集合的所有边。
所以我们可以更改割的定义:将点集划分成两个子集 U , V U,V U,V, U U U 不应包含 s s s 不可达的结点, V V V 不应包含不可达 t t t 的结点,则割就是所有从 U U U 中结点到 V V V 中结点的边集。
根据 MO 的思想,我们如果需要证明 m c = m f mc=mf mc=mf,就需要同时证明 m c ≥ m f mc \ge mf mc≥mf 和 m c ≤ m f mc \le mf mc≤mf。(这个思想比较重要)
最小割 ≥ \ge ≥ 最大流
考虑直观理解。不妨将流量想象成水流。
想象水流从源点 s s s 流向汇点 t t t。割 ( U , V ) (U,V) (U,V) 就像一道“闸门”,所有水流必须通过从 S S S 到 T T T 的边才能到达 t t t。
然后就会发现,整个网络的流量不可能超过这个闸门的总容量(即割的容量)。
可以感性理解一下,如果整个网络的流量超过了这个闸门的总流量,那就很神奇了,你这个闸门总共也就只能通过 x x x 这么多的水,然后你告诉我实际上通过了 > x >x >x 的水?
所以发现流的总容量一定小于等于割的总容量,所以发现最大流一定 ≤ \le ≤ 最小割,证毕。
最小割 ≤ \le ≤ 最大流
显然这个时候直接直观理解比较困难,所以使用一个新的概念:残余网络。
设点集为 X X X,原网络的边集为 E E E,而且 ( u , v ) (u,v) (u,v) 这条边的权值为 c ( u , v ) c(u,v) c(u,v)。并设 U , V U,V U,V 两个集合的割(也就是从 U U U 到 V V V 的所有边)为 c ( U , V ) c(U,V) c(U,V)。
并设 ( u , v ) (u,v) (u,v) 的流函数为 f ∗ ( u , v ) f^*(u,v) f∗(u,v),也就是最大流方案中 ( u , v ) (u,v) (u,v) 上经过的流量。
首先考虑构造残余网络:设 f ∗ f^* f∗ 是最大流,构建残量网络 G f ∗ G_{f^*} Gf∗:
- 正向边容量: c ( u , v ) − f ∗ ( u , v ) c(u,v) - f^*(u,v) c(u,v)−f∗(u,v)
- 反向边容量: f ∗ ( v , u ) f^*(v,u) f∗(v,u)
说人话,就是我们跑完 Ford - Fulkerson 算法之后剩下的网络。其中包括正向边和反向边,定义没有改变。
然后考虑定义割集:
U = { v ∈ X ∣ 在 G f ∗ 中 s → v 可达 } , V = X ∖ U U = \{ v \in X \mid \text{在 } G_{f^*} \text{ 中 } s \to v \text{ 可达} \}, \quad V =X \setminus U U={v∈X∣在 Gf∗ 中 s→v 可达},V=X∖U
由最大流性质, t ∉ U t \notin U t∈/U(如果 s s s 能够通过走正权边到达 t t t 的话那么就还有增广路,Ford - Fulkerson 算法这个时候还没有跑完),所以 c ( U , V ) c(U, V) c(U,V) 是合法割。
考虑分析这个时候的割边的性质:
- ∀ ( u , v ) ∈ E \forall (u,v) \in E ∀(u,v)∈E 且 u ∈ U , v ∈ V u \in U, v \in V u∈U,v∈V:(也就是原网络中的正向边)
则显然在最终的残余网络中, ( u , v ) (u,v) (u,v) 不存在了。(否则 v v v 应该属于 U U U)
则有:
f ∗ ( u , v ) = c ( u , v ) f^*(u,v) = c(u,v) f∗(u,v)=c(u,v)
- ∀ ( v , u ) ∈ E \forall (v,u) \in E ∀(v,u)∈E 且 v ∈ V , u ∈ U v \in V, u \in U v∈V,u∈U:(也就是原网络中的反向边)
根据上面同理 ( v , u ) (v,u) (v,u) 这条反向边不存在于残余网络中,否则 v v v 也应该属于 U U U。
则有:
f ∗ ( v , u ) = 0 f^*(v,u) = 0 f∗(v,u)=0
然后就考虑计算一下流的值,很显然就是这个式子:
∣ f ∗ ∣ = ∑ u ∈ S v ∈ T f ∗ ( u , v ) − ∑ v ∈ T u ∈ S f ∗ ( v , u ) |f^*| = \sum_{\substack{u \in S \\ v \in T}} f^*(u,v) - \sum_{\substack{v \in T \\ u \in S}} f^*(v,u) ∣f∗∣=u∈Sv∈T∑f∗(u,v)−v∈Tu∈S∑f∗(v,u)
代入上面我们推出来的割边性质:
∣ f ∗ ∣ = ∑ u ∈ S v ∈ T c ( u , v ) − 0 = c ( S , T ) |f^*| = \sum_{\substack{u \in S \\ v \in T}} c(u,v) - 0 = c(S, T) ∣f∗∣=u∈Sv∈T∑c(u,v)−0=c(S,T)
故 min c ( S , T ) ≤ c ( S , T ) = ∣ f ∗ ∣ = max ∣ f ∣ \min c(S, T) \leq c(S, T) = |f^*| = \max |f| minc(S,T)≤c(S,T)=∣f∗∣=max∣f∣。所以整理一下就可以知道 min c ( S , T ) ≤ max ∣ f ∣ \min c(S,T) \le \max |f| minc(S,T)≤max∣f∣。
所以这部分证毕。
综上可得:
- 最小割 ≥ \ge ≥ 最大流。
- 最小割 ≤ \le ≤ 最大流。
综上可得最小割 = = = 最大流,证毕。
Ford - Fulkerson 算法实现
我们可以从某个点开始深搜,搜到增广路就放手,然后重新来一遍。时间复杂度是玄学,只能拿到 84 分。
现在的问题是:如何找到两条互相相反的边。注意,图并不保证没有重边,所以我们不能使用 map。
但是我们可以在每一条边新开一个数,记录 ( u , v ) (u,v) (u,v) 这条边的反边 ( v , u ) (v,u) (v,u) 在 v v v 的邻接表里面的位置,这个可以在加边的时候就记录下来。然后就没有然后了。
模板题:P3376。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m, s, t;
const int N = 210;struct edge {int to, val;//要去的点,这个边的容量int id;//反边的位置
};
vector<edge> v[N];//邻接表
bool vis[N];//因为环只会让增广路上面能提取出来的流量越来越少,所以这里要排除环int dfs(int u, int fl) {//目前还需要找 x -> t 的增广路,返回 x->t 路径的最小边权(即能通过的流量)
//记 s->x 已经走的路径的最大边权为 flif (u == t)return fl;//如果已经搜到了终点就直接返回vis[u] = 1;//记录for (auto &[to, val, id] : v[u])//枚举边。因为我们待会要修改 val 所以要采用指针if (val > 0 && !vis[to]) {//如果要去的点没有被走过而且这条边能够通过流量int f = dfs(to, min(fl, val));//计算流量if (f > 0) {//如果这是一条增广路val -= f, v[to][id].val += f;//那么就更新正边和反边的容量return f;}}return 0;//没有找到增广路
}signed main() {cin >> n >> m >> s >> t;for (int i = 1; i <= m; i++) {int x, y, val;cin >> x >> y >> val;v[x].push_back({y, val, v[y].size()});//加边,注意要记录反边在对面邻接表的位置v[y].push_back({x, 0, v[x].size() - 1});//因为这个时候 v[x] 的长度已经比原来的多了一个,所以需要 -1}int ans = 0, f;//求最大流while ((f = dfs(s, 1e15)) != 0) {//找增广路memset(vis, 0, sizeof vis);//先初始化,为了待会更好地深搜ans += f;//直接加上增广路上面能提取出来的流量}cout << ans << endl;//输出return 0;
}
//Ford - Fulkerson O(?)
很容易发现,这个代码虽然很短,但是时间复杂度不能保证,以至于连 n = 100 , m = 5000 n=100,m=5000 n=100,m=5000 的数据也没法通过。
(如果我没有记错的话,这个东西的时间复杂度最差有 O ( n V ) O(nV) O(nV))
所以我们需要新的算法,这个算法就是 Edmond-Karp 算法。
Edmond - Karp 算法
Edmond - Karp 算法就是 Ford - Fulkerson 算法的一个优化话说怎么名字都这么长。
其最大的特点就是将 Ford - Fulkerson 的深搜找增广路改成了广搜找增广路。
很容易发现,Ford - Fulkerson 就是找到任意一条增广路就返回,重新更新 vis 数组了。但是有时候可以连续多找几条增广路,从而实现节省时间的作用。
这个时间复杂度是多项式的,可以通过模板题。但是已经有了更加好写的 Dinic 算法了,为什么不将其替代呢?
我们后面可以证实学习这种算法意义并不大。因为我们还有一种比它时间复杂度更加优秀,也更好写的做法。这就是 Dinic 算法。
Dinic 算法
Dinic 算法是 Edmond - Karp 算法的优化版,也就是 Ford - Fulkerson 的终极优化版。
其核心思想就一句话:先广搜分层,然后再按照层来找增广路。
注意,Dinic 算法仍然是每一次找一条增广路,然后重来继续找增广路。但是 Dinic 和 Ford - Fulkerson 也有很大的不同点。
前面我们谈到广搜分层,那么到底是个什么玩意呢?
第一层就是与 s s s 直连的点。
第二层就是与第一层直连的点。以此类推。
而我们的 Dinic 算法是这样的:只找 s → s\to s→ 第一层的结点 → \to → 第二层的结点 → ⋯ t \to \cdots t →⋯t 的增广路径,直到没有为止。
没有增广路径了怎么办呢?我们可以对剩下的残余网络进行重新分层。
补充说明:可能有些同学会问:“网络一直都没有变,分层怎么可能变呢?”,上面重新分层我少说了一个点,就是必须要走剩余容量 > 0 >0 >0 的边。
这样,每一次找到的都是当前最短的(即边数最少的)增广路。(因为显然总共的层数就是 s → t s \to t s→t 的最短路径,没有更短的了,这个很容易理解)
那走这样有什么好的呢?好处就是,我们可以计算时间复杂度了。
举个例子:
中间的边的容量为 1 1 1。
跑普通的 Ford - Fulkerson 算法的时候,我们会走很多增广路径,但是每一个增广路径只要通过了中间的边就一定不会很大。所以我们要很久才能得到正确的答案。具体的话呢, 2 2 2 条增广路就可以解决的事情我们用了 200 200 200 次,非常地浪费时间。
但是如果我们跑 Dinic 的话,我们就可能可以走最短的路径,最终得到正确答案 200。所以我们可以从直观理解来看,Dinic 确实会要比其他的算法要优秀的多。
Dinic 代码实现
在说明 Dinic 的时间复杂度之前,我们先来看一下实现。
具体就是每一次先 bfs 一遍,然后再跑增广路。能在模板题里面拿 92 分。注意到有一个点 T 了,而其他点的时间都还行。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m, s, t;
const int N = 210;struct edge {//意义同上int to, val;int id;
};
vector<edge> v[N];
int d[N];//记录i的层数,-1为未访问void bfs(int s) {//从 s 出发,采用广搜来分层memset(d, -1, sizeof d);queue<int> q;q.push(s), d[s] = 0;while (!q.empty()) {int f = q.front();q.pop();for (auto [to, val, id] : v[f])if (d[to] == -1 && val > 0)//注意,只能访问正权边d[to] = d[f] + 1, q.push(to);}
}int dfs(int u, int fl) {//意义同上if (u == t)//到达就结束return fl;for (auto &[to, val, id] : v[u])if (d[to] == d[u] + 1 && val > 0) {//只走正权边,而且是在两个相邻层数之间的边int f = dfs(to, min(fl, val));//继续前进的最大流 fif (f > 0) {//选择当前路径val -= f, v[to][id].val += f;return f;}}return 0;
}int dinic(int s, int t) {int ans = 0;//总流量while (1) {//每次一个阶段:先分层,再找当前层的所有增广路int x = 0;//当前增广路径流量bfs(s);//分层if (d[t] == -1)//不可到达就直接退出了break;while ((x = dfs(s, 1e15)) > 0)//找增广路ans += x;}return ans;
}signed main() {cin >> n >> m >> s >> t;for (int i = 1; i <= m; i++) {int x, y, val;cin >> x >> y >> val;v[x].push_back({y, val, v[y].size()});v[y].push_back({x, 0, v[x].size() - 1});//连边}cout << dinic(s, t) << endl;//跑最大流return 0;
}
Dinic 常数优化
我们来介绍一下当前弧优化。这个优化在其他东西里面也出现过但是我忘了。
当前弧优化这个优化我们一般都会写在 Dinic 里面,因为这个优化很重要,加上会对 Dinic 时间复杂度产生很大的提升。
PS:感谢 lml 巨佬指出问题。
进入正题,当前弧优化。
注意到某一个点 x x x 可以有很多入边,也可以有很多出边。如果对应的每一条入边,都需要走 x x x 的每一条出边来找到 t t t 的增广路,就会非常地浪费时间,甚至会导致时间复杂度退化。这也就是为什么有一个点比其他点多出那么多时间。
但是我们会发现,当一个 x x x 的出边 x → y x \to y x→y,对应的 y → t y \to t y→t 已经无法找到增广路的时候,其他点再走 y y y 已经完全没有任何意义了。因为走了 y y y 也找不到增广路,走它干啥。
所以这个时候可以把 y y y 设为不可达的,并使 x x x 在之后也不要访问 y y y(cur
数组就是干这个活的)。
加上这个优化之后就可以 AC 模板题了(这个模板题感觉数据造的不错),直接从 TLE 华丽变为 12ms AC。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m, s, t;
const int N = 210;struct edge {//意义同上int to, val;int id;
};
vector<edge> v[N];
int d[N];//记录i的层数,-1为未访问void bfs(int s) {//从 s 出发,采用广搜来分层memset(d, -1, sizeof d);queue<int> q;q.push(s), d[s] = 0;while (!q.empty()) {int f = q.front();q.pop();for (auto [to, val, id] : v[f])if (d[to] == -1 && val > 0)//注意,只能访问正权边d[to] = d[f] + 1, q.push(to);}
}
int cur[N];//当前弧优化,记录点i上一个未失败的边为e[i][cur[i]]int dfs(int u, int fl) {//意义同上if (u == t)//到达就结束return fl;//增加cur[x]是因为如果本次i号边找到了增广路,已经returnfor (int i = cur[u]; i < (int)v[u].size(); i = ++cur[u])if (d[v[u][i].to] == d[u] + 1 && v[u][i].val > 0) {//只走正权边,而且是在两个相邻层数之间的边int f = dfs(v[u][i].to, min(fl, v[u][i].val));//继续前进的最大流 fif (f > 0) {//选择当前路径v[u][i].val -= f, v[v[u][i].to][v[u][i].id].val += f;return f;} else//如果找不到就直接设为未可达了,因为其他点走到这个点也没有用处d[v[u][i].to] = -1;}return 0;
}int dinic(int s, int t) {int ans = 0;//总流量while (1) {//每次一个阶段:先分层,再找当前层的所有增广路int x = 0;//当前增广路径流量bfs(s);//分层if (d[t] == -1)//不可到达就直接退出了break;memset(cur, 0, sizeof cur);while ((x = dfs(s, 1e15)) > 0)//找增广路ans += x;}return ans;
}signed main() {ios::sync_with_stdio(0);cin >> n >> m >> s >> t;for (int i = 1; i <= m; i++) {int x, y, val;cin >> x >> y >> val;v[x].push_back({y, val, v[y].size()});v[y].push_back({x, 0, v[x].size() - 1});//连边}cout << dinic(s, t) << endl;//跑最大流return 0;
}
Dinic 时间复杂度证明
参考
论文
前面一部分就是算法的思路介绍,然后是伪代码,可以自行理解。后面就是证明了。当然后面的证明和论文的证明好像不太一样。
先说结论:Dinic 的时间复杂度是 O ( n ( n m + m ) ) O(n(nm+m)) O(n(nm+m))。
然后先介绍一下阻塞流(blocking flow
):就是在当前分层的图里面已经找不到增广路了,以前在这个分层的图里面找到的增广路的流量的和。
第一步,先来证明总共分层的次数不超过 O ( n ) O(n) O(n)。
很容易发现,在一个分层图的增广路已经全部消耗的时候, s s s 到 t t t 的任意路径上都至少一定有一条边的正向边边权变为 0 0 0。(这是显然的,不然就一定还存在增广路)
而这会使得在下一轮 BFS 分层的时候导致 s → t s \to t s→t 的最短路径增加至少 1 1 1,而 s → t s \to t s→t 的最短路径至多也就 n − 1 n-1 n−1,所以 BFS 分层的次数一定不超过 O ( n ) O(n) O(n)。
然后我们就只差证明 DFS 找所有的增广路的时间复杂度是 O ( n m + m ) O(nm+m) O(nm+m) 了。
默认这个时候的 Dinic 是我们的最终形态,即使用了当前弧优化。
使用当前弧优化(记录每个节点下次应尝试的边),就确保每条边在整个阻塞流计算中仅被访问一次(无论是否成功增广)。
考虑分别讨论成功找到增广路和找不到增广路的情形。很容易发现,找不到增广路一共就只有 1 1 1 次,因为发现就停止找增广路了。
- 成功找到增广路:路径长度 O ( n ) O(n) O(n),走过去再回溯回来,总共 O ( n ) O(n) O(n)。而这种情况的次数肯定 ≤ m \le m ≤m。(因为每条边都至多容量被归零 1 1 1 次)
- 所以时间复杂度 O ( n m ) O(nm) O(nm)。
- 没有找到增广路:每一条边至多在失败的寻找中出现 1 1 1 次,时间复杂度 O ( m ) O(m) O(m)。
然后把两个东西一加,然后乘上 O ( n ) O(n) O(n) 就可以得到 O ( n ( n m + m ) ) O(n(nm+m)) O(n(nm+m)) 了。
注意,这个复杂度为 O ( n ( n m + m ) ) O(n(nm+m)) O(n(nm+m)) 是当前弧优化的功劳,如果 Dinic 里面没有用当前弧优化,那么时间复杂度就不是这样子的。
为什么 Dinic 不多找几条增广路径?
根据前文和代码可以发现 Dinic 和 Ford-Fulkerson 都是每一次找增广路径都只找一条,但是如果我们可以多找几条路径,那么这个时间复杂度会不会减少呢?
例如这个例子:
这个时候我们显然可以同时提取两条增广路径,就可以获得 8 8 8 的流量。而两条路径一条一条地找的话就需要遍历两次,乍一看很快速的是不是?
教练和我一开始都是这么想的,然后教练一写,这是代码:
(很容易发现这就是某知名算法书籍的代码)
这是评测记录:
发现慢了很多!!!直接从 12ms 退化到了 279 ms!!!差了 20 倍时间!
考虑这是为什么。
首先仔细想想就可以知道这个“优化”效果并不大。我们只是少走了一点路径。原本需要走全程,现在只需要走一部分,因为你增广路本来就不会很多,而且每一条增广路还至多减少 n n n 个点的访问,而 n , m n,m n,m 都很小,显然不会有显著的优化。
而且在这个“优化”效果不大的同时,还造成了一个非常大的问题。
假设中间的点有很多条出边。
我们走最上面的哪条路径,发现成功了,然后这条路径就用完了。最后我们会发现中间的边会被访问多次,即使是使用了当前弧优化也很浪费时间。
所以时间复杂度又退化了个 m m m,不过这次没有完全退化,所以数据放这个做法过了。