「学习笔记」图论基础
有些东西可能不说原理,或者干脆没有提到,有可能是太简单了没必要,也有可能是我还不会。
根据我做题的经验,图论的很多结论都是要 “猜”,证明的话大部分思路是考虑反证,最终由定义或者引理导出矛盾。
基本上是总结类型的,如果想要看教程的话直接看 “参考资料” 里的内容()
参考资料:
如何看待 SPFA 算法已死这种说法? - fstqwq的回答 - 知乎
《算法竞赛进阶指南》李煜东
约定与定义
约定
\(G=(V,E)\) 表示点集为 \(V\),边集为 \(E\) 的图,如无特殊说明,则记 \(n=|V|,m=|E|\),也就是点数和边数。
无向图中连接点 \(x\) 和点 \(y\) 的边记作 \((x,y)\),\(x\) 到 \(y\) 的路径记作 \(x\rightsquigarrow y\);
有向图中连接点 \(x\) 和点 \(y\) 的边记作 \((x,y)\),有时为了突出边的指向记作 \(x\to y\),路径记作 \(x\rightsquigarrow y\),有时意义为 \(x\) 可到达 \(y\),具体何种含义视语境而定。
若连接点 \(x\) 和点 \(y\) 的边有权值 \(v\),则记作 \((x,y,v)\).
如无特殊说明,则 \(n\) 代指图/树的点数,\(m\) 为边数。
定义
点导出子图 / 导出子图 / 诱导子图:原图中若干个点以及端点均在这个点集中的所有边组成的图。
边导出子图 / 生成子图 / 支撑子图:原图中若干条边以及这些边连接的所有点组成的图。
常用于网络流题目中的:
闭合子图:在有向图上,点集 \(V'\) 导出的闭合子图是所有 \(V\) 可达的点的点导出子图。也就是对于 \(\forall x\in V'\),\(x\) 所有出边以及出点均在子图内,这等价于每个点能到达的所有点都在子图中。
\(k\) 正则图:若一个无向图每个点度数都为 ,则称其为 \(k\) 正则图。
注意:图论问题中可能图不是连通图,不是简单图,注意看清楚题面有无具体说明。
DFS 树
最基础的,大家都会!
注意无向图 dfs 树中只有树边和返祖边,而有向图中的 dfs 树还会有横叉边和前向边。
dfs 序:每个节点入栈时将其记录到 dfs 序中,提供了一个将子树映射到序列区间的方法。
欧拉序:以 dfs 的顺序遍历整棵树(回溯也视作遍历),按照顺序把遇到的点写下来,即为欧拉序。常见应用是 \(\mathcal{O}(n\log n)-\mathcal{O}(1)\) LCA,\(u\) 和 \(v\) 的 LCA 就是以它们在树上第一次出现的位置作为左右端点的区间中深度最小的点。
括号序:每个节点入栈或出栈时将其记录到 dfs 序中,常见应用是判断祖孙关系,以及树上莫队。判断 \(u\) 是否 \(b\) 的祖先等价于判断 \([in_u,out_u]\) 是否完全包含 \([in_v,out_v]\).
(有的地方把括号序也称为欧拉序,不过我没有找到较为权威的定义)
DFS 树也经常运用于图论相关的构造题中,可以看看 jiangly 的论文。
最短路
SPFA
大家都会!
SPFA 常见优化方式
SLF 优化:每次将入队结点的 \(dis\) 和队首的比较,如果更大则插入至队尾,否则从队头插入。带容错:比队首大过一定值才插入队尾。交换:每当队列改变时,如果队首距离大于队尾,则交换首尾。
LLL 优化:每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则从队头插入。
mcfx 优化:在第 \([L,R]\) 次访问一个节点时,将其放入队首,否则放入队尾。通常取 \(L=1,R=\sqrt n\),和 SLF 带容错结合有很好的效果。
mcfx 优化和 SLF 带容错结合的 SPFA 实现,虽然仍然可以被卡,但是在一些可以差分约束的题中用这种优化来得到更多的分数。
SPFA 判负环
SPFA 判负环的方式有两种,一个点进入队列次数(不是被松弛次数),或者到某个点最短路的边数大于 \(n-1\),则说明整张图存在负环。后者会快一些,只需要在用 \(u\) 松弛 \(v\) 的时候令最短路边数 \(l_v\gets l_u\) 即可。
注意:很多 SPFA 优化方式并不能很快地判出负环。
Johnson
求解具有负权边的全源最短路径。
考虑对边 \((u,v)\) 的费用 \(w(u,v)\) 重新赋予新的值 \(w'(u,v)\),使得新图中满足 \(w'\geq 0\)(边权非负使得其能跑 Dijkstra),并且可以通过新图中的最短路求出原图中的最短路。
考虑对每个点 \(i\) 赋予一个势能 \(h_i\),并将 \(w'(u,v)\gets w(u,v)+h_u-h_v\),不难发现新图中一条路径 \(s\to v_1\to \cdots \to v_k\to t\) 的总权值即为 \(w(s\to t)+h_s-h_t\),只于 \(s,t\) 有关,所以求出新图上的最短路即可通过减去 \(h_s-h_t\) 来得到原图中的最短路。
怎样赋予这样的势能 \(h\) 使得 \(w'(u,v)=w(u,v)+h_u-h_v\geq 0\)?将其移项得到 \(w(u,v)+h_u\geq h_v\),得到了一个三角形不等式,也就是最短路中松弛的条件,于是新建一个超级源点 \(S\),向每一个点 \(i\) 连一条权为 \(0\) 的边,用 SPFA 把超级源点到每个点的最短路跑出来(顺便判一下是否有负环),\(S\) 到每个点的最短路即为所需要的 \(h\).
跑出 \(h\) 得到新图后,对每个 \(i\) 作为起点跑 Dijkstra 算法即可。时间复杂度为 \(\mathcal{O}(nm\log m)\).
Floyd 与传递闭包
Floyd 大家都会!图的传递闭包说的是,如果原图中 \(x\) 有到 \(y\) 的路径,那么在传递闭包的关系图上具有 \(x\) 到 \(y\) 的边。通俗地讲,就是得到两点直接是否可达。
使用 Floyd 暴力实现传递闭包是 \(\mathcal{O}(n^3)\),使用 bitset
优化则为 \(\mathcal{O}(\frac{n^3}{w})\).
bitset<N>f[N];
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
if(f[i][k])
f[i]|=f[k];
差分约束
差分约束系统提供了通过图论建图以最长路/最短路的形式刻画变量之间的不等关系。常见的应用是判断不等关系是否有合法解。
对于最短路,我们将 \(x_v\leq x_u+w\) 描述为 \(dis_v\leq dis_u+w\),感性理解一下,在这里描述的是 \(x_v\) 的上界,而且通过跑最短路找到了 \(x_v\) 的上界。虽然跑的是“最短路”,但在不等式组的意义下 \(x_v\) 可能会更小,所以这里是在满足不等式组的条件下让 \(x_v\) 取到了它的上界。
类似地,如果想要用最长路来跑差分约束,得到的是每个变量的下界。
总结:最短路跑出来的是每个变量最大可能的值,最长路跑出来的是每个变量最小可能的值。
同余最短路
求解一类问题:给定一些数 \(a_i\),求在一个范围内的数有多少数可以被给定的数的系数非负的线性组合表示出。也就是多少个数可以被完全背包得到。
如果 \(r\) 可以被表示出,那么 \(r+xa_i,x>0\) 也能被表示出,所以固定一个 \(a_t\) 作为模数,按照余数对每个数划分等价类,对于每个等价类求出最小的能被表示出的是多少即可。
不难建出一张图,对于每个余数 \(r\),对于每个 \(i\) 连边 \((r,(r+a_i)\bmod a_t,a_i)\),然后跑最短路即可。
直接跑 Dijkstra 或 SPFA 固然可行,但是这个图看上去很强的性质,能不能利用起来?
考虑固定最大的 \(a_t\) 作为模数,对于一个余数 \(r\),其最小能表示的数为 \(r+k\times a_t\),令这个 \(k\) 为最短路。不难发现,当一个 \(r\) 进行松弛的时候,若 \(r+a_i\geq a_t\),由于 \(r<a_t,a_i\leq a_t\),那么其会拓展到 \(r+a_i-a_t+(k+1)\times a_t\),也就是 \(dis_{r+a_i-a_t}\gets dis_{r}+1\);若 \(r+a_i<a_t\),则为 \(dis_{r+a_i}\gets dis_r+1\).
也就是这张图的边权变为了 0/1,跑 01 bfs 即可。时间复杂度是 \(\mathcal{O}(na)\).
当然,如果 \(\min a\) 很小的话跑 SPFA 会有更好的效果(在这里图比较特殊, SPFA 不容易跑满)。
最短路树/图
最短路树
最短路树是原图一个生成树。对于起点 \(s\) 求出到每个点最短路径,如果 \(v\) 节点的最短路是由边 \((u,v)\) 松弛得到,则在生成树中保留 \((u,v)\).由于每个点都只会通过自身的最短路链接自己的前驱,而不断追溯前驱即可找到起点 \(s\),所以是一棵树。
根据定义,其具有性质:树上 \(s\) 到 \(x\) 的路径长度是 \(s\) 到 \(x\) 的最短路。
大部分题目或者证明是围绕这个性质展开分类讨论之类的。
最短路图
虽然是“最短路图”,但实际上是个 DAG。
有一个起点 \(s\),对于每个点 \(x\) 保留原图上所有 \(s\) 到 \(x\) 的最短路,即为最短路图。
性质:
- 如果将每一条边根据哪一端点离起点更近来定向(更近的连向更远的),那么连出来是一个 DAG,因为最短路径不会有环出现(除非图中出现了负环,此时不存在最短路图)。
- 最短路图的所有生成树都是最短路树,每一个最短路树都是最短路图的一棵生成树。
求出这个图的方法:跑 \(s\) 为起点到每个点的最短路,如果对于一条边 \((u,v,w)\) 满足 \(dis_u+w=dis_v\),则连边 \(u\to v\).
如果仅想要保留某个终点 \(t\) 来说,所有 \(s\) 到 \(t\) 的最短路,那么就判断一条边 \((u,v,w)\) 是否满足 \(dis(s,u)+w+dis(v,t)=dis(s,t)\),是则连边。
性质一的证明思路能够类似地证明很多结论,比方洛谷 P2149 的结论。
最短路树和最短路图不难板子的例题:BZOJ 3694,[SDOI2009] Elaxia的路线,CF1163F。
最小生成树
Kruskal
将边从小到大排序并遍历,如果边的两个端点还没有连通,则将这条边加入最小生成树,时间复杂度 \(\mathcal{O}(m\log m)\).
证明。
Prim
维护当前已经在最小生成树中的点集 \(V\),每次选择距离 \(V\) 最近的点加入最小生成树,然后松弛剩下的点到 \(V\) 的距离。
暴力实现是 \(\mathcal{O}(n^2)\),与 Dijkstra 同样可以使用堆优化,使用优先队列优化的时间复杂度为 \(\mathcal{O}(m\log m)\).
Borůvka
别乳卡/jy
每一轮操作,对于每个点来说,让它和 "与他相邻的最近的还未连通的点" 相连。假设连通块个数为 \(k\),每一轮每个连通块都至少连出去一条边,而至多会去除 \(\frac{k}{2}\) 条重边,每成功连一次边连通块个数都会 \(-1\),所以一轮后连通块个数至少减半。
时间复杂度为 \(\mathcal{O}(m\log m)\).
Borůvka 通常运用在一类特殊的最小生成树问题,如无法显示地将图建出来,但是能快速计算两点之间边的距离的一类问题。
最大生成树
将边权取反跑最小生成树,即得到最大生成树。实现上,可以直接改变偏序关系的判断,例如 Kruskal 变为从大到小排序。
瓶颈生成树
一张图的最大边权最小的生成树是瓶颈生成树。
可以简单地用反证法证明,瓶颈生成树包含最小生成树,或者说最小生成树是瓶颈生成树的充分不必要条件。
最小生成树一定是瓶颈生成树:若不是的话,瓶颈生成树中任意一条边都小于最小生成树的最大边,那么将最小生成树的最大边删去,用瓶颈生成树中的任意一条可以连接删剩下的两棵子树的边,来连接删剩下的两棵子树,即可得到边权和比最小生成树更小的生成树,产生矛盾。
瓶颈生成树不一定是最小生成树:考虑任意一棵最小生成树,只需要固定其最大边必须选,小于最大边的任意选,连接出的一棵生成树即为瓶颈生成树,但这不一定是最小生成树,根据 Kruskal 最小生成树一定是尽量选权较小的边。
类似地,可以询问任意 \(x,y\) 两点之间简单路径中最大边权最小是多少,只需要找出任意一棵最小生成树上 \(x\) 到 \(y\) 的最大边权即可,正确性证明同理。
Kruskal 重构树
Kruskal 的过程中,维护每个已经连通的集合一个代表元(初始时单个节点本身的集合代表元就是它自己),连边 \((u,v)\) 时需要将 \(u\) 所在集合 \(U\) 和 \(v\) 所在集合 \(V\) 合并成新的集合 \(T\),给新的集合一个新的代表元,然后连边 \((U,T),(V,T)\).
重构树的点数为 \(2n-1\),注意要开两倍空间的数组。
根据建重构树的过程,以及前面对 Kruskal 以及瓶颈生成树相关性质的剖析,不难得到重构树具有以下性质:
- 重构树是一棵二叉树;
- 原图中的节点对应重构树中的叶子;
- 忽略叶子的权值(或者将其权值视作 \(0\) 或 \(+\infty\))重构树上的点权满足堆的性质,即父亲节点权值 \(\geq\)(如果是从大到小加边则为 \(\leq\))儿子节点权值;
- 对于原图中 \(u,v\) 两点,它们之间所有路径的 经过边权最大值 的最小值 是 \(u,v\) 在重构树上 LCA 的权值;
- 对于原图中 \(u\) 点,其只经过 \(\leq w\) 的边能走到点,为 \(u\) 在重构树上最浅的满足权值 \(\leq w\) 的祖先 \(a\) 子树中的叶子,如果从大到小加边就是 “\(\leq\)”,找到这个点可以使用倍增。
围绕这些性质,有关 “仅能走权值小于等于(或者大于等于)某个值的边” 的连通性问题,要想到 Kruskal 重构树。
例题
套路:
经过点权有上下界,点权转边权,点权重构树:仅能走点权 $w\leq $ 某个值 \(c\) 的点,转成边权,那么对于边 \((u,v)\) 来说能经过必须要 \(u\) 和 \(v\) 同时满足 \(w\leq c\),于是记录其边权为 \(\max(w_u,w_v)\),问题变成只能走 \(\leq c\) 的边。
经过边权有上下界,上下界重构树:仅能走边权 \(\geq L\) 且 \(\leq R\) 的边。考虑从小到大和从大到小建两棵重构树,这样 \(u\) 能到达的点就是在两棵重构树中 \(u\) 根据上/下界倍增调到的节点的子树的交。数个数的话可以直接利用 dfs 序转化成二维数点。
经过点权有上下界,多叉重构树:Kruskal 重构树提供了一个边权转点权并保留了某些性质,点权重构树中将点权转为边权再转成点权,能不能直接点权到点权转化呢?假如说限制的是仅能走点权 \(\leq c\) 的点,那么按照点权从小到大排序尝试加入重构树中,每次对于一个 \(u\),遍历其出边 \(v\),若 \(w_u>w_v\),说明 \(v\) 已经被加到重构树中,并且 \(\max(w_u,w_v)\) 此时取的值为 \(w_u\),那么将 \(w_u\) 作为 \(v\) 所在连通块的代表元的父亲即可。不难发现这个过程与按照 \(\max(w_u,w_v)\) 作为边权排序等价。由于不需要重新赋边权及建虚点之类的。常数更小。
例题:
牛客练习赛 62 E 水灾:比板子多了一个小结论,Code。
[IOI2018] werewolf 狼人:可以用上面的提到的套路,Code。
欧拉图
定义
有孤立点到底算不算?我暂且存疑。不过在实际题目中并不需要拘泥于这个定义。
欧拉路径/回路:通过图中所有边恰好一次的路径/回路。
欧拉图:具有欧拉回路的无向图(或有向图)。
半欧拉图:具有欧拉路径,但不具有欧拉回路的无向图(或有向图)。
判定
无向图为欧拉图当且仅当图连通且每个点度数都是偶数。
必要性:对于回路中每个点,进这个点一次就一定会出这个点一次。
充分性:先任意找一个环,去掉这个环之后图上会形成若干个连通块,由于图连通所以这些连通块都一定和这个环有交点,对这些连通块递归下去构造出欧拉回路,再拼到这个环上。
类似地,可以得出结论:
无向图为半欧拉图当且仅当仅有两个点度数为奇数,这两个奇度点就是欧拉路径的两个端点。
有向图为欧拉图当且仅当图弱连通(将所有边替换为无向边后连通),且所有点入度等于出度。
有向图为半欧拉图当且仅当图弱连通,且仅存在一个点出度比入度大 \(1\),仅存在一个点出度比入度大 \(1\),这两个点就是欧拉路径的起点和终点。
求欧拉回路
Hierholzer 算法:如果有欧拉回路的话,对图进行 dfs,经过一个点的时候把从这个点出发的环拼到整个大环上。实现上,每次 dfs 完 \(u\) 之后将 \(u\) 加入到路径中,即得到欧拉回路。如果是求欧拉路径的话,得到的就是反过来的欧拉路径。
每次 dfs 到一个点的时候要不断走第一条没有遍历过的边,需要使用类似 dinic 中的当前弧优化技巧。
字典序最小的欧拉回路/路径?将每条出边按照字典序从小到大排序再求即可。
混合图欧拉回路
混合图就是图中既有有向边也有无向边,无向边两种方向都能经过,但是只能经过一次。
考虑到最终的欧拉回路,相当于给每个无向边都定了一个向。
那么先给每条无向边都任意钦定一个方向,算出来每个点的 \(d\) 值为出度 \(-\) 入度,如果有奇数就无解了。
那么现在想办法调整若干条无向边的方向,来让 \(d\) 都为 \(0\),这个看上去很像流量平衡,于是考虑网络流做法。
对于每条无向边 \((u,v)\) 定向为 \(u\to v\),连一条 \(u\to v\),流量为 \(1\) 的有向边,表示如果将其反向,则 \(d_u\) 减少一个 \(2\),\(d_v\) 增加一个 \(2\).
对于每个点 \(d_x>0\),从源点 \(S\) 连向 \(x\) 流量为 \(\frac{d_x}{2}\),表示从 \(x\) 要流出去 \(\frac{d_x}{2}\) 个 \(2\);
\(d_x<0\),从 \(x\) 流向 \(T\) 流量为 \(-\frac{d_x}{2}\),表示要流入 \(x\) \(-\frac{d_x}{2}\) 个 \(2\).
跑最大流,如果不满流则无解,否则有流量的边就是要反向的边。
连通性相关
有向图连通性
定义
以下定义均在有向图中。
对于两个点 \(u,v\),若它们能互相到达,则这两个点强连通,具有强连通性。
若图 \(G\) 中任意两个点都是强连通的,则 \(G\) 为一个强连通图。
极大的强连通子图,称为强连通分量(SCC)。
对一张图进行 SCC 缩点后得到的是一个 DAG。
Tarjan
dfs 的时候用栈存下当前遍历到的节点,如果一个点 \(x\) 满足 \(dfn_x=low_x\),则这个 \(x\) 是其所在 SCC 的最浅的节点,有结论是栈中 \(x\) 及以上所有点是这个 SCC,那么不断弹出记录一下即可。
注意此时图是有向图,可能会出现横叉边与前向边,不能用来更新 \(low\)(\(low\) 是只能走返祖边),前向边对 \(low\) 没有影响,而横叉边就判断一下当前走非树边走到的 \(v\) 有没有已经在某个 SCC 中,如果有的话那么这条边是反向边,否则就可以拿来更新 \(low\).
Kosaraju
先求出 dfs 树,然后按照出栈序倒序在反图上 dfs,每次 dfs 所有能走到的点都构成了一个强连通分量,然后将它们在图上删去。
时间复杂度 \(\mathcal{O}(|V|+|E|)\).
正确性的话,感性理解一下:
考虑缩点后的 DAG,对于一个 SCC,显然能找到它的所有节点,证明其不会走到其它 SCC 的节点即可:
-
它不会走到它的前驱 SCC 以外的 SCC:因为反图上没有到达其的边;
-
它不会走到它的前驱 SCC:因为它的前驱 SCC 出栈序比它大,已经被标记过了。
例题 Claris’ Contest # 4 Day 2 A. 友好城市(只有在机房才能看)
无向图连通性
部分内容可能比较“显然”,因为严谨的说明太烦琐了。
定义
以下定义均在无向图中。
割点:删去后使得连通分量个数增加的点。
割边 / 桥:删去后使得连通分量个数增加的边。
在通常语境下是无向连通图下的定义,不连通的割点/割边定义为其所有连通分量割点/割边的并。
值得注意的是,孤立点,以及链的两个端点,均不是割点。
点双连通性:对于一对点来说,删掉除了这两个点以外的任意一个点之后这两个点依然连通,则这两个点具有点双连通性。(注:点双连通性最原始的定义应为任意两点之间至少有两条点不重复的路径,由于并不常用,便定义为这个几乎等价的定义)
边双连通性:对于一对点来说,删掉任意一条边之后这两个点依然连通,则这两个点具有边双连通性。
点双 / 点双连通分量:不存在割点的无向连通图 / 子图。
边双 / 边双连通分量:不存在割边的无向连通图 / 子图。
在点之间点双连通性不具有传递性,而边双连通性具有传递性。
更具体地,在点之间仅有边双连通性具有传递性,如果我们类似地定义边的点边双连通性,则在边之间仅有点双连通性具有传递性。
几乎等价的性质是,点双缩点之后一条边仅属于一个点双,边双缩点后一个点仅属于一个边双,这同时也说明边双缩点之后是通过割边作为树边的树。
考虑 \(x\) 和 \(y\) 边双连通,\(y\) 和 \(z\) 也边双连通,这意味着无论删掉哪条边,都一定依然满足 \(x\) 和 \(y\) 连通,\(y\) 和 \(z\) 连通,则 \(x\) 和 \(z\) 一定也连通,即 \(x\) 和 \(z\) 具有边双连通性。
考虑 \(x\) 和 \(y\) 点双连通,\(y\) 和 \(z\) 也点双连通,这意味着无论哪个删掉除了 \(x,y,z\) 以外的点,\(x\) 和 \(y\) 和 \(z\) 都一定连通,但是这不意味着 \(x\) 和 \(z\) 具有点双连通性,因为可能删掉 \(y\) 之后 \(x\) 和 \(z\) 不连通。例子是一个 8 字形,\(x,y,z\) 分别是顶上,中间的割点,以及底部的点,也就是 \(x\leftrightarrow y\leftrightarrow z\)。
割点
定义 \(dfn_x\) 为 \(x\) 的 dfs 序,\(low_x\) 为 \(x\) 最多经过一条返祖边能走到的 dfs 序最小的节点的 dfs 序为多少。
在 dfs 树上,如果一个点不是根,那么其为割点等价于删掉它之后子树内的点不经过 \(x\) 可达的点均在子树内部,也就是存在一个儿子 \(v\) 满足 \(low_v\geq dfn_x\)(\(low\) 不能定义为经过多条非树边,\(v\) 子树内可以通过 \(x\) 向上的返祖边走到子树外会影响判断)。
如果这个点是根,那么其为割点的充要条件是儿子个数 \(>2\)(这样删掉根之后子树之间会互相不可达)。
割边
返祖边一定不会是割边,与割点类似,对于一条边 \(e(u,v)\) 来说,\(u\) 为祖先,那么 \(e\) 是割边的充要条件是 \(e\) 是树边并且 \(low_v>dfn_u\).
由于图可能不是简单图,所以需要通过记录边的编号来判断一条边是不是从父亲来的边,因为这样的边会影响 \(low\) 的记录。
另一种方法:对于非树边 \((u,v)\),标记树上 \(u\) 到 \(v\) 路径上的树边不是返祖边,一条树边数割边充要条件就是其没有被标记,可以用树上差分来实现。
边双缩点
找出割边之后,所有不包含割边的极大子图就是极大边双。
每个点最多在一个边双中,可以在 dfs 的过程中将遍历到的节点压入到一个栈中,当判断出从 \(u\) 走到 \(v\) 的边 \((u,v)\) 是割边时,有结论是栈中 \(v\) 及以上的所有点在一个极大边双中,将它们不断弹出并记录即可。
感性理解一下正确性,就是在 dfs 的过程中,每次找到一个 \(v\),其子树内并不含割边,并且它和它父亲所连的边 \((u,v)\) 是割边,然后把 \(v\) 的子树标记为一个边双连通分量,再将其从图中删去。
例题:CF555E。
连通性相关问题要想到割点割边,点双边双这一套理论,而具体和点相关还是和边相关再考虑利用哪个解决问题。
点双缩点
见后文圆方树。
2-Sat
定义
- 命题变元:取值为真或假的变量。
- 逻辑合取:合取 / 逻辑与 \(\vee\);析取 / 逻辑或 \(\wedge\);否定 \(\neg\);蕴含 \(P\to Q\) 如果 \(P\) 成立则 \(Q\) 成立;将命题变元用这些关系来描述得到命题公式。
- 文字:命题变元及其否定。
- 简单析取式:若干个文字的析取,如 \(P=x_1\wedge x_2\wedge \cdots \wedge x_k\),其中 \(x_i\) 是一个文字,也就是命题 \(p_i\) 或其否定 \(\neg p_i\).
- 合取范式:若干个简单析取式的合取,如 \(Q=P_1\vee P_2\vee\cdots \vee P_k\),其中 \(P_i\) 是一个简单析取式。
布尔可满足性问题(SAT)是检查一个命题公式是否存在一组合法的解,是一个 NPC 问题。
而 SAT 的简单版本,命题公式为合取范式,其中每个简单析取式都由至多 \(k\) 个文字组成,这样的问题称作 k-SAT .
\(k\geq 3\) 时 k-SAT 时 NPC 问题,\(k=2\) 时及 2-SAT 存在多项式解法。
解法
称每个简单析取式为条件,那么每个条件一定固定的几种情况:\(a,\neg a,a\wedge b,a\wedge \neg b,\neg a\wedge b,\neg a\wedge \neg b\).
由于每个条件至多与两个文字有关,所以可以从图论的角度思考,对每个文字建立一个点,用有向边 \(x\to y\) 来表示蕴含关系,即“若 \(x\) 成立,则 \(y\) 成立”(值得注意的是,蕴含具有传递性,这同样提示我们可以将蕴含对应到图上的可达性),那么将每种可能的简单析取式都放在图上就是:
- \(a\):\(\neg a\to a\),由于 \(a\) 和 \(\neg a\) 不能都成立,而想要表达 \(a\) 一定为真,即可用这种边来表示 \(\neg a\) 一定不成立;
- \(\neg a\):\(a\to \neg a\),同理;
- \(a\wedge b\):\(\neg a\to b,\neg b\to a\),即其中一个文字不成立时,另一个文字一定成立;
- \(\neg a\wedge b\):\(a\to \neg b,\neg b\to a\);
- \(\neg a\wedge \neg b\):\(a\to \neg b,\to b\neg a\).
后三种关系中,具有对称性,因为一个命题成立则其逆否命题也成立,这是十分重要的一点性质,它保证了我们后面 2-SAT 算法的正确性。
一个解在图中是什么样的形态呢?对于一组解中为真的 \(n\) 个代表不同命题变元的文字,它们在图中导出的闭合子图,若包含了其它文字,则说明一定出现了一个值为真的文字 \(p\),其否定也成立,这是不合法的。这说明了这个闭合子图等同于这些点的点导出子图。这等价于不存在一个假的文字被一个真的文字所蕴含。
首先,若对于一个命题变元 \(p\) 来说,\(p\) 和 \(\neg p\) 若在一个强连通分量内,即为“若 \(p\) 则 \(\neg p\),若 \(\neg p\) 则 \(p\)”,这说明此问题无解;
否则的话,尝试构造出一组解。
注意到如果 \(\neg p\) 能到达 \(p\),那么 \(p\) 一定为真。而如果 \(\neg p\) 能到达 \(p\) 说明 \(\neg p\) 拓扑序大于 \(p\)(注意反之并不成立),这启发我们通过判断 \(p\) 和 \(\neg p\) 的拓扑序大小来判断 \(p\) 的真假。
即为,若 \(\neg p\) 的拓扑序大于 \(p\) 的拓扑序,则 \(p\) 为真,否则 \(p\) 为假。
这样的解一定合法吗?尝试反证。
记文字 \(x\) 的拓扑序为 \(d_x\).对于任意两个文字 \(a,b\),它们的拓扑序一定各自大于其否定的拓扑序,即 \(d_a>d_{\neg a},d_b>d_{\neg b}\),若 \(a\) 能到达 \(\neg b\),则有 \(d_{\neg b}>d_a\),由对称性可知 \(b\) 能到达 \(\neg a\),则有 \(d_{\neg a}>d_b\),整理可得 \(d_a>d_{\neg a}>d_b>d_{\neg b}>d_a\) 即 \(d_a>d_a\),导出矛盾,故不存在一个值为真的文字可到达一个值为假的文字。
在实现上,由于 Tarjan 时已经得到了缩点后 DAG 的反向拓扑序,直接判断所在强连通分量编号即可比较拓扑序。
时间复杂度 \(\mathcal{O}(n+m)\),\(n\) 为命题变元个数,\(m\) 为简单析取式的数量。
圆方树
圆方树上圆方果,
圆方树下你和我,
圆方树前建虚树,
欢乐多又多。
—— big_news
十年前流行的东西?因为以前没学过就多做了一点题,但实际上还是没有把遇见的题都写完了因为看起来好难写啊。
定义
仙人掌:每条边只在不超过 \(1\) 个简单环中的无向连通图。
子仙人掌:以 \(r\) 为根的仙人掌上的点 \(p\) 的子仙人掌是从仙人掌中去掉 \(p\) 到 \(r\) 的简单路径上的所有边之后,\(p\) 所在的连通块。
点双 / 点双连通分量:不存在割点的无向连通图 / 子图。
点双缩点之后一条边仅属于一个点双,一个点可能出现在多个点双中。
对于每个点双,建立一个代表这个点双的新的点,称之为方点,对于每个点双,将其的方点向这个点双里的每个点在新树里连边,这样每个点双形成了一个菊花,多个点双之间再通过割点相连,这样建出来的新树即为圆方树,注意其点数 \(<2n\),记得要开两倍空间的数组。
似乎把一般图上的圆方树称为广义圆方树,仙人掌上的圆方树称为圆方树,就不细分了。
构建
圆方树构建方式相对于找割点而言,类似于边双缩点相对于找割边:
-
在 dfs 过程中从儿子 \(v\) 回溯到 \(u\) 时,如果 \(low_v\geq dfn_u\),说明 \(u\) 是一个割点;\(v\) 目前还没有被割掉的子树,也就是栈中 \(v\) 及其以上到栈顶的部分,与 \(u\) 构成了一个新的点双,并且栈中的顺序即为在环上的顺序。
-
新建方点与它们连边即可,然后将 \(v\) 及以上的点弹出(也就是删掉 \(v\) 子树),注意不要判成“直到到 \(u\) 弹出”,但要连上 \(u\)。
Alex_Wei 言:圆方树完整地保留了原图的必经性。围绕此言的若干性质与推论,可以看这里 3.2 部分。
例题
有些题不一定必须要把圆方树显式地建出来(其实这些题就是缩点双),在遇到仙人掌题 / 点与点之间连通性问题 要想到圆方树。
仙人掌题
套路是把树上的做法搬到仙人掌上,然后单独考虑环上怎么做(和基环树好像啊),大多都是定义 \(f_x\) 为 \(x\) 的子仙人掌内的 dp 值(类比树形 dp 中"子树内的 dp 值")。
随机生成一棵仙人掌的方法:随机生成一棵树,然后对树进行随机链剖分,随机几个点向链顶连边(划分出的一条链最多加一条边),或者每次随机一条链,链底连向链顶亦可。可能需要判一下重边和自环。
仙人掌上 dp:
BZOJ1023:仙人掌的直径(任意两点最短路的最大值),Code。
利用圆方树解决仙人掌问题:
虚仙人掌:
UOJ 87 mx的仙人掌:虚仙人掌 + BZOJ2125 + BZOJ1023 大融合?
UOJ 189:代码实现太难了><
广义圆方树题
[ZJOI2004]嗅探器:板子题,用来巩固对定义及基本性质的理解,Code。
[HNOI2012]矿场搭建:板子题,讨论一下下即可,Code。
[APIO2018]铁人两项:性质题,需要想一想合法点在圆方树上满足什么条件,Code。
圆方树上建虚树,欢乐多又多:
[SDOI2018]战略游戏:虚仙人掌板子。
圆方树上树剖:
CF487E:性质还是铁人两项那道题的性质,转成一个树剖问题,然后还有一个单独维护父亲,批量维护儿子的 trick。
支配树
鸽了!
DAG 必经点/边
判断 DAG 上一个点 / 边是否是 \(s\) 到 \(t\) 的必经点/边,换而言之,判断去掉这个点/边之后,\(s\) 是否由存在到 \(t\) 的路径变为不存在到 \(t\) 的路径。
拓扑排序求出 \(fs_x\) 表示从起点 \(s\) 到 \(x\) 的路径个数;
记忆化搜索,或者在反图拓扑排序求出 \(ft_x\) 表示从 \(x\) 到 \(t\) 的路径个数。
由于是在 DAG 上,不会出现有环的路径。所以 \(fs_x\times ft_x\) 就是经过 \(x\) 的 \(s\) 到 \(t\) 的路径个数,对边也类似。
那么就有:
- 若 \(x\) 满足 \(fs_x\times ft_x=fs_t\),那么 \(x\) 就是 \(s\) 到 \(t\) 的必经点;
- 若边 \((u,v)\) 满足 \(fs_u\times ft_v=fs_t\),那么这条边就是 \(s\) 到 \(t\) 的必经边。
由于 \(fs,ft\) 可能是指数级别的数,所以选取一个大质数,在模意义下计算即可。为了让出错的概率更低可以选取更多的模数进行计算。