do_while_true

一言(ヒトコト)

几个图论 trick

歪歪球/se

总结几个遇到过的图论 trick.

模拟图论算法

面对图论中的问题(又或是其他方向的问题),在我们手中有的工具是 Kruskal, Borůvka, Tarjan, Kosarajo 甚至于 dfs, bfs,这些在一般图中解决问题的算法。而面对特殊的图时,要做的就是模拟这个算法,这更需要去深刻理解算法的本质,想要模拟这个算法需要维护哪些信息。

可达性

CF920E

在一张图的补图中维护可达性信息:Codeforces 920E

和上面 Kosaraju 的应用类似,依然是考虑模拟 dfs / bfs 的过程。需要支持的操作依然是找到与一个点相连的还未被访问的点。大略地来看,在补图中与一个点不相连的点很少,尝试利用这个性质。直接记录下还未被访问的点集,每次松弛一个点的时候暴力在这个点集中寻找能够走到的点,再把不能够走到的点重新放回集合中。由于一个点在补图中不能够被松弛到当且仅当在原图中有边,所以被放回的总次数是 \(\mathcal{O}(m)\) 的。

练习题:CF1508C

bitset 模拟 Kosaraju

(可能没啥用,就是看起来好玩一点)

算法简要:求出 dfs 树,然后按照出栈序倒序在反图上 dfs,每次 dfs 所有能走到的点都构成了一个强连通分量,时间复杂度是 \(\mathcal{O}(|V|+|E|)\)

相对于 Trajan 的优势是,Tarjan 需要返祖边相关的 \(dfn,low\) 等信息,但是 Kosaraju 只需要正反两遍 dfs,其仅需要支持查询 \(x\) 的出边中还未访问的点。这一简单信息的优势使得 Kosaraju 可以压位,用 bitset 存储 \(x\) 的出边 \(g_x\),以及访问过的点 \(S\),想要找到 \(x\) 出边中还未访问的点直接找到 \(g_x\operatorname{and} S\) 中第一个 \(1\) 即可。在 Claris’ Contest # 4 Day 2 A. 友好城市(只有在机房才能看) 这道题中可以运用到这个,单次模拟 Kosarajo 时间复杂度是 \(\mathcal{O}(\frac{|V|^2}{w})\),和边数无关。

最小生成树

Borůvka

算是最常见的,或者说大部分别乳卡的题都是要用什么东西来模拟别乳卡。

XIX OPEN CUP GP OF GOMEL F. SIX WORDS

题意:无向图,点有点权,边有边权。定义一个图的线图是,每条边建一个点,点权是这条边的边权;如果两条边有公共端点,则在它们对应的点之间连一条边,边权是这个点的点权。求一张图的线图的线图的最小生成树。\(n\leq 10^5,m\leq 2\times 10^5\)

很好的题!

假如说原图是 \(G\),线图的线图是 \(L\).先考虑 \(L\) 是个什么东西,\(L\) 中的点对应着 \(G\) 中一个有公共点的“边的二元组”(称之为边组),\(L\) 中的边对应着一对边组之间的公共边,权值即为这个公共边的权值。

进而分析 \(L\) 中的边有两种,第一类是它所链接的两个边组成 T 字形,也就是这三条边有一个公共的端点,第二类是组成一个 1 字形,也就是这三条边形成一个长度为 \(3\) 的链,公共边是中间那个边。

更进一步分析性质,一个边组所连的第二类边是被第一类边所包含的。利用 “边集的并” 的 MST 等价于 “边集的 MST” 的并的 MST 这个结论,可以先用第一类边将边组合并,考虑一个中心点的出边从小到大排序是 \(w_1,w_2,\cdots w_k\),那么模拟 Kruskal 的过程,将每个边组的两条边的排名作为横纵坐标写在坐标轴上,那么它们的贡献就依次是 \(k-2,k-2,k-3,\cdots,0\)

这样中心点相同的边组被合并到一个连通块里了,然后在考虑再利用第二类边将这些连通块合并。将 \(G\) 中的边按照边权排序,如果一条边链接了两个还没合并到一个连通块的两个中心点(注意一度点不能成为中心点),那么就用这条边将两个中心点挂在上面的边组给 merge 起来。

这里需要有个结论是二类边不会在加入它之后连成环的情况下,顶掉一个一类边成为 MST 中的边,我还不会证 kdw 教会我了,观察上面那个图,一类边的权值一定是(边组的两条边权值 min)的 max,现在连出去两条二类边,一定是断掉大的那一条,那么可以观察到这两条二类边较大的那一条一定大于等于在三角形中跨过的一类边,比方说 \((3,5)\)\(3\) 连出去,\((1,3)\)\(1\) 连出去,那么 \(3\)\(\geq\) \((3,5)\)\((1,3)\) 链上的边权的。

