强连通
- 强连通分量
- 将强连通分量缩点后是 DAG;
- Kosaraju 算法求 SCC:
- 先在原图上做 DFS,当结束一个点 \(u\) 搜索时将 \(u\) 压入一个栈中。
- 将所有边反向,得到一个反向图。
- 接下来每次从栈中弹出一个点,在反图上 DFS,我们声称每次 DFS 出来的可达的点就构成一个强连通分量。
- 证明:首先搜出来的连通块一定是互相可达的。这是因为从 \(u\) 开始搜到的每个点 \(v\) 都满足在原图上可达 \(u\),且由于 \(u\) 的完成时间晚于 \(v\),因此在原图的 DFS 树上,\(u\) 要么是 \(v\) 的祖先,要么和 \(v\) 不在一个 SCC。并且由于我们相当于是按照原图缩点后的拓扑序进行的 DFS,因此不存在第二种情况,即 \(u\) 一定是 \(v\) 的祖先,所以 \(u,v\) 互相可达。
- Tarjan。
2-SAT 判定
将每个点拆成 \(x\) 和 \(\lnot x\) 两个点,两个点 \(u\to v\) 连边代表 \(u\) 为真那么 \(v\) 也为真,则所有限制可以被拆成若干条边的形式。我们对这个建出来的图缩点。
原先的 2SAT 问题有解当且仅当对于每一个 \(x\),\(x\) 与 \(\lnot x\) 不在同一个 SCC 里。证明:
(必要性)如果 \(x\) 与 \(\lnot x\) 在同一个 SCC 里,即 \(x\iff \lnot x\),则有矛盾,所以无解。
(充分性)如果没有一对 \(x,\lnot x\) 在同一个 SCC 里,按照以下方法能构造出一组合法解:
- 按照拓扑序的逆序遍历每个 SCC,如果一个 SCC 还没有被赋值,则将这个 SCC 里的所有变量设为 \(true\),对于这个 SCC 里的变量的反变量设为 \(false\)。
- 声称这个能保证合法性,因为蕴含关系是双向的。
双连通
- 边双:删除任意一条边图仍然连通
- 点双:删除任意一个点图仍然连通
- 桥(割边),割点。
Tarjan 算法
- 一条边 \((u,v)\) 是割边 \(\iff\) 它不是返祖边且 \(low[v]\ge dfn[u]\)。
- 缩点:求出所有非割边,然后用并查集 union 起来。
CF1000E We Need More Bosses
- 有一个\(n\)个点\(m\)条边的无向连通图,小A和小B在上面玩游戏,小A先选择两个点\(s\)和\(t\)作为起点和终点,之后小B会在尽可能多的边上设置障碍,但是他需要保证任何\(s\)到\(t\)的路径都会经过他设置的所有障碍。小A想让这个游戏更具挑战性,所以他想知道如何选择\(s\)和\(t\),使得小B能设置的障碍最多。
- \(n-1 \le m \le 3 * 10^5\)
相当于每个障碍物都要设置在割边上面,于是缩点之后求树的直径。
割点
- 每个割点属于多个点双,每个非割点只属于一个点双。
- 割点:非根节点 \(u\) 是割点 \(\iff\) 存在一个子节点 \(v\) 满足 \(low[v]\ge dfn[u]\),或 \(u\) 是根节点且 \(u\) 有多于两个子节点。
圆方树
求出每个点双之后,我们将一个点双表示为一个方点,原先的点表示为一个圆点,则会形成一棵树的形式,这就是圆方树:


