我不会图论

基本定义

边导出子图:选出若干条边,以及这些边所连接的所有顶点组成的图叫做边导出子图。

点导出子图:选出若干个点, 以及两端都在该点集的所有边组成的图叫做点导出子图。

闭合子图:定义在有向图上,点集 \(V\) 的闭合子图是所有 \(V\) 可达的点的点导出子图。

1.最短路

1.1 Bellman-Ford 算法

一种非常暴力的求解最短路的方法 。

称一轮松弛为对于每一条边 \((u, v)\), 用 \(dis_u+w_{u,v}\) 更新 \(dis_v\), 对于每一条边执行。 每一轮都至少有一个点的最短路被更新, 松弛 \(n - 1\) 遍即可。

若第 \(n\) 次松弛还有节点的最短路被更新, 那么图存在负环。

时间复杂度为 \(O(nm)\),实际上远远跑不满 \(n\) 轮。

1.2 Dijkstra

一种基于贪心的最短路算法,适用于非负权图

称一轮扩展节点 \(u\) 为对于 \(u\) 的所有出边, 用 \(dis_u + w_{u,v}\) 更新 \(dis_v\)

在已经得到最短路的节点中, 取出没有扩展过的 dis 最小的节点并扩展,由于没有负权边, 每次扩展的 dis 单调不降。

每次取出一个节点,就是它的最短路。使用优先队列,每次取出队首即可。

注意一个点有可能多次进入优先队列, 但它第一次被取出时,得到的一定是最短路, 为此记录 vis[i] 表示一个节点是否被扩展。

1.3 SPFA

关于 SPFA,它__。

松弛节点 \(x\) 时找到接接下来有可能松弛的点, 即与 \(x\) 相邻且最短路被更新的点。

时间复杂度相比 BF 没有差异, 在一般图上效率很高,但容易被特殊构造的数据卡成平方,因此能用 dijkstra 就不要用 SPFA。

一个点被松弛时不一定是最短路。

SPFA 可以用来判负环:若一个点进入队列超过 \(n - 1\) 次,或者最短路大于 \(n - 1\),则存在负环。

1.4 Johnson

用来解决带有负权边全源最短路问题。

Johnson 算法的巧妙之处在于为每个点赋予势能 \(h_i\),正如物理上的势能, 从一个点走到另一个点, 势能的总变化量是一定的, 我们将边 \((u, v)\) 的权值 改为 \(w_{u,v}+h_u - h_v\)

容易发现在更改边后,从 \(S\)\(T\) 的最短路就变成了 \(dis +h_S - h_{T}\), 这与路径上经过了哪些点无关, 只与 \(S, T\) 有关,因此,原图上 \(S \to T\) 的最短路在新图上仍是最短路。

接下来我们要合理地为每个点附上势能, 使得 \(w_{u,v}+h_u - h_v \geq 0 \Leftrightarrow h_u + w_{u,v} \geq h_v\), 即为三角形不等式。

如图无负环,可以直接使用 BF 算法,初始令 \(h\) 全为 0, 做 \(n - 1\) 遍松弛即可。

此时无负权边,可以放心用 dijkstra 对于每个 \(i \in [1, n]\) 计算从 \(i\) 出发的最短路, 总时间复杂度为 \(O(nm + nm\log n)\)

最后把 \(dis_{u,v}\) 加上 \(h_v - h_u\) 即是原图最短路。

Floyd 与传递闭包:

Floyd 同样可以解决带有负权边的全源最短路问题, 只不过复杂度为 \(O(n^3)\)

算法只有四步:枚举 \(k\), 枚举 \(i\), 枚举 \(j\),用 \(dis(i,k)+dis(k,j)\) 更新 \(dis(i,j)\)

注意枚举顺序,作为中转点的 \(k\) 必须在最外层枚举。

正确性证明:考虑归纳法。

考虑 \(u \to v\) 的最短路 \(u \to p_1 \to p_2 \dots \to p_k \to v\), 令 \(p_i\)\(p\) 中最后一个枚举的点,若 \(dis(u,p_i)\)\(dis(p_i,v)\) 均取到最短路, 那么 \(dis(u,v)\) 自然也取到最短路。

此外 floyd 还可以求解传递闭包,有向图 \(G\) 的传递闭包定义为 \(n\) 阶布尔矩阵 \(T\)\(T_{i,j} = 1\) 当且仅当 \(i\) 可达 \(j\)

bitset 可以将传递闭包优化成 \(O(\dfrac{n^3}{w})\), 缩点后传闭包可以做到 \(O(\dfrac{nm} w)\)

例题

[GXOI/GZOI2019]旅行者

不同的两个数至少有一位二进制位不同,直接对每一个二进制不同的位置分两组跑最短路即可。

[CSP-S 2021] 交通规划

考虑网络流,源点 \(S\) 向所有黑色附加点连边,所有白色附加点向 \(T\) 连边,答案就是最大流。

由于是平面图,当 \(k = 2\) 时直接平面图转对偶图后跑最短路。

\(k>2\) 时,由于源点向黑色附加点连边会有交叉不好直接转对偶图,由于相邻黑色点之间不会连边,考虑相邻的异色点之间新建点,设 \((0, 1) / (1, 0)\) 表示这个点相邻点颜色,那么我们让 \((0, 1)\)\((1, 0)\) 匹配,容易发现这样的匹配与删边方案一一对应。

现在我们要证明,匹配不会出现相交的情况,因为如果存在相交的两条路径,显然可以改变匹配点使得不相交而且花费减少。

由于两两不相交,直接区间 dp 即可,破环成链,设 \(dp_{l,r}\) 表示区间 \([l,r]\) 的匹配情况,要么 \(l\)\(r\) 匹配要么 \(dp_{l, p}+dp_{p+1,r}\)

预处理两两匹配点的最短路,时间复杂度 \(O_(knm\log {nm}+k^3)\)

[POI2014]RAJ-Rally

有向图删点最短路。

2.差分约束

差分约束问题形如 若干个 \(x_a - x_b \leq c\)\(x_a - x_b \geq c\) 的形式,求出一组 \(x\) 的合法解。

将条件改写成 \(x_a + c \geq x_b\),即为三角形不等式,由于 \(c\) 一般都有负数,因此使用 spfa 跑最短路,每个点的 \(dis_i\) 就是答案。

例题

P5590 赛车游戏

\(d_x\) 表示 1 到 \(x\) 的最短路长度,只要保证对于所有边 \((x, y, z)\)\(d_y = d_x+z\) 就能保证所有 \(1 - n\) 的路径长度相等。

由于 \(1 \leq d_y - d_x \leq 9\), 直接转化成差分约束即可。

注意判断 1, n 不连通的情况和无用边。

[省选联考 2021 A 卷] 矩阵游戏

显然可以看出,若我们确定 \(a_{1, i}\)\(a_{i,1}\) ,整个矩阵完全确定。

先假设他们都等于 0,得到一组特解 \(a_{i,j}\), 由于 \(b_{i,j}\) 是一个矩形的和,当我们把一行 / 列同时加上一个正负号交替的数时,仍然合法。

也就是说,设 \(r_i, c_j\) 分别表示第 \(i\) 行, 第 \(j\) 列的加的总和,那么所有合法的 \(a_{i,j}\) 都能写成 \(a_{i,j}+(-1)^i c_j+(-1)^{j}r_i\)

于是我们要求 \(0 \leq a_{i,j}+(-1)^ic_j+(-1)^jr_i \leq 10^6\)

由于可能 \(i, j\) 奇偶性相同导致无法变成差分约束的形式,我们考虑如下构造:

\[\begin{bmatrix} +, -, +\dots, + \\ -,+,-\dots, - \\ \vdots \\ +, -, +,\dots, + \end{bmatrix} 和 \begin{bmatrix} -,+,-,\dots, - \\ +,-,+,\dots,+ \\ \vdots \\ -,+,-,\dots,- \end{bmatrix} \]

容易发现,这样构造能使得任意 \(r_i, c_j\) 不同号,直接差分约束即可。

3.生成树相关

3.1 最小生成树

由于 kruskal 和 prim 被讲烂了,只讲一讲 boruvka(不乳卡)

boruvka

考虑对于每一个点,求出与它相连的不在同一个连通块的边权最小的点连边。容易发现操作轮数不会超过 \(\log n\) 次。设对于一个点寻找与其相连边权最小的点的复杂度为 \(f(n)\),时间复杂度为 \(O(n f(n)\log n)\)

例题

[BJWC2010] 严格次小生成树

严格次小生成树显然和最小生成树只有一条边的差别。

枚举非树边 \((u,v)\),它能替换的边要么是最小生成树中 \(u \to v\) 的最大边,要么是严格次大边,倍增 / 树剖预处理即可。

[JSOI2008]最小生成树计数

此题引申到最小生成树的几个性质:

  • 所有最小生成树中,同一个权值的边数量相等。
  • 同一个权值的边加完后,所得的连通块个数相同。

本题中,由于相同权值的边不超过 10 个,直接枚举相同权值的边选的情况,判断是否满足上面条件即可。

[SHOI2010]最小生成树

将其它边减少 1 等价于这条边增加 1,考虑边权 \(\leq w\) 的所有边构成的连通块,在还未加入 \((u,v)\)\(u, v\) 一定不在同一个连通块。而让一条边删除的代价为 \(w- w_{now}+1\),等价于最小割。