这样就说明了这样连出去的两个蓝色的二类边,较大的那个一定 \(\geq\) 中间绿色的部分。而至于环中其他部分,交给其他的二类边来考虑,一定也是 \(\geq\).所以二类边中断掉的最大的那个,要 \(\geq\) 环中所有的一类边,故而二类边不会顶掉一类边。

各种最短路变形

最短路

最短路(Dijkstra)实际上将边和点都待求解的对象,其中一条边的最短路被定义为它的所有起点的最短路最小值加上边长。每次取出一个 \(dis\) 最小的元素,如果是点,那么松弛以起点包含它的边;如果是边,那么松弛它的所有终点。由于每次取出的都是最短路,而边权非负,所以每个点都最多会被一条边松弛,也就是这个点被一条边松弛就可以将这个点删去了。

例题是 NOI 2019 弹跳,在这里将矩形看作边。如果弹出一个点的话就直接松弛矩形。如果弹出一个矩形的话,松弛这个矩形里面的所有还未被删除的点,然后把这些点删除即可。拿个线段树套 set 就可以。

边权只有常数种的最短路

给每个边权开一个队列,一个点被哪种边权松弛,就将它压入哪个队列。再对所有队列的队首元素建堆。

正确性是因为队列中的元素 \(dis\) 都是单调的,证明方法可以直接归纳然后反证,如果压入队列的一个元素 \(dis\) 比前面的要小,那么根据归纳假设后面那个一定要比前面那个先松弛从而产生矛盾。

其实上面两个东西依然离不开上一章所阐述的模拟图论算法(即模拟 Dijkstra)的过程。

变式题

ARC 150 C

题目链接

由于子序列匹配是贪心能匹配就匹配,所以想要判断一个点 \(u\) 是否有一条到 \(n\) 的失配的路径,最优一定是选择已经匹配个数最少的那条路径继续往后走。

所以令 \(dis_u\)\(1\) 点走到 \(u\) 点是最短已经匹配到了 \(B\) 的哪个前缀。然后 \((u,v)\in E\) 则更新 \(dis_v\gets dis_u+[B_{dis_u+1}=A_v]\).如果注意到边权仅与 \(dis_u\) 有关而与 \(u\) 无关,所以每个点只有在第一次时被松弛才是有用的,故 Dijkstra 时间复杂度为 \(\mathcal{O}(N\log N+M+K)\),使用 01 bfs 的时间复杂度为 \(\mathcal{O}(N+M+K)\)

CF1749E

把仙人掌看成能走的点,转化成斜着走,从第一列某个点走到最后一列某个点,01 bfs.这个转化还挺难想到的/yun

CF1693C

学习大佬的理解方法!

先封完再走一定更优,然后考虑就是封若干条边,然后答案是 \(1\)\(n\) 的最长路加上封的边数。

先考虑 DAG 怎么做,令 \(f_i\)\(i\)\(n\) 的答案,那么对于一个点 \(u\) 其所有后继 \(v\) 按照 \(f\) 从大到小排序后是 \(f_{v[1]},f_{v[2]},f_{v[3]},\cdots,f_{v[k]}\),一定是封前几条,那么 \(f_u=\min\{f_{v_i}+i\}\)

一般图怎么做?考虑借助 Dijkstra 的思想,令 \(g_u\) 为 若 \(u\) 的出边中还没确定的 \(f_u\) 均为正无穷(也就是不考虑从它们那里转移),它的值是多少。每次可以找 \(g_u\) 最小的那个点,它的 \(f_u\) 一定就是 \(g_u\).所以用小根堆存所有的 \(g\) 每次取最小的那个确定为 \(f\),然后再用这个 \(f\) 更新其他的 \(g\).因为取出的 \(f\) 是单调的,并且用 \(f\) 更新 \(g\) 的时候有 \(g>f\)(边权非负),所以这样更新是正确的。代码写出来和 Dijkstra 很像。

CF1842D

假如每一列是若干条竖着的线段,代表这个元素出现的若干段时刻。发现把它们同时往上尽可能地移动,也就是上端对齐,是贪心尽可能让限制满足。那么一定存在一个最优方案使得选的集合是每次添进一个元素。

这个 input 容易联想到把图建出来,考虑一条边 \((u,v,w)\) 代表着什么,加入 \(u\) 是在 \(t_u\) 时刻加入到集合中的,那么就要求 \(t_v\leq t_u+w\)

所以就考虑不停往里面添数,每次添“\(\leq\)”带来的限制的最紧的那个,将其 \(t\) 取上界,直到不得不取 \(n\) 就结束了。

这个过程其实就是连边出来跑 \(1\)\(n\) 的最短路。

posted @ 2022-11-14 18:47  do_while_true  阅读(179)  评论(0编辑  收藏  举报