先约定:记边数 ,点数 ,点集 ,边集 。
这篇 blog 旨在挂博主这只鸽子。
网络:有向图 ,对每条边 有容量 ,有唯一源点 和唯一汇点 。其中若 ,则 。
对一个网络 ,定义映射 ,满足:
- 容量约束:
- 反对称:
- 流守恒:
称这个映射为网络 的流,该网络是一个流网络。
注意看好这里的定义,特别是流守恒,不要弄错网络流研究的问题。在网络的一个可行流中,对于除源汇外的任意一个点,都应该满足 Σ流入 = Σ流出 !绝对不是 流出 ≤ Σ流入 之类的!
表示 到 的流量。 的时候、称 这条边饱和。
是网络的流。
点的高度:点到汇点的不加权最短路径长度;
点的层次:点到源点的不加权最短路径长度。
事实上因为习惯的原因,高度和层次这两个概念的定义经常会比较模糊(
残量(剩余流量函数):
给一个网络 , 是 的流。
——定义上面两个流的和 ,满足 ,可以证得它的容量约束、反对称、流守恒三个性质,于是流的和也是流。
增广路:在剩余网络里面的一条 到 的路径。
这个增广路和二分图里面的增广路是不一样的;当然也有相似的地方。
在流网络中,从源点流入无限的流量,然后求在汇点那里最多可以流出多少流量。
最常用的算法应该是 Dinic ,写 ISAP 也挺好的。
最大流-最小割定理:最小割 = 最大流。
——所以可以暴力枚举最小割,根据最大流-最小割定理求最大流的值。 。
容易想到每次找增广路,更新流量,直到残量网络内源点和汇点在两个连通块内。
这样就能得到最优解吗?
简单起见,暂且认为增广一律用 DFS 。
模拟几次就可以发现,假如我们现在有多条增广路,我们显然会随机选一条增广;然而这个时候,我们可能会走一条届不到最大流的错误的路。那好像就有可能得不到最优解了。有没有办法改进呢?
应该意识到的是,我们要坚持从源点出发到汇点的原则。就是说我们还是要找增广路。然后呢,之前那条劣的增广路:我们假设现在它中间有一些边的一部分容量不应该被浪费。
实际上甚至可以设得更简单一些:我们可以考虑它中间有一条边整条都不应该走,这个要推广回去是很简单的。
那么,这条路径除了这条边的部分还是要走的:这些部分显然还在最大流方案里面。所以我们要找到最大流方案,就相当于要给这条路径删掉不该走的部分、然后补上另外的路。
怎么做能够方便实现?现在上面那条路径变成了三部分,左部分、要删的、右部分。首先我们会从源点引一段路接上右边的部分。然后从左边的部分引一条路到汇点。会做这种事情的前提显然是走现在的两条路比走原来的路径更优。
比较大小可以化差,于是不难想到用负权边来代替“删除走错的边”这一操作。
现在我们还是从源点开始,先引一条路到右部分的左端:这时候我们不要去汇点,而是应该走那条负权边,然后刚好连到左部分的右端,接着引一条路到汇点。这样就用一条路完成了删补补三件事情。
“删除走错的边”的根本目的还是得到更优的解,增广也是要得到更优的解,注意到这个,于是会产生把前者统一到后者的想法;把前者统一到后者,那么我们要用后者的方法解决前者,就是走增广路;
注意到会损失一部分贡献然后加上另一部分贡献,联想到负这个概念,所以想到了负权路,放到实现里面也发现是反向边。
这条负权边应该是要反着走的,也就是反向边;所以建图的时候就得多建一倍的边了。现在建图的时候就得先把一条边跟它的反向边配对。之后我们叫正、反向边就不是建图时候的意思了,而是相对的。
比如说,一开始的正向边的反向边是对应的一开始的反向边,然后一开始的反向边的反向边是对应的一开始的正向边。
为了配合各种定义,走一条有向边的时候要给这条边的容量减掉流过去的量,给对应反向边的容量加上同样的量。
这样的话,我们一直增广,一定能找到最优解。现在也不用担心最大流会被阻塞了。同时也不会出现无限循环的事情。因为每次增广都会对答案有贡献。
于是可以得到 Ford-Fulkerson 方法 :每次找增广路,沿增广路更新流量。
称采用 Ford-Fulkerson 方法的一类求最大流的算法为增广路算法。
注意:这个方法如果用 DFS 实现的话,只保证有解和能找到解,效率嘛……虽然每次增广都会产生贡献,但是产生的贡献可能会每次都很少。
比如:+1 +1 +1 …
记最大流为 ,最多找 次增广路,增广一次 ,则复杂度 。
前面 DFS 会炸是因为 DFS 的增广是盲目的。但是 BFS 不一样。
可以每次用 的 BFS 找增广路增广,这样和用 DFS 增广的不同之处在于 BFS 找到的增广路的长度(不带权)是单调不递减的,并且在这种情况下可证得最多增广次,于是复杂度 。
引入了层次图的概念。
构建层次图, 次 BFS 增广直到源汇不连通,然后重新构建层次图增广,重复上述步骤直到层次图中的源汇不连通就找到最大流了。复杂度 。
它跑网络流和二分图都比较快。很常见。
Dinic 算法用一次 DFS 代替 MPLA 的多次 BFS 来进行增广。则 Dinic’s algorithm 的复杂度是 ;
特别地,对 unit network , Dinic’s algorithm 的复杂度是 。
由此可见,在二分图上(最大匹配)跑 Dinic’s algorithm 的时候,复杂度是。
Dinic 算法可以加一个 cur (当前弧)优化:在某一轮构建出的层次图里,对于从一个点引出去的多条边,假如里面有几条边(在这一轮里)被走过了,我们就不会再走那几条边。
当前弧优化的前提是我们 DFS 的时候,会后退当且仅当没有路可以走了。不过这个同时也是为了保证复杂度必须采取的移动策略,所以这是废话(
另外我们还可以再加一个优化,就是 BFS 构建层次图没有必要构建完,只要源汇连通就可以了。它的可行性在于,我们不用实际地构建层次网络,只需要记录一下层次,所以源汇连通的时候,我们已经遍历完深度更低的所有点了。显然我们需要的那一部分边都出来了。
PS. 建层次图可以从源点开始 BFS 也可以从汇点开始,这个看个人喜好?
可以用 LCT 优化成 。
首先、边在 LCT 里面变成点。然后用 LCT 实现以下操作:
- 插入:加边(增广)、直到源点和汇点都在同一棵树里;
- 查询:找到增广路上残量最小的边;
- 修改:更新增广路上边的残量;
- 删除:删掉残量为零的边。
分析一下复杂度?
- 一般流网络 的证明:
根据层次和层次网络的定义,最多也就是构建 次层次图;每次 BFS 构建层次图是 的。
现在考虑增广的时间复杂度。本来我们 BFS 增广,每次至少让一条边消失, ;现在改用 DFS 增广,要期望它的复杂度有所降低,就要从另外的角度进行分析。尝试把目光放到整个层次图上来分析。这同时也是因为 DFS 的过程是盲目的、难以预测的;我们并没有一个所谓更优的选择路径的策略。
于是要将 DFS 对复杂度的贡献分开来计算,尝试拆成修改的复杂度和移动的复杂度。
修改 是怎么样的?我们考虑整张层次图,按照上面的结论,一张层次图增广 次,每次增广修改的次数是 的。这个结论对于 BFS 同样适用,这也正如我们所预料的: BFS 最有可能出现冗余步骤的地方正是它的搜索过程。
分析 DFS 的 移动 。之前提过不少次了,增广的次数是 的;但是我们一次增广,移动的步数现在就不一定是 的了,毕竟不可能一下子就找到增广路。所以我们要对那些找不到增广路的步骤进行分析。“找不到增广路”,实际上讲的是不能到达,或者说是走到的点没有出度了。这个时候我们显然要回溯:同时,这个点已经是没用的了,可以删除。很显然,我们删除一个点同时也意味着我们后退了一步。把后退的代价拆出来,后退次数是 的,同时对答案有贡献的“前进”次数前面分析过了是 的。需要注意的是,这一部分的复杂度分析基于层次图的设计和正确的移动策略(就是说不要随便后退,一定要等到无路可走了再退)。
可以说层次图是一个很巧妙的设计。它将一开始 EK 中 BFS 的优势转移到层次图上来,为 DFS 增广提供了可能性。
Dinic 的常数一般比较优秀(当然想要让它跑满很轻松【参考】)。
在一些数据下,可以在 DFS 增广的时候不给反向边加流量,等到增广完毕再加上去(就是等到下一次增广的时候再退这一次的流),可能会快一点?【引用】
#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cctype>
#include <queue>
#include <algorithm>
using namespace std;
const long long inf = 2147483647147483647;
int n, m, S, T, tot = 1;
int head[105], nxt[20005], to[20005], dep[105], cur[105];
long long cap[20005];
#define add_edge(a, b, c) nxt[++tot] = head[a], head[a] = tot, to[tot] = b, cap[tot] = c
int q;
queue<int> Q;
bool pre() {
while (!Q.empty()) Q.pop();
for (register int i = 1; i <= n; ++i) dep[i] = -1;
dep[S] = 0;
Q.push(S);
while (!Q.empty()) {
q = Q.front();
Q.pop();
if (q == T) {
return 1;
}
for (register int i = head[q]; i; i = nxt[i]) {
if (cap[i] > 0 && dep[to[i]] < 0) {
dep[to[i]] = dep[q] + 1;
Q.push(to[i]);
}
}
}
return 0;
}
void update(const int &i, const long long &c) {
cap[i] -= c;
cap[i ^ 1] += c;
}
long long preflow;
long long aug(int x, long long flow) {
if (x == T) {
return flow;
}
for (register int i = cur[x]; i; i = nxt[i]) {
cur[x] = i;
if (dep[x] + 1 == dep[to[i]] && cap[i] > 0) {
preflow = aug(to[i], min(flow, cap[i]));
if (preflow == 0) {
continue;
}
return update(i, preflow), preflow;
}
}
return 0;
}
int main() {
long long c;
scanf("%d%d%d%d", &n, &m, &S, &T);
for (register int a, b, i = 1; i <= m; ++i) {
scanf("%d%d%lld", &a, &b, &c);
add_edge(a, b, c);
add_edge(b, a, 0);
}
long long maxflow = 0, temp = 0;
while (pre()) {
for (register int i = 1; i <= n; ++i) cur[i] = head[i];
while (temp = aug(S, inf)) maxflow += temp;
}
printf("%lld", maxflow);
return 0;
}
- 理论复杂度依旧是 ,不过常数(在大多场合下)很小,而且代码短。【参考】
ISAP 可以算作是改进的 Dinic 算法(虽然 SAP 是指 EK ……)。Dinic 每次都要重建层次图,但是层次图的改动实际上并不大——于是想到尝试在 DFS 的时候顺便修改层次图。
比如:现在在某个点上,当前层次图中这个点已经不能再拓展了,这时候我们就要修改这个点的层次以寻找继续拓展的可能性。
现在需要找一条路往后走,那么应该是要把当前点的层次修改到可以接上后面至少一个点的程度。同时因为我们要保证走的是最短路,很显然是要接到汇点最短的那一条。
所以现在我们记录的不应该是点的层次了,而应该是点的高度(不过为了方便一般还是叫做层次,就当作是把 作为源点的层次之类的?)
所以我们每次先从 开始 BFS 初始化层次图,然后 DFS 找增广路顺便更新层次图。
另外, ISAP 一般还会加上一个 GAP 优化:如果增广的时候,某一次重标记一个点的层次后,发现已经没有哪个点的层次和这个点之前的那个层次一样了(或者说某个层次上已经没有点了),那么 和 实际上已经不连通了,就再也找不到新的增广路了。算法结束。
前面 Dinic 的一些常数优化显然也同样适用于 ISAP 。
实现的时候, ISAP 的 BFS 步骤实际上也可以用 DFS 重标号的形式来间接完成。不过速度可能会稍微降低(虽然我觉得是我写错了
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cctype>
using namespace std;
#define getchar() (frS==frT&&(frT=(frS=frBB)+fread(frBB,1,1<<12,stdin),frS==frT)?EOF:*frS++)
#define add_edge(a,b,c) nxt[++tot] = head[a], head[a] = tot, to[tot] = b, cap[tot] = c
const int MAXN = 105, MAXM = 10005;
const int inf = 2147483647;
char frBB[1<<12], *frS = frBB, *frT = frBB;
int N, M, S, L, head[MAXN], nxt[MAXM], to[MAXM], dep[MAXN], gap[MAXN], tot = 1;
int cap[MAXM];
void read (int &x) {
x = 0; char ch = getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
}
int flowrec;
int aug (const int &x, const int &flow) {
if (x == L) return flow;
int cost = 0;
for (register int i = head[x]; i; i = nxt[i]) {
if (dep[x] == dep[to[i]] + 1 && cap[i] > 0) {
flowrec = aug(to[i], min(flow - cost, cap[i]))
cap[i] -= flowrec, cap[i^1] += flowrec;
if ((cost += flowrec) == flow) return cost;
}
}
if (!--gap[dep[x]++]) dep[S] = N + 1;
return ++gap[dep[x]], cost;
}
int ta, tb, tc;
long long ans;
int main () {
read(N); read(M); read(S); read(L);
for (register int i = 1; i <= M; ++i) {
read(ta); read(tb); read(tc);
add_edge(ta, tb, tc);
add_edge(tb, ta, 0);
}
for (ans = 0, gap[0] = N; dep[S] <= N; ans += aug(S, inf));
return printf("%lld\n", ans), 0;
}
我觉得不太行。
这个算法叫做位缩放。大概思路:二进制拆分容量。从最高位开始、每次加一位然后试图增广填上多的容量。复杂度有点复杂,
之前介绍的都是增广路算法;HLPP (最高标号预流推进)则是一种预流推进算法。
增广路算法基于路径/割;预流推进算法基于点。 HLPP 的理论复杂度比上面介绍的几个要低,但是常数比较大,实际效果一般不是很好。
ps. 我下面写嗨了,看着烦就跳过吧(
预流推进大概是这样的:首先,下面讲到的推进,比如说 push(u,v) ,仅当 u 的高度等于 v 的高度加 1 的时候才会发生。
首先,我们从源点那里流进去无限的流量;然后沿着出边把流量推进给连着的点;接着把连着的点加入队列,让它们继续把流量向汇点推进。假如在这个过程中,流入某个点的流量没办法全部流出,就让某些入边反向,把多出来的流量回流。注意:整个算法进行的过程中,跟增广路算法不一样,这里只会往网络中流入一次流量:在一开始的时候,从源点那里流入。
但是如果只是这样的话显然可能会出现流量反复横跳的问题,所以得把有向边用无向边代替,具体说就是加入类似于层次图的概念,不过在这里变成了记录高度。只允许“水往低处流”。这样的话,前面的“让某些入边反向”就有了一个更明确的定义:把当前点的高度擡高,这样自然就可以让一些流量回流了,也算是达到了反向的效果吧?
需要注意的是,在这里,某一个点上的流量只是暂时储存的(补充一个概念:在某一个时刻,暂时储存了流量的点叫做储流点,或者 active node ),就是说我们不会保存在这个点这里曾经流过了多少流量之类的,流走了就是流走了。
实际上讲起来感觉挺自然的,要特意提是因为前面增广路算法里面是一点一点流的,就得知道从某条边上曾经一共流过了多少流量,才知道有没有超过容量了;但是这里不同,预流推进算法是预流的时候直接流满,流到过载,再慢慢把过载的部分调回去,在这种调整的过程中,某条边上流过的流量只会减少,不会增加,所以就不用考虑“某个点/边上一共流过多少流量”了。
上面提到的推进过程中,如果一次推进使一条边的流量达到容量,那么称这一次推进是 saturating ;否则称为 unsaturating push 。
预流推进算法的复杂度(用一般的 FIFO 队列实现):
relabel:首先注意到除了源点和汇点不会被重新标号之外,每一个点的高度在算法进行的过程中一定只会变大,不会变小;并且每个点在算法进行的过程中可能达到的最大高度应该是 2n-1 。重标号的时间复杂度一共 。
Saturating Push:每条边上都会正向 saturating push 次,同时最多会被反向 saturating push 次,一共最多可能发生 2n 次 saturating push 。显然 Saturating Push 的时间复杂度一共 。
Unsaturating Push / Non-saturating Push:这个分析比较复杂。可以考虑用势能分析,那么首先要找到一个合适的势能,并且我们容易发现在算法进行的过程中,所有 active node 的高度和初始为 0,然后经过一系列变化最终归于 0 。并且在这个变化的过程中, 所有 active node 的高度和一定不会是负数。记所有 active node 的高度 ℓ 的和为 Φ ,前面分析过的 relabel 和 saturating push 两种操作都只会让 Φ 不减,那么让 Φ 归零的任务就只有 unsaturating push 可以完成了。 现在回过头来深入分析一下前面两个操作对 Φ 的影响: relabel 不会改变 node 的 active 状态,只会擡高 ℓ ,且一次 relabel 只会让 Φ 增加 1 ,显然 relabel 一共最多令 Φ 增大 。然后, saturating push 对 Φ 的影响是通过 activating inactive nodes 实现的,一次最多令 Φ 增大 2n-1 ,最多 saturating push 2nm 次,合计的话大约是令 Φ 增大 。又因为 unsaturating push 一次最少令 Φ 减小 1 并且的确可能一直只让 Φ 减少 1,所以 unsaturating push 的复杂度 。
预流推进算法有一个重要的优化,就是 cur 优化,延伸出一个 discharge 操作:类比前面讲过的那种 cur ( Current-Arc ) 优化,容易发现 HLPP 要做 cur 优化,就必须强迫:走一条边的话、就要把所有经过那条边可以到达的地方都给走一遍,把那里面所有的 saturating push 和 unsaturating push 都给处理完。这样的话,走过的边才不需要再走。
HLPP 则采用优先队列,每次从优先队列/堆中取出高度最高的点推进。
Cheriyan 和 Maheswari 利用 trace (忽略细节来说是指 flow atom 的过去运动路径)证明了采用 cur 优化的时候, 采用 Largest-Label 的预流推进算法的复杂度是 ; Levent Tuncel 则利用 future course (忽略细节来说是指 flow atom 的未来运动路径)证明了不采用 cur 优化的情形下, HLPP 的复杂度同样是 。
具体的复杂度证明可以参考 Tuncel, L. 「On the complexity of preflow-push algorithms for maximum-flow problems」。
前面分析 saturating push 和 unsaturating push 的复杂度的时候,讲得有些模糊,最主要的原因就是进行一次操作的时候势能具体的增减情况非常的不确定。所以考虑定义一个新的势能。
Fʟoᴡ Aᴛoᴍ - Dᴇғɪɴᴀᴛɪoɴ
Ⅰ 对点 v 完成一次 relabel(v) 操作后,会在点 v 处产生一个 flow atom 。如果点 v 处本来就有一个 atom ,那么旧的 atom 会消失。
Ⅱ 在 saturating push(u,v) 的时候, u 点上的 atom 会消失,然后在 u 点上产生一个新的 atom ,这个 atom 会沿着 (u,v) 移动到 v 点。如果在 saturating push 操作结束之后, u 点仍然 excess ,那么在 u 点处会再产生一个新的 atom 。
Ⅲ 假如在 push(u,v) 操作进行之前 v 点就已经 excess ,那么操作完成后会在 v 点产生一个 atom 。当然, v 点上如果本来就有 atom ,那么在操作完成后 v 点上本来有的 atom 会消失。
根据之前的势能分析,不难做出这样的一个定义。分析 flow atom 显然要比分析高度的变化要来得简单一些。可以发现 flow atom 实际上就是仅随着 unsaturating push 移动的 flow excess 。(如果 saturating push , atom 会 die ;如果 relable , atom 也会 die )
然后考虑 largest-label 时 unsaturating push 的复杂度。直接的势能分析只能得到比较弱的上界,那可以考虑一下其它的复杂度分析方法:比如经常见到的那种,因为对于路径的重叠有限制,所以复杂度被图的点数或者边数限制一类的情况。
那首先就得有一个运动的概念。 flow atom 的定义Ⅱ里面就有讲到 flow atom 的移动,然后这里再定义一个 future course 表示某个 flow atom 直到消失前的移动路径。
注意:一、future couse 不考虑源点和汇点;二、如果一个 atom 移到一个点上就立刻消失了,那么最后这一步移动不算在 future course里面。
关于 future course 有什么可以分析的马?考虑一下两个同时存在的 flow atom ,它们的 future course 会有交点吗?——假设有,首先两个 atom 不可能同时在那个交点上,不然就消失了,也就没有交点了;其次,如果一个先一个后的话,先到那个交点上的点在到交点之后就不能移动了:因为我们是遵循 Largest-Label 规则的,一个点到交点上了,另一个点显然拥有一个更大的高度( label ),那么这个时候就得先移动拥有 largest label 的点,然后两个点还是会在一起,仍然不能相交。所以假设不成立。
也就是说同一时刻,网络上任意两个 flow atom 的 future course 不相交,并且 flow atom 的总数应该约是 的。一个 atom 的 future course 长度 ,假如直接用这个来算的话次数是 的 ;但是 label 是会变的,并且会影响到 future course 的长度,会影响到 push 的次数。
关于 label ,有一个结论: largest label 最多大约变化 4n2 次。论文没有给出证明,我猜是因为 relabel 最多大概 2n2 次,然后因为采用 Largest-Label ,一次 relabel 最多只能提供 2 次改变 largest label 的机会?(口胡
注意到之前对 unsaturatnig push 的次数分析里面, 每一次产生的 flow atom 在非极端小数据的情况下, future course 的长度貌似不太可能一直都接近 。可以考虑将 future course 按照长度分类。
那么 future course 可以分长度大于等于 ξ 的和长度小于等于 ξ 的情况来讨论,显然长度小于等于 ξ 的 future course 产生的 unsaturating push 最多 次。
长度大于等于 ξ 的 future course 就要被某一种 phase 限制:前面讲了 largest label 最多大约变化 4n2 次,这个结论就可以在这里利用,把一次 largest label of active nodes 的变化当作一个 phase 的结束和另一个 phase 的开始。一个 phase 中所有的 push 操作都会以拥有当前 phase 的 largest label 的那些点为起点被执行。要把这些 future course 限制进同一个时刻的网络里,那么可以考虑这些点是不是在某一个时刻全部都出现了:因为采用的是 largest label ,那么我们在这个 phase 里面显然不应该产生新的 label 等于该 phase 的 largest label 的点。那么,一个 phase 里面很显然最多只能有 个长度大于等于 ξ 的 future course ,一共就有 个。
取一个 ξ 最小化 ,尝试令 ,解得 。
所以 HLPP 的复杂度是 。
相对其它的最大流算法来说, HLPP 主要是对稠密图特化的(毕竟它复杂度里面的 m 带了个根号,而无论是 Dinic + LCT 、 ISAP 、 还是 Orlin + KRT , 复杂度里面都是完整的 m )。
HLPP 的核心部分代码其实可以写得比较短【参考】。不过跑随机图的话, ISAP 的常数太优秀了以至于远远达不到上界,所以基本上还是能够吊打常数相对大的 HLPP ;而且 ISAP 的代码还可以写得更短……(当然特意构造的话还是能够卡复杂度的 【参考】,不过最多也就是卡一下娱乐?)
PS. HLPP 显然也可以用 GAP 优化。【参考】
关于理论复杂度低的最大流算法:首先,对于 oier/acmer 来说它们肯定是用不到的。 Orlin’s algorithm 的理论复杂度近 (虽然这个的价值远不如 Spielman-Teng);也有复杂度接近 的算法【参考】。
另外,网络流问题还可以用网络单纯形法(不是线性规划那个单纯形法)解决【参考】。网络单纯形法的速度非常快【参考】,当然实现起来也比较精污(一般来说最大流就 dinic 或者 isap ,费用流就 zkw)。如果有哪天必须得写网络单纯形 + 动态树,那我想出题人得先被吊起来打一顿(
最左转线也叫做最小左转法。
例题:WC2013. 平面图 hnoi2016 矿区 ZJOI2008 Risk
只要求求出费用最小的流。连续spfa即可。
对费用先求一条最短路,然后继续求最短路、如果求出来的最短路权和为负就加上。
如果想要 dijkstra 就用 Johnson 那个方法,把所有边 的权 重赋权为 得到正权图。
首先是一个最大流,然后要求费用最小。
一般来说用 EK (也有人叫做 SPFA 费用流)就够了,基本上也不会有人专门去卡 EK 。分类讨论和处理也不繁琐,最多就是消负环。
如果你坚持拥护 spfa 死了就写 dijkstra 吧。。。或者写完 spfa 改成 dijkstra??至于写起来有多恶心(每次搞都想婊一波卡 spfa 的,,,
反正大力上啊 。
写带优化 spfa 卡常数就别想了 跑负权一炸一个稳 优化多了还不如写 dijkstra 。。。
如果当前费用是在当前流量下的最小费用,那么以最小费用增广之后的费用也为增广后的流量下的最小费用。不断增广找到的就是最小费用最大流。所以把费用作为权值,每次跑最短路增广直到最大流就可以了。记最大流为 ,如果用 SPFA 找最短路(代替 BFS 找增广路),用 Dijkstra 增广,那么复杂度为 。
实际上,就像最小费用最大流问题是最大流问题的推广,增广路算法和预流推进算法都有一定的从最大流推广到最小费用最大流问题上的可能。
慢。
消圈定理:一个流是这个流量下最小费用流的充要条件是残量网络中不存在负费用圈。
实现:先最大流求残量网络,这时候残量网络 S - T 可能有带负权的增广路,然后消负环。因为消的是环,所以最大流不变;因为消的是负,所以费用减小。
复杂度 。其中 是边的最大容量, 是边的最大费用。最大流费用不超过 ,而每次消去负费用圈至少使得费用减少 ,因此最多执行 次 的找 + 消负环。
关于费用流负边、负环的处理:消圈算法比较慢。消除负环则可以不断 SPFA 找负环,然后把整个负环上面所有边的容量减去负环上容量最小的边的容量。负环消除后,消负边可以考虑重赋权【参考】, 求一次每个点到汇点的最短路,然后把每条边 的 加上 再减去 得到 reduced cost ,用必定非负的 reduced cost 跑费用流算法。注意这样跑出来的费用要进行调整。
reduce cost 之后,假如说之前这条边是负权边,那么 reduce cost 之后它的权值为 (因为负环被消了, ;假如之前权非负,为 ,那么有 那么 reduced cost 。并且在之后的增广过程中不会产生新的负权边:因为每次增广走的都是最短路上的边。
做费用流的时候,增广后要重新标号的原因是什么?因为增广可能会破坏最短路,让每个点的顶标失效;然后就得重新用最短路算法求出每个点到汇点的最短距离作为合法的顶标。
然而被改动的顶标的数量是有限的。可以考虑直接重标号。类比 KM 算法,求每次 S-T 不连通时的 ,将这一轮中最后一次 DFS 访问过的点的顶标 增加 ,然后重复增广。
缺点挺明显的。如果用来跑稀疏图会爆炸。
连续最短路(SPFA + SLF) + 多路增广(DFS) + Reduced Cost 。在 Reduced Cost 下, 一个是因为边权(指费用)缩小, SPFA 的速度会加快,并且除了第一次跑 SPFA 重赋权,后面都是在正权图中跑 SPFA ,此时用 SLF 不会退化并且可能可以起到一定的玄学优化效果。实际操作起来因为现在 SPFA 基本会随手被卡…… .
正权图非要跑 SPFA 是作死,更稳妥的话:第一次用 SPFA 处理掉负权。之后在 Reduced Cost 下都是正权图就一律用 Dijkstra + Heap (要追求效率就手写堆,偷懒就 priority_queue 。用 stl 的话写起来 dij 跟 spfa 差不多长,虽然会让常数变大)。正权图也可以用 SPFA + SLF 跑,看脸。注意一下,因为这里负权图只用跑一次就还无所谓,不然带 SLF 跑负权图可能爆炸。
注意一下 SLF 优化不要拿来跑负权图,复杂度可能会退化。
Primal-Dual 某种程度上也许可以看成 isap 的推广(?
朴素 Network Simplex 的论文:J. B. Orlin「A polynomial time primal network simplex algorithm for minimum cost flows」
LCT 优化:R. E. Tarjan「Dynamic trees as search trees via euler tours, applied to the network simplex algorithm」
工业用。不要想着考场上能够码这种东西((
- 无源汇有上下界可行流(循环流) -
- 有源汇有上下界可行流 -
- 有源汇有上下界最大流 -
可行流 + 最大流 即可。
- 有源汇有上下界最小流 -
- 有源汇有上下界费用流 -
一个无向图是二分图的充要条件:不存在奇环。
二分图最大匹配 = 最大流。
Dinic 证明了 Dinic’s algorithm 求最大匹配的时候最多增广次,也就是说这种情况下它的时间复杂度是 。
这时候的 Dinic’s algorithm 也被称为 Hopcroft–Karp algorithm 。
Dinic’s algorithm 跑二分图最大匹配曾经是理论复杂度坠吼的那个,不过之后被预流推进翻了。当然实际上写还是写 Dinic 好吧(
二分图最大权匹配 = 最大费用流。
二分图最小点覆盖 = 最大费用流。
König 定理:最小点覆盖 = 最大匹配
BZOJ1143
任意图最大独立集 = 点数 - 最小点覆盖。
https://yzmduncan.iteye.com/blog/1149057
任意图最大点权独立集 = 点权和 - 最小点权覆盖。
无向最大团 = 补图最大独立集
无向图最大独立集 = 补图最大团
最大权闭合子图 = 正点权和 - 最小割
- 一般形式 -
无向图子图密度:
转化成分数规划 +最小割。
- 点权 -
- 点权 & 边权 -
带花树。
一个特殊情况是平面图中的全局最小割,