直接跑网络流即可。

CF888G Xor-MST

发现普通的 kruksal 和 prim 根本没法做,考虑 boruvka。

异或问题考虑从高到低贪心,考虑从大到小的每一位 \(p\), 我们把数划分成两个集合,分别表示含有二进制 p 和不含。

容易发现,对于两个集合之间的连边最多会有一条,因为若大于 1 条显然可以在集合内部连边,边权显然 \(<2^p\)。 然后递归下去两个子问题即可。

递归深度不会超过 \(O(\log V)\) 级别。

把其中一个集合建 trie,现在要查询一个数 \(x\) 与集合中的 \(y\)\(\min(x \ \mathrm{xor} \ y)\),按位贪心即可。

【UOJ176】新年的繁荣

同样考虑 boruvka,只不过权值变成了 \(x \ \mathrm{and} \ y\)

对每一轮建出一棵大 trie。

在 trie 上进行游走时,若当前 \(x\)\(p\) 位为 1 且当前点存在右儿子就递归右儿子,否则递归左右儿子。

考虑先把右儿子合并到左儿子,这样就只用递归左儿子了。

查询一个子树内部是否存在一个不为 \(c\) 的点,可以记录当前子树最大 / 最下编号判断。

时间复杂度 \(O(n \log ^2n)\)

3.2 kruskal 重构树

kruskal 重构树用来解决树 / 图上路径的最值问题。

我们将 kruskal 的过程改写:把边从小到大排序后,若 \((u, v)\) 不连通,我们新建节点 \(p\), 将 $u,v $ 的父亲设为 \(p\),继续操作。这样子得到的一棵树叫做重构树。

一般情况下我们是对边建出的重构树,当然也可以对点进行操作,若要求 \(\leq d\), 就把边 \((u,v)\) 的权值设为 \(\max(w_u,w_v)\)\(\geq d\) 同理。

性质

  1. 一般情况下是是一棵二叉树。

  2. 原图的所有节点是重构树上的叶子节点。

  3. 对于节点 \(u\) 和其父亲节点 \(v\), 满足相同的偏序关系,即父节点的权值总不大于儿子的权值或总不小于儿子节点。因此 \(u \to v\) 上的边权最大 / 最小值为重构树上的 LCA 节点

    求解节点 \(x\) 在经过边权不超过 \(d\) 的边所能到达的连通块,就是在重构树上 \(u\) 的最浅祖先满足其权值 \(\geq d\) 的子树。

例题

[NOI2018] 归程

建出重构树后倍增,就变成了子树最小值。

[IOI2018] werewolf 狼人

人形态只能走 \(\geq L\) 的点权,狼形态只能走 \(\leq R\) 的点权,我们只需要分别对人和狼建出一棵重构树后倍增跳到对应节点后,问题变成了两段区间 \([L_1, R_1]\)\([L_2, R_2]\) 是否有交集。

将节点 \(p\) 在第一棵树上的位置 \(pos_i\) 作为下标按照第二棵树的顺序建出线段树,问题又变成 \([L_2, R_2]\) 中是否存在 \([L_1, R_1]\) 的数,主席树即可。

时间复杂度 \(O(n \log n)\)

BSOJ7688【10.03模拟】超级加倍

分别对最大最小建出重构树后,问题等价于在第一棵树上 \(x\)\(y\) 的祖先, 第二棵树上 \(y\)\(x\) 的祖先的 \((x,y)\) 的数量,树状数组统计。

3.3 笛卡尔树

笛卡尔树用来解决序列上的最值问题。

笛卡尔树是一棵二叉树,其每一个点有两个键值 \((k, w)\),其中 \(k\) 满足二叉搜索树(中序遍历为原数组)的性质,\(w\) 满足堆(父亲和儿子节点满足相同的偏序关系)的性质的二叉树。

通常对一个序列建笛卡尔树,就是将它的下标作为二叉搜索树的键值,权值 \(val\) 作为堆的键值。

性质

  1. 一定是棵二叉树,而且当点权互不相同时,笛卡尔树唯一。
  2. 笛卡尔树上的节点 \(x\) 的子树对应序列上连续的一段。
  3. 笛卡尔树本质上是一种不平衡的 treap,当权值随机时,其也具有 treap 深度为 \(\log n\) 的性质,因此可以用 treap 来维护笛卡尔树。

构建算法

假设我们要建大根堆的笛卡尔树,实时维护一个栈表示当前最靠右的链节点,其值单调递减。

加入到 \(x\) 时,不断弹出 \(\leq w_x\) 的节点,设最后一个弹出的节点为 \(lst\), 将 \(x\) 的左儿子设为 \(lst\)。设当前栈顶为 \(z\), 将 \(z\) 的右儿子设为 \(x\)。 加入 \(x\)

for(int i = 1; i <= n; ++i) {
    c[i] = read(); 
    while(tp && c[stk[tp]] <= c[i]) lc[i] = stk[tp --]; 
    if(tp) rc[stk[tp]] = i; stk[++tp] = i; 
}

应用

[HNOI2016]序列

对于一次询问 \([l,r]\), 我们找出其最小值位置 \(p\),那么经过 \(p\) 的最小值显然就是 \(a_p\),贡献为 \((p - l+1) \times (r - p+1) \times a_p\),再考虑不经过 \(p\) 的区间。

\(f_i\) 表示以 \(i\) 结尾的所有区间的最小值之和,转移考虑找到在单调栈的栈顶结点 \(j\),显然有 \(f_i = f_j+(i - j) \times a_i\)

对于 \((p,r]\) 这个区间,显然答案可以写成 $\sum\limits_{i = p+1}^{r}f_i - f_{p} $,记一个前缀和数组即可 \(O(1)\)

对于 \([l,p)\) 这个区间同理,我们只用维护后缀和即可。

时间复杂度 \(O(n \log n)\),瓶颈在于区间最值。

CF1117G Recursive Queries

显然,一个区间 \([l,r]\) 的答案为将 \([l,r]\) 中元素建笛卡尔树后,每个点的深度之和(假设根节点深度为 1)

将询问离线按照右端点排序,实时维护 \([1,r]\) 构成的笛卡尔树。

而区间 \([l,r]\) 的笛卡尔树的根就等于这条右链上第一个编号 \(\geq l\) 的点。它的子树就是 \([l,r]\) 的笛卡尔树的右儿子。

现在我们只需要实时维护每个点的深度即可,注意到若当前新插入 \(x\),当前栈顶为 \(p\),则 \(x\) 的左儿子区间为 \([p+1, x - 1]\), 将这些点的深度 + 1 即可,\(x\) 的深度就等于当前栈的元素个数。

当然这样只维护了右儿子,对于左儿子,将数组翻转后再做一次即可。

使用线段树维护区间加,时间复杂度 \(O(n \log n)\)

P5044 [IOI2018] meetings 会议

\(dp_{l,r}\) 表示将编号在 \([l,r]\) 的人聚集起来的最小花费,设 \([l,r]\) 的最大值位置在 \(p\),显然我们不可能在 \(p\) 点举行会议,这样显然不优,则有转移:

\(dp_{l,r} \leftarrow \min(dp_{l,p - 1}+(r - p+1) \times a_p, dp_{p+1,r}+(p - l+1) \times a_p)\)

由于 dp 涉及区间最值的位置,考虑建出笛卡尔树。设询问 \([l,r]\),我们只要求出 \(dp_{l, p - 1}\)\(dp_{p+1,r}\)\(\min\) 。按照上面一个题的套路,后者将整个序列翻转后再求解一遍,于是我们只考虑右儿子也就是 \(dp_{p+1,r}\) 的求解。

设当前在笛卡尔树的 \(p\) 点,设 \(p\) 管辖的范围为 \([l,r]\),实时维护 \(dp_{l, i}, i \in[l,r]\)

假设当前已经把左右儿子的 dp 值求出来了,考虑合并,观察上面的柿子,等价于 \([l,p - 1]\) 整体加上一个一次函数,\([p+1,r]\) 整体加后对一个一次函数取 \(\min\)

\(dp_{l,i}\)\(i\) 的增大单调不降,而且“斜率” 显然要比 \(a_p\) 小,我们可以证明 \([p+1,r]\) 和这个一次函数只有最多一个交点。

由于只有一个交点,被这个一次函数更新的一定是一段区间,于是直接用线段树维护区间加,区间对一个只有一个交点的一次函数取 \(\min\)

实现细节可以对每个区间 \([l,r]\) 保存其端点也就是 \(l\)\(r\) 的值,对一个一次函数取 \(\min\) 时当且仅当这个一次函数在 \(l, r\) 的取值都小于当前取值才更新。

时间复杂度 \(O(n \log n)\)

BS7037【11.18测试】随机排列

先建出笛卡尔树,考虑两个 \(i, j(p_i > p_j)\)在什么条件下会连边。

稍加分析发现,一个点 \(x\) 会向它的左儿子以及左儿子的所有右链,右儿子的所有左链连边。

考虑 dp,设 \(dp_{x,0/1}, dpl_x, dpr_x, dplr_x\) 分别表示笛卡尔树上 \(x\) 的子树内,使得 除了 \(x\)\(x\) 的左链,\(x\) 的右链,\(x\) 的左链以及右链其它点都被覆盖最小花费,转移是简单的。