最后这个圆方树一定是树,证明:
如果出现一个环,分为三种情况:
- 环上有两个 BCC,则这两个 BCC 应该被合并为一个。
- 环上只有一个 BCC,即两个点之间有边,则这两个点都可以被合并到那个 BCC 里。
建圆方树:
在 Tarjan 找 BCC 的时候,每次从割点连一条边到 BCC(同时把 BCC 内部的点连向这个 BCC)。
用处
- 两个点 \((u,v)\) 的路径之中必须要经过的点即为他们在圆方树上的路径的所有圆点。
- 两个点 \((u,v)\) 的简单路径能够经过点,即为树上路径的所有圆点与方点的并。
例题
- 这世界上有\(N\)个网络设备,他们之间有\(M\)个双向的链接。这个世界是连通的。在一段时间里,有\(Q\)个数据包要从一个网络设备发送到另一个网络设备。一个网络设备承受的压力有多大呢?很显然,这取决于\(Q\)个数据包各自的路径。不过,某些数据包无论走什么路径都不可避免的要通过某些网络设备。你要计算:对每个网络设备,必须通过(包括起点、终点)他的数据包有多少个?
- \(N, M, Q \le 10^5\)
圆方树上每次路径加一,直接树上差分就好:\(u+1,v+1,LCA(u,v)-1,parent(LCA(u,v))-1\).
LOJ2587「APIO2018」铁人两项
当我们固定 \(s,f\) 之后,合法的 \(c\) 就是所有 \(s\) 到 \(f\) 路径上的点,于是我们建出圆方树以后相当于要求 \(\sum_s\sum_f path(s,f)\)。
如果我们把圆方树上每个方点的权值赋为对应边双的大小,圆点的权值赋为 \(-1\),则最终的答案就是要求 \(s,f\) 之间的路径权值之和(这是因为在路径上的每个割点都要被恰好两个边双共享,所以要减掉 \(1\) 的贡献,且 \(s,f\) 不能为 \(c\) 所以也要减掉),于是就变成经典问题:对于所有点对求路径点权和。
考虑拆贡献,一个点 \(x\) 会被统计多少次贡献呢?不妨容斥一下,统计有哪些路径没有经过 \(x\),在树上将 \(x\) 删掉后会分成若干个连通块,这些连通块内部之间的路径才是不会经过 \(x\) 的,也就是 \(\sum_{i} \binom{size[i]}{2}\)。
CF487E Tourists
给一个\(n\)个点\(m\)条边无向图,点有点权\(w_i\), \(Q\)次操作:
- 修改一个点的点权
- 询问\((A, B)\),求一条简单路径\(v_1 = A, v_2, ..., v_k = B\),最小化\(\min (w_{v_1}, w_{v_2}, ..., w_{v_k})\),输出这个最小值。
\(n, m, q \leq 10^5\)
根据上述性质,两个点之间的简单路径就是圆方树上的所有边双的并,所以每次修改的时候,可以对每个方点开个线段树维护所有非割点,然后再树剖维护树上的点权最小值。这样每次修改只会改 \(O(1)\) 个点,所以最终时间复杂度 \(O(q \log n)\)。
好像开 multiset 也行。
虚树
我们有一个很大的树 \(T\),但是我们不关心 \(T\) 完整的形态,我们只关心 \(T\) 中的一个子集 \(T'\subset T\) 和这些点两两之间的关系。这个时候就可以用虚树。
具体来说,我们需要拿到一个点集 \(S\),要包含 \(T'\) 以及 \(T'\) 之中所有点两两的 LCA。暴力做这个至少是 \(O(n^2)\) 的。但是可以:
- 在原树上做一遍 DFS 拿到每个点的 DFS 序;
- 将 \(T'\) 中的所有点按 DFS 序从小到大排序为数组 \(v\)。
- 求出 \(v\) 数组中每一对相邻的两个点的 LCA,记为集合 \(L\)。
- 声称最终的虚树点集 \(S=T'\cup L\)。
证明:不妨设有两个点 \(LCA(v_i,v_j)\not \in S\),此时由于 \(v_i\) 的 DFS 序小于 \(v_j\),分两种情况讨论:
- \(v_i\) 是 \(v_j\) 的祖先,则 \(LCA(v_i,v_j)=v_i\in S\),不成立。
- 否则,\(v_i,v_j\) 应该分属 \(LCA(v_i,v_j)\) 的两个子树,则不妨设 \(v_p\) 是 \(v_i\) 所在子树里 DFS 序最大的,\(v_q\) 是 \(v_j\) 所在子树里 DFS 序最小的,则 \(LCA(v_p,v_q)\) 就应该要等于 \(LCA(v_i,v_j)\),否则他们就应该处在 \(LCA(v_i,v_j)\) 的同一个子树内。因此这种情况不成立。
故原命题为假,不存在 \(LCA(v_i,v_j)\not\in S\)。这个方法是 \(O(n\log n)\) 的。
如果想要去掉 \(\log\),则可以把排序换成基数排序,再把 LCA 换成 \(O(n)-O(1)\) LCA。
圆方树和虚树可以结合起来,解决很多题目。