对于修改操作,由于 排列随机,笛卡尔树的期望树高为 \(O(n \log n)\),直接用 treap 维护 dp 的过程,交换就把相邻的点 split 再交换左右儿子即可。

时间复杂度 \(O(n \log n)\)

3.4 树分治

静态点分治

树分治用来处理所有路径的信息的问题。

类似序列上的分治找中点,树的中点是什么呢?就是树的重心。

点分治的具体流程如下:找到当前连通块的重心 \(p\)(可以一次树形 dp 求得),统计在这个连通块内经过重心路径的信息。然后将重心删除,对于剩下的几棵子树递归上面的过程。

由于删去重心后剩下的所有连通块大小 \(S'\) 不会超过当前大小 \(\dfrac S 2\),因此分治树的层数最多为 \(\log n\), 设统计经过根的信息的复杂的为 \(f(n)\),总时间复杂度为 \(O(n f(n) \log n)\)

当强制经过某一点的路径信息不好求得时,若维护信息具有可减性,利用容斥的思想,可以先计算这个连通块内所有点对路径的答案,在对删去根后的所有儿子 \(y\), 减去 \(y\) 连通块的答案。此时求得的就是强制经过根节点的答案。

例题(静态点分治)

[IOI2011]Race

\(f_n\) 表示权值为 \(n\) 时最小的边,在统计一棵子树时直接预处理子树中每个点到根的权值和经过的边数,然后更新答案,然后更新 dp 值。

[BJOI2017] 树的难题

预处理每个点到分治中心对应子树的颜色权值。在拼接的时候分颜色是否相同考虑,如果相同还要而外减去对应颜色的权值。

考虑开两棵线段树分别维护与当前点同色 / 不同色的权值。把分治中心的所有儿子按照颜色排序后,相同的颜色对应相同的区间。若为两个颜色的交界处直接把同色的信息合并到异色。这个操作可以用线段树合并来完成。

查询时就是区间 \(\max\),可以直接维护。

时间复杂度 \(O(n \log^2n)\)

[WC2010]重建计划

首先分数规划,问题变成找一条经过边在 \([L,R]\) 之内的边权 \(\geq 0\) 的路径,我们只保存最大的即可。

依然是先预处理,可以直接套上线段树区间查询,但是时间复杂度变成了 \(O(n \log^3 n)\)

考虑优化区间查询这一过程,将待查询的深度排序后,随着深度的变化,此时呈现了一个滑动窗口的过程,使用单调队列优化。

将使所有儿子的最大深度从小到大排序后加入,时间复杂度 \(O(n \log^2 n)\)

[CTSC2018]暴力写挂

将答案式乘 2 变成:\(\mathrm{dis}(x,y)+\mathrm{dep}_1(x)+\mathrm{dep}_1(y) - 2\mathrm{dep}_2(\mathrm{lca}(x,y))\)

考虑点分治,设一个点 \(x\) 的权值为 \(w(x) = \mathrm{dep}(x)+\mathrm{len}(x)\), 其中 \(\mathrm{len}(x)\) 表示 \(x\) 到分治重心的距离。

因为答案还和 \(x,y\) 在第二棵树上的 lca 有关,考虑把分治涉及到的这些点建出虚树,枚举 \(\mathrm{lca}\), 答案就是不在同一个子树内(第一棵树上),而且在 lca 的不同子树内(第二棵树上)的最大值,对于虚树上的点维护最大值和不为最大值颜色的次大值即可。

时间复杂度 \(O(n \log^2 n)\), 瓶颈在于对虚树上的点的 dfn 排序。

[WC2018]通道

现在两棵树变成三棵树了,但是保证边权非负。

我们将对第一棵树进行点分治改为边分治,保证只有两种不同的颜色方便合并。

仍然对第二棵树建出虚树,问题就在于如何维护第三棵树上的点对距离。

我们设一个点对 \((x,y)\) 的“距离” 为 \(val_x +val_y + \mathrm{dis}_3(x,y)\),其中 \(val_x = \mathrm{dep}_1(x)+\mathrm{len}(x)+\mathrm{dep}_2(x)\), 数组的定义和上面的题一样。对于每一个 lca,我们要求不同子树内的 “距离”最大值。

你可能很奇怪为什么我要定义一个距离而不是权值,实际上,由于边权非负,直径的信息是可以合并的。

具体地,设 \(S_1,S_2\) 为两个点集,设它们直径的端点分别为 \((a, b)\)\((c,d)\), 则 \(S_1 \cup S_2\) 的直径只可能是 \(a,b, c,d\) 任意组合后得到的最大值。

查询时就分别把黑色点和白色点的直径端点两个两两拼接取 \(\max\) 即可。

时间复杂度 \(O(n \log^2 n)\)

4.无向图连通性

定义

  • 割点:删去后使得连通分量增加的点。
  • 割边:删去后使得连通分量增加的边。
  • 点双连通图:不存在割点的无向连通图。
  • 边双连通图:不存在割边的无向连通图。
  • 返祖边:在 dfs 树中,一条非树边由儿子连向祖先的边。
  • 前向边:在 dfs 树中,一条非树边由祖先连向儿子的边。
  • 横叉边:仅在有向图中有定义,在 dfs 树中,连接的两个点不互为祖先关系的边。
  • \(\mathrm{dfn}_x\) 表示在 dfs 所得到的生成树中,\(x\) 的位置,\(\mathrm{low}_x\)表示 \(x\) 经过一条返祖边后,所能达到的最小的 \(\mathrm{dfn}\) 值。

Tarjan 算法

用于 \(O(n)\) 求出割边和割点。

算法流程:任选一个点作为根开始 dfs,假设当前在 \(x\) 节点,初始化 \(\mathrm{dfn}_x = \mathrm{low}_x = \mathrm{sign}\),表示当前能回溯的点只有 \(x\)

遍历所有 \((x,y)\),若 \(y\) 未被访问,则该边为树边,访问 \(y\) 并更新 \(\mathrm{low}_x \leftarrow \min(\mathrm{low}_x, \mathrm{low}_y)\);否则该边为返祖边或前向边,若为返祖边,更新 \(\mathrm{low}_x \leftarrow \min(\mathrm{low}_x, \mathrm{dfn_y})\)

求完两个数组后,我们有如下定理:

  • 割边判定法则:一条树边 \((x, y)\) 为割边当且仅当 \(\mathrm{low}_y > \mathrm{dfn}_x\)
  • 割点判定法则:一个点 \(x\) 为割点当且仅当 \(x = root \and deg_x > 1\) 或 $x \neq root $, 存在树边 \((x, y)\) 使得 \(\mathrm{low}_y \geq \mathrm{dfn}_x\)

代码如下:

    inline void Tarjan(int x, int fa) {
	    dfn[x] = low[x] = ++sign; int deg = 0; 
	    for(int &y : adj[x]) {
	    	if(y == fa) continue ; 
	    	if(!dfn[y]) {
	    		Tarjan(y, x), low[x] = std :: min(low[x], low[y]); 
	    		if(low[y] >= dfn[x] && fa) mark[x] = 1; // 割点
                 if(low[y] > dfn[x]) mark2[(x, y)] = 1 // 割边
			    deg ++;   
			}
			else low[x] = std :: min(low[x], dfn[y]); 
		}
		if(deg > 1 && (!fa)) mark[x] = 1;  // 特判根节点
	}

边双连通分量:

可以直接求出割边过后对每个没有割边的连通块 dfs,或者也可这样:

inline void upd(int x) {
		++tot; int y; 
		do {
			y = stk[tp --], ans[tot].push_back(y); 
		} while(y ^ x); 
	}
	inline void Tarjan(int x, int fa) {
	    dfn[x] = low[x] = ++sign, stk[++tp] = x;
	    for(auto &z : adj[x]) {
	    	int y = z.first, r = z.second; 
	    	if(r == fa) continue ; 
	    	if(!dfn[y]) {
	    		Tarjan(y, r), low[x] = std :: min(low[x], low[y]); 
	    		if(low[y] > dfn[x]) upd(y); 
			}
			else low[x] = std :: min(low[x], dfn[y]); 
		}
	    if(!fa) upd(x); 
	}

点双连通分量:

实时维护一个栈,考虑每条边 \((x,y)\),当 \(\mathrm{low}_y \geq \mathrm{dfn}_x\) 时,就把所有 \(y\) 的子树中还未出栈的点出栈。

CF51F Caterpillar

5.有向图连通性

定义

  • 定义强连通分量为对于任意 \(u, v\)\(u \to v\)\(v \to u\)

考虑每一条非树边 \((u, v)\) 的状态:

  • \((u, v)\) 为前向边,在删去后 \(u\) 仍然能到达 \(v\), 没有影响。

  • \((u,v)\) 为返祖边,它会使 \(u \to v\) 的所有点都形成一个强连通分量,多个返祖边组合能形成更大的强连通分量。

  • \((u,v)\) 为横叉边,此时必有 \(\mathrm{dfn}_u > \mathrm{dfn}_v\),此时 \(v \to u\) 的充要条件为 \(v \to \mathrm{lca}_{u,v}\)

    证明:若 \(v\) 不经过 \(\mathrm{lca}\) 而直接到达 \(\mathrm{lca} \to u\) 的子树,设为 \(x\),则 \((v, x)\) 为一条横叉边,则有 \(\mathrm{dfn}_x < \mathrm{dfn}_v\), 而 \(\mathrm{dfn}_v < \mathrm{dfn}_u\) ,因此 \(u\) 子树内的所有点的 \(\mathrm{dfn}\) 都比 \(v\) 大,显然与上面不符。所以 \(v\) 只能先到 \(\mathrm{lca}\) 后再到达 \(v\)

因此,有向图的 \(\mathrm{low}\) 数组应该定义为所有 \(u\) 子树内的返祖边和前向边的指向的 \(\mathrm{dfn}\) 的最小值。

算法流程:实时维护一个栈表示当前还没有在 SCC 中的点,对于一个 SCC, 我们在最浅点统计它。在更新 \(\mathrm{low}\) 值时,前向边不会造成影响可以忽略,否则若是返祖边直接更新;若是横叉边 \((u,v)\),当 \(v\) 已经在 SCC 中时不能用它来更新,否则可以更新。

代码:

inline void Tarjan(int x) {
	dfn[x] = low[x] = ++sign, in[stk[++tp] = x] = 1; 
	for(int &y : G[x]) {
		if(!dfn[y]) Tarjan(y), low[x] = std :: min(low[x], low[y]); 
		else if(in[y]) low[x] = std :: min(low[x], dfn[y]); 
	}
	if(low[x] == dfn[x]) {
		++cnt; int y; do {
			y = stk[tp --], in[y] = 0, bl[y] = cnt, b[cnt] += a[y]; 
		} while(y ^ x); 
	}
}

应用:SCC 缩点后会形成一个 DAG,通常和拓扑排序结合在一起使用,对于某些 ”点只能经过一次“ 的问题可以方便地解决。

例题

[USACO15JAN]Grass Cownoisseur G

因为每个草场只能算一次贡献,缩点后建出分层图跑最短路。

[POI2006]PRO-Professor Szu

最后肯定能通过从 \(n + 1\) 节点反向拓扑排序 dp 得到答案,考虑什么时候会有不合法情况:

  1. 出现自环,输入时判掉即可。
  2. 为一个大小 \(\geq 2\) 的强连通分量,且从 \(n+1\) 出发能够遍历到。

通过 tarjan 缩强连通分量后直接做即可。

[ARC092D] Two Faced Edges

先考虑对原图缩点,分别考虑连接 \((u,v)\) 为同一个分量的边和不同分量的边。

对于后者,若将 \((u,v)\) 反向后个数改变,则说明在删去 \((u,v)\)\(u \to v\) 至少还有一条路径。

对于前者,若将 \((u,v)\) 反向后个数改变,则说明在删去 \((u,v)\) 后没有 \(u \to v\) 的路径。二者判定条件相反,只需最后判断一下即可。

现在我们要解决的是删去一条边后的连边情况,我们可以用一段前缀和后缀拼接而得,直接对于每一个点 dfs 。

[HNOI2012]矿场搭建

6.网络流

定义

有向图 \((V,E)\),存在源点 \(S \in V\)汇点 \(T \in V\)。对于每条边,有容量限制 \(c\) 和实际流量 \(f\),满足 \(0 \leq f \leq c\) 。 除 \(S, T\) 以外的点都满足 \(\sum\limits_{(v, u) \in E} f_{v, u} = \sum\limits_{(u, v) \in E}f_{u, v}\) ,即流量平衡。此时 \(\sum\limits_{(S, u) \in E} f_{S, u} = \sum\limits_{(u, T) \in E} f_{u, T}\) ,称为流量

定义残量网络为将每条边的容量减去流量后得到的网络,一般忽略容量 = 流量的边。

定义增广路 \(P\) 为残量网络上一条从 \(S\)\(T\) 的路径。

定义为将 \(V\) 划分成互不相交的两个点集 \(A, B\),其容量为 \(\sum\limits_{u \in A}\sum\limits_{v \in B}f_{u, v}\)

最大流的求解

常见算法有 EK,Dinic,SAP,ISAP。由于网上资料很多,故不一一介绍。

为了方便,这里放上 Dinic 算法模板:

	struct Net {
		int head[M], nxt[N], head2[M], dis[M], to[N], w[N]; 
		bool vis[M]; 
		int cnt, S, T; 
		inline void add_e(int x, int y, int z) {
			to[++cnt] = y, nxt[cnt] = head[x], w[cnt] = z, head[x] = cnt; 
		}
		inline void add(int x, int y, int z) {
			add_e(x, y, z), add_e(y, x, 0); 
		}
		inline void init() {
			memset(head, 0, sizeof(head)), cnt = 1; 
		} 
		inline void set(int s, int t) {
			S = s, T = t; 
		}
		inline bool bfs() {
			std :: queue < int > q; memset(vis, 0, sizeof(vis)), memset(dis, 0, sizeof(dis)), vis[S] = 1, q.push(S); 
			while(!q.empty()) {
				int x = q.front(); q.pop(); head2[x] = head[x]; 
				for(int i = head[x], y = to[i]; i; i = nxt[i], y = to[i]) 
				    if(!vis[y] && w[i] > 0) dis[y] = dis[x] + 1, vis[y] = 1, q.push(y); 
			}
			return vis[T]; 
		}
		inline int dfs(int x, int flow) {
			if(x == T) return flow; int s = 0; 
			for(int i = head2[x], y = to[i]; i; i = nxt[i], y = to[i]) {
				head2[x] = i; if(dis[y] != dis[x] + 1 || !w[i]) continue ; 
				int dlt = dfs(y, std :: min(flow, w[i])); s += dlt, flow -= dlt; 
				w[i] -= dlt, w[i ^ 1] += dlt; 
				if(!dlt) dis[y] = -1; if(!flow) return s; 
			}
			return s; 
		}
		inline int mxmf() {
			int res = 0; 
			while(bfs()) {
				res += dfs(S, INF); 
				if(res >= INF) return -1; 
			}
			return res; 
		}
	} net ; 

最大流最小割定理

内容很简单:最大流 = 最小割

证明:显然最大流不大于最小割。考虑一个最大流,设从 \(S\) 可到达的点集为 \(A\)\(T\) 反向到达的点集为 \(B\)。由于不存在增广路显然, \(A \cap B = \emptyset, A \cup B = \{1, 2, \dots, n\}\) ,所以跨过 \(A,B\) 的边必然满流,而这就是一种合法的割。

费用流

每条边额外存在单位费用 \(cost\),若该边流量为 \(f\) 则代价为 \(f \times cost\),一个网络的费用等于每条边的代价之和。

费用流的求解

贪心,每次找 \(S \to T\) 总单位费用最小的可行流进行增广,使用 spfa 即可。 在增广时,既可以类似 EK 一条一条地回流,也可以类似 Dinic 一样 dfs,具体实现见代码。

使用 spfa 的费用流称为 SSP。下面为最小费用最大流代码:

	struct Net {
		int head[M], head2[M], w[N], cost[N], to[N], nxt[N]; 
		int cnt, S, T; 
		ll dis[M]; 
		bool vis[M]; 
		inline void add_e(int u, int v, int w1, int w2) {
			to[++cnt] = v, nxt[cnt] = head[u], w[cnt] = w1, cost[cnt] = w2, head[u] = cnt; 
		}
		inline void add(int u, int v, int w, int cost) {
		 	add_e(u, v, w, cost), add_e(v, u, 0, -cost); 
		}
		std :: queue < int > q; 
		inline void init() {
			memset(head, 0, sizeof(head)), cnt = 1; 
		}
		inline void set(int s, int t) {
			S = s, T = t; 
		}
		inline bool spfa() {
			memset(vis, 0, sizeof(vis)); 
			for(int i = 0; i <= 2 * n + 1; ++i) dis[i] = INF; 
			dis[S] = 0, q.push(S); 
			while(!q.empty()) {
				int x = q.front(); q.pop(); head2[x] = head[x]; vis[x] = 0; 
				for(int i = head[x], y = to[i]; i; i = nxt[i], y = to[i]) 
				    if(dis[y] > dis[x] + cost[i] && w[i]) dis[y] = dis[x] + cost[i], (!vis[y]) && (vis[y] = 1, q.push(y), 1);  
			}
			return dis[T] < INF; 
		}
		inline int dfs(int x, int flow) {
			if(x == T) return flow; int s = 0; vis[x] = 1; 
			for(int i = head2[x], y = to[i]; i; i = nxt[i], y = to[i]) {
				head2[x] = i; if(dis[y] == dis[x] + cost[i] && w[i] && !vis[y]) {
					int dlt = dfs(y, std :: min(flow, w[i])); w[i] -= dlt, w[i ^ 1] += dlt; 
					s += dlt, flow -= dlt; if(!dlt) dis[y] = -1; if(!flow) break;  
				}
			}
			return s; 
		}
		inline void mcmf(int &s1, ll &s2) {
			while(spfa()) {
				int v = dfs(S, SNF); 
				s1 += v, s2 += (ll)v * dis[T]; 
			}
		}
	} net; 

有一些屑题,卡 spfa,只能用 dijkstra,而边权又有负的,那怎么办呢?

使用 dijkstra 的费用流称为 Primal-Dual,即原始对偶。

其算法与 Johnson 全源最短路类似。给每个边附一个势能 \(h_i\),满足 \(\forall (u, v,w) \in E\)\(h_u+w \geq h_v\),即三角不等式,再将边权设为 \(w+h_u - h_v\),这样每条边边权都非负了。

找到增广路后会改变残量网络的形态,将 \(h_u \leftarrow h_u+dis_u\),其中 \(dis_u\) 为每次 dijkstra 跑出来的 \(dis_i\)

正确性证明:若 \((u, v, w)\) 在增广路上,则 \(dis_u+w+h_u - h_v = dis_v\),即 \((dis_u+h_u) + w \geq (dis_v+h_v)\)。 若不在,就将上面的等于号换为大于号即可。

代码【最小费用最大流】:

	struct Net {
		int head[N], to[N], nxt[N], w[N], cost[N]; 
		int cnt, S, T; 
		inline void init() {
			memset(head, 0, sizeof(head)), cnt = 1; 
		}
		inline void set(int s, int t) {
			S = s, T = t; 
		}
		inline void add_e(int x, int y, int z, int cst) {
			to[++cnt] = y, nxt[cnt] = head[x], w[cnt] = z, cost[cnt] = cst, head[x] = cnt; 
		} 
		inline void add(int x, int y, int z, int cst) {
			add_e(x, y, z, cst), add_e(y, x, 0, -cst); 
		}
		std :: priority_queue < pii > pq; 
		int head2[N], flow[N]; 
		ll dis[N], h[N]; bool vis[N]; 
		inline bool dijkstra() {
			for(int i = 0; i <= n; ++i) dis[i] = INF; memset(vis, 0, sizeof(vis)); pq.push(pii(dis[S] = 0, S)), flow[S] = 1e9; 
			while(!pq.empty()) {
				int x = pq.top().se; pq.pop(); if(vis[x]) continue ; vis[x] = 1; head2[x] = head[x]; 
				for(int i = head[x], y = to[i]; i; i = nxt[i], y = to[i]) {
					ll we = cost[i] + h[x] - h[y]; if(dis[y] <= dis[x] + we || vis[y] || (!w[i])) continue ; 
					dis[y] = dis[x] + we, flow[y] = std :: min(flow[x], w[i]); if(!vis[y]) pq.push(pii(-dis[y], y)); 
				}
			}
			memset(vis, 0, sizeof(vis)); 
			return dis[T] < INF; 
		}
		inline int dfs(int x, int flow) {
			if(x == T) return flow; vis[x] = 1; int s = 0; 
			for(int i = head2[x], y = to[i]; i; i = nxt[i], y = to[i]) {
				head2[x] = i; if(dis[y] != dis[x] + cost[i] + h[x] - h[y] || vis[y] || (!w[i])) continue ; 
			    int dlt = dfs(y, std :: min(flow, w[i])); w[i] -= dlt, w[i ^ 1] += dlt; 
			    flow -= dlt, s += dlt; if(!dlt) vis[y] = 1; if(!flow) return s; 
			}
			return s; 
		}
		inline void mcmf() {
			ll ans1 = 0, ans2 = 0; 
			while(dijkstra()) {
				int f = dfs(S, INT_MAX); 
				ans1 += f, ans2 += (ll)f * (dis[T] - h[S] + h[T]);
				for(int i = 0; i <= n; ++i) h[i] += dis[i]; 
			}
			cout << ans1 << ' ' << ans2 << '\n'; 
		}
	} net; 

上下界网络流

此部分主要靠背诵。

如果有源汇,新建边 \((T,S, \infty)\),转化为无源汇。

上下界:若一条边 \((u, v, l, r)\),则强制让 \(l\) 先流了,每个点维护 \(d_u = \sum\limits_{(v, u) \in E}e_{v, u} - \sum\limits_{(u, v) \in E}e_{u, v}\)。新建源点汇点 \(S', T'\),若 \(d_u > 0\),连边 \((S', u, d_u)\),否则 \((u, T', -d_u)\)

最小流:首先得满足 \(\sum\limits_{d_u > 0}d_u = \mathrm{mxmf}\),然后将 \((T, S )\) 边断掉,重新设置 \(S' \leftarrow T, T' \leftarrow S\),再跑最大流,二者之差就是答案。

最大流(有源汇):先跑 \(S' \to T'\) 的最大流,再跑 \(S \to T\) 的最大流。

最小费用可行流:不断增广,直到最短路长度为正。

费用流边的处理:若边 \((u, v, l, r, w)\),则先让答案累加 \(l \times w\),然后和无费用的基本相同。

7.网络流建模

现在你已经对网络流的基本原理有了一定了解,就让我们来看一看下面这些简单的例子,把我们刚刚学到的知识运用到实践中吧。

平面图最小割

平面图最小割等于对偶图最短路。

[ICPC-Beijing 2006] 狼抓兔子

我愿称之为典中典中典。

将每个平面看做一个点,若两个平面相邻,边权为将它们分隔开的边权。左上和右下分别为两个特殊的平面。

[NOI2010] 海拔

调整法可知一定存在一个割满足割左上的都是 0,右下的都是 1,因此答案为最小割,套用 1 中算法即可。

[CSP-S 2021] 交通规划

见 最短路 部分例题 3。

最大权闭合子图

可以概括为 \(n\) 个物品,有权值 \(v_i\) 可正可负,满足若干个要求,每个要求形如选了 \(x\) 就要选 \(y\),最大化选的总和。

建图:源点 \(S\) 和所有权值为正的连边,所有权值为负的与 \(T\) 连边,若有限制 \((x,y)\) 就连边 \((x, y, \infty)\)。所有正的总和减去最大流就是答案。

输出方案:先得到点集 \(A,B\) 分别为 \(S,T\) 可达点,由于被割边一定不是 \(\infty\),因此若正的被割了,表示正的没有选;负的被割了,表示负的被选了。

P2762 太空飞行计划问题

模板。

[CEOI2008] order

可以租,就把 \((x, y)\) 的边权换为租的费用。一个工作能完成当且仅当所有相关的工序都完成了。

[NOI2009] 植物大战僵尸

直接建图会存在环,需要 tarjan / 拓扑排序把环扣出来再跑。

CF1082G Petya and Graph

一条边被选,则相邻两个点被选。

集合划分模型

可抽象为 \(n\) 个 01 变量 \(x_i\),最经典的为最小化:

\[\min_{x_1,x_2,\dots,x_n}(\sum\limits_{i}a_ix_i+b_i\overline{x_i})+(\sum\limits_{(u, v)}c_{u,v}x_u\overline{x_v}) \]

建图:源点 \(S\)\(i\) 连边,表示 0;\(i\) 和汇点 \(T\) 连边,表示 1,权值分别为选择 0 / 1 的代价。若限制 \((u, v)\) 则双向边 \((u, v, c_{u, v})\),将所有收益加上再减去最小割就是答案。

输出方案:对于每个点而言,割去哪个点,就表示选择了另外一个点。

拓展

  • 若要求 \(u\)\(A\)\(v\)\(B\) 有代价 \(w\),则把双向边改为单向边即可。
  • 若要求 \(u, v\) 在相同集合有代价,则需要黑白染色后再处理。
[国家集训队]happiness

条件取反,若 \(u, v\) 不在同一个集合里面就需要付出 \(w\)。 若限制 \((u, v, w_0)\),则新建点 \(p\),连边 \((S, p, w_0)\)\((p, u, \infty)\)\((p, v, \infty)\)\((u, v, w_1)\) 同理。

P1361 小M的作物

同理,只不过把连两个点变成连多个点即可。

[国家集训队]圈地计划

要求是同一个集合了,先黑白染色,若 \(u\) 为白色则连边 \((S, u, w_0), (u, T,w_1)\);否则连边 \((S, u, w_1),(u, T, w_0)\)

若限制 \((u, v, w)\),连双向边 \((u, v, w)\)

[THUPC2022 初赛] 分组作业

一样的套路。

[BJOI2016]水晶

一个三维坐标 \((x, y, z)\) 与二维坐标 \((x - z, y - z)\) 对应。将图按照 \(\bmod 3\) 染色后,两种共振都等价于一对 \((0, 1, 2)\) 共存,以 0 作为中转点,1 作为左部点, 2 作为右部点,把 0 拆点设为 \((k, k', w)\)\(S \to i\) 连 1 颜色权值,\(i \to k\) 连 inf,\(k' \to j\) 连 inf,\(j \to T\) 连 2 颜色权值。最小割即是答案。

离散变量模型

每个变量有一些取值 \(x_i = \{1, \dots, m\}\),选择不同取值有不同代价,不同变量之间的取值也有一些代价,一般和二者的差有关。

建图:每个点拆成 \(m+1\) 个点,S 和 \((x_i, 1)\) 相连,\((x_i, m+1)\) 和 T 相连,\((x_i, j)\)\((x_i, j+1)\) 边权为 \(x_i = j\) 的代价。若要求 \(x_i - x_j \leq D\),则对每个 \(x \in [D + 1, m]\)\((i, x)\)\((j, x - D)\) 连 inf 边,表示若让 \(x_i = x\),则 \(x_j\) 的取值必须大于 \(x - D\)。 对于 \(x_i - x_j \geq D\) 同理。

输出方案:同集合划分模型。

[HNOI2013]切糕

每个 \((i, j)\) 拆成 \(R\) 个点,然后相邻 \(|i - i'|+|j - j'| = 1\) 的按照上面的方式连边。

P6054 [RC-02] 开门大吉

计算出第 \(i\) 个人做第 \(j\) 套题的期望收益 \(f_{i,j}\),拆点后每个 \((i, x)\) 连边 \((j, x+k)\),权值 inf。

但会出现无解情况,此时可能存在一个点被割了两条边的非法情况 ,需要特殊处理,也可以在相邻两个点额外加边 \((i, x) \to (i, x- 1)\),边权为 inf。

流量分配和匹配模型

包括:流量分配,二分图匹配,路径覆盖,动态加点,下凸函数费用。

[SCOI2012]奇怪的游戏

\(n, m\) 都是奇数,根据 \(x \times cnt_0 - sum_0 = x \times cnt_1 - sum_1\) 可直接得出 \(x\),然后直接判断。

否则答案存在可二分性,二分答案 \(x\),黑白染色后,将一个黑点与相邻白点连边,边权为 inf,源点向每个黑点连边 \(x-a_{i,j}\),对于每个白点同理。若最后满流则有解。

[SDOI2010]星际竞速

即要么一个点是直接花费 \(A_i\) 的代价到达,要么由一个编号比它小的点到达。于是拆点,边 \((u, v, w)\) 连边 \((u, v', \infty, w)\)\((S, u, 1, 0), (u', T, 1, 0)\)\((S, u',1, A_i)\)。然后最小费用最大流。

CF277E Binary Tree on Plane

实际上二叉树这个限制没什么用。

由于形成一颗树,除了根每个点入度都是 1,出度不超过 2。然后随便连边跑最小费用最大流就行了。

[SCOI2007] 修车

费用提前计算,若第 \(i\) 辆车是倒数第 \(t\) 时刻被 \(j\) 修的,代价就是 \(T_{i,j} \times t\),由于每个人每个时刻只能修一辆车。于是把 1 个人拆成 \(n\) 个点即可。

[NOI2012] 美食节

和上面的一模一样,只是数据范围大了亿点,考虑证怎么减少点边数。

容易发现,第 \(i\) 个人一定是依次在 \(1, 2, \dots, k\) 时间修车,于是最初可以只建 \((i, 1)\) 的点。每次增广后找到增广点对它新建 \(k+1\) 时刻的点即可。

于是点数就优化到了线性。

[WC2007]剪刀石头布

若一个 \((i, j, k)\) 构不成三元环,则一定存在一个点,使得其有两个出度。故一个完全图的三元环个数为 \(\dfrac{n(n - 1)(n - 2)}6 - \sum\limits_{deg_i} \dbinom i 2\)

化一下柿子可以得到答案只和 \(\sum_i deg_i^2\) 有关。于是考虑增量法,把每个未定向边拆点 \(p\),连边 \((S, p, 1, 0)\)\(p\) 向两个端点连边,\((p, i, 1, 0)\)\((p, j, 1, 0)\)。每个 \(i\) 再分别向 \(T\) 连边 \((i, T, 1, 2 \times j - 1)\) ,其中 \(j\)\(i\)\(j - 1\) 变到 \(j\) 的花费。由于是凸函数,于是一定会从小到大增广。输出答案就很简单了。

【CHINA-FINAL 16】Mr.Panda and TubeMaster
【BZOJ3961】 [WF2011]Chips Challenge

路径覆盖问题

一个 DAG,选出最少不相交路径,使得覆盖所有点。

建图:拆点构造二分图,若原图存在 \((x, y)\) 则加边 \((x, y')\),则一个 \(x \to y', y \to z', z\to \dots\) 的匹配描述了一条路径。

输出方案:一个总匹配数为 \(k\) 的方案描述了一个个数为 \(n - k\) 的路径匹配方案,因此 \(n\) 减去最大匹配数就是最小不相交路径覆盖。每次从未访问过的左部点开始寻找即可。

拓展:DAG 可相交路径覆盖:求出 \(G\) 的传递闭包 \(G'\),在 $G' $ 中求解不相交路径覆盖。容易证明 \(G'\) 的一组不交路径覆盖对应 \(G'\) 若干个可交路径覆盖。

LGP6061 [加油武汉]疫情调查

问题相当于将 \(n\) 个点分成若干个环,每个环的费用是连接相邻两点的边权,若是单点则是 \(a_i\)

因此可以直接先求出任意两点之间最短路,然后建立二分图,\(u \to u'\)\(a_u\) 的边,若 \(dis_{u, v} = w\) 则在 \((u, v, w)\),最后完美匹配就是答案。

但此时边数达到了 \(O(n^2)\) ,考虑新建边 \((u', u, \infty)\),这样从一个点 \(v\) 出发到达了 \(u'\) 还可以继续走最短路到达其他点,于是就可以替代 \(n^2\) 条边。

[ZJOI2011]营救皮卡丘

容易发现,题目相当于选出不超过 \(K\) 条可相交路径,且每条路径的末端点都是编号最大的点的最小花费路径覆盖。

考虑建图,仍然是 floyd \(n^3\) 求出任意两点在约束条件下的最短路,由于可以走 \(K\) 次一号点,所以连边 \((S, 1, K, 0)\),其他点连边 \((S, u, 1, 0)\)\((u', T, 1, 0)\)\(dis_{u, v} = w\) 连边 \((u, v', w)\)。然后最小费用最大流就是答案。

LGP4553 80人环游世界

国家拆点,然后跑有源汇上下界最小费用最大流。

上下网络流模型

给定 \([1,m]\)\(n\) 个区间 \([l_i, r_i]\),每个区间最多选择 \(c_i\) 次,代价为 \(w_i\)。每个点 \(i\) 被区间覆盖的次数要求在 \([a_i, b_i]\) 中间,最小化花费。

建图:将 \(1\)\(m+1\) 连成一条链,源点向 \(1\) 连一个足够大的数 \(X\),汇点同理。一个区间 \([l, r, c, w]\),连边 \((l, r+1, c, w)\),每个点的覆盖次数用 \(X\) 减去 \(i \to i+1\) 的流量 \(c\) 来表示,因此连边 \((i, i + 1, [X - b_i, X - a_i], 0)\),最小费用上下界最大流就是答案。

输出方案:显然,\(i \to i+1\) 流了 \(c\) ,那么 \(i\) 就被覆盖了 \(X - c\) 次,区间选择的次数就是流过的次数。

P3358 最长k可重区间集问题

\(X = m\),就没有下界了,直接连边 \([l, r, 1, r - l]\) 即可。

【AHOI2014】支线剧情

问题即是 DAG 最小边覆盖,等价于每条边至少经过 1 次,直接跑有源汇上下界最小费用可行流。

CF708D Incorrect Flow

对每条边讨论:

  • \(f \leq c\),连边 \([u, v, [f,f], 0], [u, v, c - f, 1], [v, u, f, 1], [u, v, \infty, 2]\),表示初始 \(f\) 流量,增加 \(f\) 流量不超过 \(c - f\),减少 \(f\) 流量不超过初始 \(f\) (显然不可能同时加和减),将 \(c\)\(f\) 同时 + 1。
  • \(f > c\),直接将答案加上 \(f - c\),然后连边 \([u, v, [f,f], 0],[v, u, f - c, 0],[u, v, \infty, 2],[v, u, c, 1]\) 表示初始 \(f\) 流量,减少单位流量或扩大单位容量,同时 + 1,减少单位流量。

然后跑有源汇上下界最小费用可行流。

[NOI2008] 志愿者招募

\(X = \max a_i\),就没有下界了,直接套模板。

[NEERC2016]Delight for a Cat

考虑经典调整法,先让猫全部睡觉,对于每条边 \(i \to i+1\),设其表示的是 \([i - k+1, i](i \geq k)\) 的把睡觉变成进食的次数 \(c_i\),则需要满足 \(m_e \leq c_i \leq k - m_s\)。再将 \(i\) 时刻修改影响的区间 \([i, \min(n, i+k-1)]\) 当作一个区间,令 \(X = k - m_s\),就没有下界了。

8. 二分图

定义

  • 二分图:设无向图 \(G = (V, E)\) 若能将 \(V\) 划分成两个点集,使得两个点集之间没有边,那么称 \(G\) 为一个二分图

  • 图的匹配: 选出一些边,使得每两条边之间不存在公共点。

  • 图的独立集:选出一些点,使得每两个点之间不存在边相连。

  • 图的点覆盖:选出一些点,使得每条边至少有一个端点被选。

  • 图的边覆盖:选出一些边,使得每个点都至少被一条边覆盖。

  • 图的团:选出一些点,使得每对点都有边相连。

二分图判定定理:图 \(G\) 是二分图的充要条件为不存在奇环

二分图匹配

用 Dinic,时间复杂度 \(O(n\sqrt n)\),具体原因未知。匈牙利 \(O(n^2)\)

二分图最小点覆盖:\(S\) 连向左部点, 右部点连向 \(T\),一条边 \((u, v)\),从左部点 \(u\) 连向 \(v\),流量 inf,则最小割就是答案,又因为这张图的最大流就是最大匹配。因此最小点覆盖等于最大匹配

二分图最大独立集:同样是上面的建边,一条边至少要一个点不选,于是总点数减去最大匹配数等于最大独立集

二分图最小边覆盖:等于最大独立集

最大团:等于补图的最大独立集

Hall 定理:设 \(N(S)\) 表示 \(S\) 所连向的点集,若一个二分图存在完美匹配 $\Leftrightarrow $ \(\forall S \in [n]\)\(|N(S)| \geq |S|\)

证明:

必要性:若存在 \(S\) 满足 $|N(S)| < |S| $,显然不存在 \(|S|\) 个点的完美匹配。

充分性:设 \(|N(S)| \geq |S|\) 已经满足,若不存在完美匹配,考虑未匹配的点,其一定有出边,且所有出边的点都被 \(S\) 匹配过,则 \(|N(S)|\) 会多出至少一个点未被匹配,因此可以继续增广。

推论:二分图最大匹配数为 \(|X| - \max (|S| - |N(S)|)\)

9.模拟费用流

模拟网络流 / 费用流

在一些特殊的网络流模型(序列,树)中,增广路有一定的特点,可以不利用最大流算法进行求解,而可以直接维护最大流 / 最小割的形态,以较少的信息表示出一条增广路,从而达到优化时间复杂度的目的。

一般在题目中,可以预先找到一组合法的满流,在此基础上寻找负环(交换条件)。

[BS3055] 种树

设在位置 \(i\) 种树,称 \(i\) 占据了位置 \(i\) 和位置 \(i+1\),则问题相当于每个位置最多被占据一次。

建出二分图,奇数作为左部点,偶数作为右部点,相邻的两个点右边。

则增广一次 \(u\) ,新的经过 \(u\) 增广路变成了 \(a_{u - 1}+a_{u+1} - a_u\),因此直接用堆维护。

[AGC018C] Coins

容易建出一个双层的费用流模型:\(S\) 向所有人连边 \((S, i, 1, 0)\)\(i\) 再分别向三种金币连边 \((i, A / B / C, 1, a_i/b_i/c_i)\)\((A/B/C, T, x/y/z/, 0)\)。费用流就是答案。

随便找一组满流,在此基础上,发现有如下 5 种交换方式:

  1. 一个 \(A\) 和一个 \(B\) 交换,新增收益为 \((b_i - a_i)+(a_j - b_j)\)
  2. 一个 \(A\) 和一个 \(C\) 交换,收益为 \((c_i - a_i)+(a_j - c_j)\)
  3. 一个 \(B\) 和一个 \(C\) 交换,为 \((c_i - b_i)+(b_j - c_j)\)
  4. 还可以 \(A \to B \to C \to A\) 交换。
  5. 还可以 \(A \to C \to B \to A\) 交换。

于是维护 6 个堆分别表示 \(i\) 换成 \(j\) 的收益,每次判断堆顶是否合法再分别加起来取最大的增广,若最大收益 \(\leq 0\) 则表示已交换完毕。

【PA2014】Muzeum

显然是一个最大权闭合子图的模型。

\(i\) 警卫能看到手办 \(j\) 当且仅当 \(y_i \geq y_j, \dfrac{|x_i -x_j|}{y_i - y_j} \leq \dfrac w h\),注意到直接移项后若 \(y_i - y_j < 0\) 显然不合法,直接把绝对值拆成两个后就有:

\[x_bh+y_bw \leq x_ah+y_aw \\ x_bh-y_bw \geq x_ah-y_aw \]

变换坐标为 \((x' = xh +yw, y' = xh - yw)\),等价于 \(x_b \leq x_a, y_b \geq y_a\) ,是一个左上角的形式,遵循“能满就满”的思想,按照 \(x\) 排序后,越靠下的手办越难流出去,于是只需要对每个警卫贪心地从最小的 \(y_b \geq y_a\) 的点开始流,若流为 0 就删除。可以用 set 维护。

[NOI2019] 序列

要求至少有 \(L\) 组相同等价于至多 \(K - L\) 组不同,因此新建虚拟节点 \(p\) 设其流量上限为 \(K - L\),每个 \(a_i\) 连边 \((S, i, 1, a_i)\)\((i, p, 1, 0)\)\((i, i', 1, 0)\)\(b_i\) 同理,再限制 \(S\) 流出的流量为 \(K\),答案就是最小费用流。

贪心地先选出 \(L\) 对使得满流,然后考虑负环的形态。设 \(p\) 的剩余流量为 \(freflow\)

  1. 不使用 \(freflow\), 选择一个下标 \(p\) 使得 \(a_p\) 未被选择, \(b_p\) 被选择,再选择一个下标 \(q\) 使得 \(b_q\) 未被选择,将 \(a_p\)\(b_p\) 匹配,\(b_p\) 原先匹配 \(a_i\)\(b_q\) 匹配。
  2. 不使用 \(freflow\),对于 \(b\) 序列进行 1 操作。
  3. 选择下标 \(p\) 使得 \(a_p\) 未选,\(b_p\) 选;\(q\) 使得 \(b_p\) 未选,\(a_p\) 选,将 \(a_p\)\(b_p\) 匹配,\(a_q\)\(b_q\) 匹配,\(b_p\)\(a_q\) 之前的匹配匹配。此时增加 1 点 $freflow $
  4. 使用 $freflow $ ,则选择下标 \(p, q\) 使得 \(a_p\) 未选,\(b_q\) 未选,将他们匹配。 当且仅当存在 \(freflow\) 且上面的 3 种决策都比 4 劣时才能选择。

直接开 4 个堆维护,时间复杂度 $O(n\log n) $。

【BZOJ4849】 [Neerc2016]鼹鼠隧道Mole Tunnels

显然是一个匹配模型。问题在于怎么快速地找到一个有洞的点,使得总费用最小。

注意到每条边都有一个双向的 \((u, v, \infty, 1)\),反向边最多一边才有,可以对于每个点维护其父亲到他的反向边流量。对于一个在 \(u\) 的老鼠,考虑直接枚举其 lca,找在 lca 子树内距离最小的点 \(v\),然后将 \(u \to v\) 的反向边流量 + 1。

维护 \(f_u\) 表示 \(u\) 子树内距离 \(u\) 最小的存在洞的点,每次将 \(u \to v\) 反向边 + 1 后从底向上开始更新即可,注意要更新到根节点。

CF724E Goods transportation

显然连边 \((S, i, p_i), (i, T, s_i)\),对于 \(i < j\) 连边 \((i, j, c)\),答案就是最大流。

由于中每个边权都是 \(c\),考虑直接算出最小割,每个点 \(i\) 有两种状态:割掉 \((S,i, p_i)\) ,还需要割掉在 \([1, i - 1]\) 中与 \(S\) 相连的边;割掉 \((i, T, p_i)\)

设计 \(dp_{i,j}\) 表示考虑到前 \(i\) 个点,目前有 \(j\) 个点选择的是与 \(S\) 连通的最小代价,则 \(dp_{i,j} = \min(dp_{i - 1, j}+p_i+j \times c, dp_{i - 1, j-1} + s_i)\),然后 \(O(n^2)\) 转移即可。

然而可以直接贪心选出最优的最小割,令初始点都与 \(S\) 相连,则一个点从与 \(S\) 相连变成与 \(T\) 相连所需代价为 \(p_i - s_i +(i - 1) \times c\),直接按照 \(v_i = p_i - s_i +(i - 1) \times c\) 排序后,再减去选出的点之间的连边,依次取 \(\min\) 就是答案。时间复杂度优化到了 \(O(n\log n)\)

单向链模型

单向链:一个图 \(G\)\(p_i\)\(p_{i+1}\) 有边,\(S\)\(p_i\) 有边,\(p_i\)\(T\) 有边。 例如:

(luogu.com.cn)

因其增广路单一,可以方便维护。

主要维护方法有线段树,数据结构维护凸函数等。

【BZOJ4977】跳伞求生

题意:\(n\) 个玩家,第 \(i\) 个战斗力为 \(a_i\)\(m\) 个敌人,第 \(i\) 个战斗力为 \(b_i\),赏金 \(c_i\)。玩家 \(i\) 能打败敌人 \(j\) 当且仅当 \(a_i > b_j\),会得到 \(a_i - b_j + c_j\) 的收益,求最大收益。

解法 1(模拟增广路)

所有人按照 \(a_i / b_i\) 排序,先建出单向链。

用堆维护替换的增广路,若原先老鼠 \(a\) 匹配洞穴 \(b\) ,现在改为 \(c\) 匹配 \(b\)。在图上体现为一个负环,增量为 \(v_c - v_a\),当 \(a, b\) 匹配时,预先加入 \(-v_a\) 表示替换代价。可以直接用堆来维护。

解法 2(凸函数)

不妨把价值取反变成取最小代价。

将人看作左括号,敌人看作右括号,那么一组合法的方案就是括号匹配。

\(f_{i,j}\) 表示当前考虑到前 \(i\) 条边,目前剩余左括号为 \(j\) 个的方案。答案就是 \(f_{n, 0}\)

若新加入一个权值为 \(x\) 的左括号,显然有转移 \(f_{i, j} = \min(f_{i - 1, j}, f_{i - 1, j - 1}+x)\)

若加入一个权值为 \(x\) 的右括号,也显然有 \(f_{i, j} = \min(f_{i - 1, j}, f_{i-1,j+1}+x)\)

我们断言 \(f_i\) 是关于 \(j\) 的凸函数,因为设 \(g_{i,j} = f_{i,j} - f_{i, j - 1}\),则当 \(g_{i - 1,j} \leq x\) 时,\(f_{i,j} = f_{i - 1, j}\),否则 \(f_{i,j} = f_{i - 1, j - 1} + x\)。故找到第一个 \(g_{i - 1, j} > x\) 的点 \(p\) 后,\(p\) 之前的 \(g_{i, j} = g_{i- 1, j}\)\(g_{i, p} = f_{i - 1, p - 1}+x - f_{i - 1, p - 1} = x\)\(p\) 之后的 \(g_{i, p+1} = f_{i-1, p}+x - (f_{i - 1, p - 1}+x) = g_{i - 1, p - 1}\),相当于在 \(p\) 之前插入一个 \(x\)

若加入一个 \(x\) 的右括号同理,等价于删除第一个 \(g_{i - 1, 0}\) 然后加入一个 \(-x\)

从费用流的角度来看,第一种插入一个 \(x\) 相当于在候选增广路中加入 \(x\);而第二种等于选择最小代价与 \(x\) 匹配,然后反悔插入 \(-x\)。与第一种做法一样。

【CF802O】April Fools' Problem (hard)

解法 1 :显然可以 wqs 后变成例题 1 的单向链。时间复杂度 \(O(n\log^2 n)\)

解法 2 :直接用线段树维护每次增广路。

由于初始不存在负环,故增广过程中也不会出现负环。则路径一定是形如 \(S \to u \to .. \to v' \to T\)。只用考虑走正向边和反向边两种情况。

若走正向边,由于从小到大的流量都是 \(\infty\),直接维护区间 \(\min a_i\)\(\min b_i\),每次合并只用把左边的 a 和右边的 b 合并即可。

若走负向边,设走 \((a, b)\),设 \(f_i\) 表示 \(i+1\)\(i\) 的负向边流量,则需要保证\([a, b - 1]\)\(f_i > 0\)。套路地维护一个 \(pre\)\(suf\) 表示该区间最靠右的满足 \([l, pre]\) 都有流的,最靠左的满足 \([suf, r+1]\) 都有流的点。则合并直接 \(pre, suf\) 拼起来。

还需要维护区间 \([r+1, l]\) 的流量 \(f\)\(tag\)。但发现下推一个 tag 时 \(pre\)\(suf\) 几乎无法更新。考虑差分,将 \([l,r+1]\) 的所有流量减去 \(f\) 后再维护 \(pre\)\(suf\),这样下推的 tag 不会造成任何影响。同时,查询 \([1,n]\) 的答案时,由于 \(f_n = 0\),也不会对答案造成影响。

时间复杂度 \(O(m\log n)\)

【CF280D-BZOJ3638】k-Maximum Subsequence Sum

简单建出一个费用流模型后,当增广了一条路径后把边取反,相当于将 \([l, r]\) 的值全部乘上 \(-1\) ,然后继续求最大子段和。

由于不存在两次选择选出的左端点右端点相同,所以直接维护区间最大子段和,最小子段和,最大前后缀等信息即可。

【ICPC World Finals 2018】征服世界

总之你就是想要征服这个世界。

显然连边 \((S, i, a_i, 0), (i, T, b_i, 0), (u, v, \infty, c)\) 跑最小费用流。由于 \(\sum\limits_{i}b_i \leq 10^6\) ,考虑分别对每单位流量分开考虑,然后反悔。

\(dis_u\) 表示根到 \(u\) 的边权和,枚举 \(lca = x\),则 \(u, v\) 匹配代价为 $dis_u +dis_v - 2 \times dis_x $。则显然贪心策略是找到最小的源点 \(dis_u\),汇点 \(dis_v\),由于每个 \(b_i\) 都要跑满,因此预先将汇点代价减去 \(\infty\),然后每次取 \(val_u +val_v - 2 \times dis_x\) \(< 0\) 的匹配。

反悔就是 \(u\)\(x\) 上面的点 \(p\) 的某个儿子 \(v\) 匹配,因此反悔代价为 \(2 \times dis_x - dis_v\),用两个可并堆维护即可。

由于路径之间肯定不相交,若一组匹配两个都反悔,则一定不优,因此总操作次数是 \(O(\sum a+\sum b)\) 级别。

[NOI2017] 蔬菜

每种蔬菜可以看成有 \(x_1\) 个标记为第一天后变质,\(x_2\) 个第二天后变质...那么第 \(i\) 天可选择的蔬菜为变质时间 \(\geq i\) 的蔬菜。由此对每个蔬菜 \(i\) 建出费用流:\((S, i, m, 0), (i, i + 1, \infty, 0), (i, T, x_i, a_i)\)\(b_i\) 的限制只用把最后一天变质的蔬菜的一个流量变成 \(a_i+b_i\)

由于总流量不超过 \(O(m\max(p_i))\),因此考虑从左往右依次加入与源点相连的边增广,由于每个链上没有边权,流量只可能是 \(i \to j(i \leq j)\)\(i \to j(i >j)\)。对于第一种情况直接增广然后将反向边流量 +1,第二种查询 \([j, i)\) 的反向边流量是否都 \(>0\) 即可,可以简单的用线段树维护,每次贪心找权值最大的蔬菜进行匹配。

双向链模型

【CTSC2010】产品销售

连边 \((S, i, D_i, 0)\)\((i, i+1, \infty, C_i)\)\((i+1, i, \infty, M_i)\)\((i, T, U_i, P_i)\),答案就是最小费用最大流。

从小到大增广起始边,每次一定是形如 \(S \to i \to j \to T\) 的形式。若 \(j > i\),不会出现 \(-M_i\) 的负向边,所以只会走 \(C_i\) 边;若 \(j < i\),则会出现 \(-C_i\) 的反向边,所以会预先走 \(-C_i\),若没有才会走 \(M_i\)

用两棵线段树分别维护从当前点到 \(i\) 的正向费用,反向费用,每次找最优的增广。为了维护是否存在 \(-C_i\) 边,需要再维护一颗线段树表示当前 \(i +1 \to i\) 负向边流量。每次当流量变成 0 时就删除,否则在第二棵线段树上加入 \(-C_i - M_i\) ,需要暴力在第三棵树上查询。但由于一个负向边只会在小于它时加入,大于它时删除,因此复杂度为 \(O(n\log n)\)

【UOJ455-UER #8】雪灾与外卖

显然可以套用上面一题的思路用线段树维护。 但由于边权是距离,因此可以直接反悔。

按照坐标排序后,对老鼠和洞穴都尝试向左匹配,用两个堆维护老鼠的决策和洞的决策。

一个位置在 \(x_i\) 的老鼠:

  • 贪心策略:设最优的洞代价为 \(v\),则花费为 \(v+x_i\)
  • 反悔策略:老鼠可能和右边洞穴匹配,在老鼠的堆中加入 \(-(v+x_i) - x_i\)

一个位置在 \(y_i\),代价为 \(w_i\) 的洞穴:

  • 贪心策略:设最优老鼠代价为 \(v\),则花费为 \(v+y_i+w_i\)
  • 反悔策略 1:这个洞可能和右边的老鼠匹配,在洞中加入 \(-(v+y_i+w_i) - y_i+w_i\)
  • 反悔策略 2:这个老鼠可能和更靠右的洞匹配,在老鼠中加入 \(-(w_i+y_i)\)

正确性:若老鼠和左边洞穴匹配,将左边洞穴次数 -1 后反悔和右边洞穴匹配,我们要证明不存在右边的老鼠和这个被反悔的洞穴匹配,因为匹配的洞穴是一样的,而两个老鼠匹配必定有交叉,因此一定不优。对于洞穴的证明同理。

【LOJ574】黄金矿工

初步想法是建出树上的双向链,维护 3 个线段树分别表示矿工收益,黄金收益,流量。对于一个矿工,先跳有反向边流量的父亲边,跳到最上面的点,然后在子树里面查询黄金的最大值,然后增广;对于一个黄金,同样可以跳父亲找到最优的矿工,但可能是在轻子树内的,所以第 2 棵线段树还要维护可达的轻子树的点。

但很快发现这样不行,因为增广时需要改变链上的反向边流量,会导致频繁地在 12 树上更改,势能不对。

换个维护方式:直接把每个矿工收益变成 \(P_x -dis_x\),黄金变成 \(Q_y+dis_y\),那么匹配就是二者收益和,就不用再维护每个点的收益了。黄金和矿工的匹配仍然按照上面的思路。若 \(u\) 上的一个权值为 \(w_u\) 的矿工和 \(v\) 上的权值为 \(w_v\) 的黄金匹配后,又来一个矿工与 \(w_y\) 匹配,则它一定可以到达 \(u\),可以看作 \(u\) 上有一个权值为 \(-w_u\) 的黄金,\(v\) 上有一个权值为 \(-w_v\) 的矿工,那么就可以直接维护了。

posted @ 2022-10-01 21:41  henrici3106  阅读(83)  评论(0编辑  收藏  举报