图论 tricks
杂项
特殊权值图中,最短路在MST上
如果权值满足“若 \(a<c且b<c\),那么 \(a+b<c\) ”(如 \(\{2^i\}\))那么多点之间最短路的并在这些点的MST上,贪心建MST即可。
证明:prim 和 dijkstra 的区别就在于上面两个式子。因此只要有左推右的条件,就有这一性质。
三元环计数
用于枚举给定图的所有三元环个数。复杂度 \(O(m\sqrt{m})\)。
给每条边定向,从度数较少的点连向度数较多的点。在这个DAG上,对于每个点的每两对出边检查一下即可。
每个点的出边最多有 \(O(\sqrt{m})\) 条,因此复杂度正确。证明:如果原来的度数是 \(>\sqrt(m)\) 的,那么这样的点只有 \(O(\sqrt{m})\) 个,而由于定向的规则,每个这样的点只会联出 \(O(\sqrt{m})\) 个边。
建图/建树 trick
这是一类非常广泛的trick。其做法就是把一些状态当成图上的点跑图上算法(比如最短路),或者把某些结构转化为树,然后跑树上算法。
建图的情形一般出现在:要dp极值的情况 etc.
Kruscal 重构树
进行kruscal算法的过程中,当我们试图加一条边的时候,改为把两个连通块的根连向一个新的点,这个新的点就是这个新连通块的根,可以把边权赋值给这个点。
这一结构上,两个原来就存在的点(而不时新增的)的LCA的权值就是他们两个点的路径上的最大边权的最小值。
树
LCA 最近公共祖先
使用 树链剖分 法,可做到 \(O(n) - O(\log n)\),但是跑不满。
使用 倍增 法,可做到 \(O(n\log n) - O(\log n)\),但是会跑满。
使用 RMQ(dfn序)法,可做到 \(O(n\log n) - O(1)\)。事实上,使用 分块st表 可以做到 \(O(n)\) 预处理,但是我不会。
RMQ 法
将树按照 dfn 摊到线段上。如果要询问 \(x\) 和 \(y\) 的 LCA,查询 \([dfn_x+1,dfn_y]\) 区间内深度最小的点,该点的父亲就是 LCA。注意特判 \(x=y\),此时 LCA 就是 \(x(y)\)。
求任意两点树上路径value之和
对于树上路径的价值 \(=\) 边权之和的,使用树形dp即可。
对于树上路径的价值 \(=\) 边权 \(\max\) 的,使用类似最小生成树的算法,加边时统计两侧size乘积即可。
子树相关
子树操作、查询
一、摊开成线段。使用dfn序把节点映射到线段,记录每个点子树中最大的dfn,称为last。容易发现,一个节点的子树在线段上是连续的一段区间,因此可以适用区间操作。
二、离线然后直接dfs。把操作离线到点上,然后dfs的时候,把当前节点的操作做一下,回溯的时候撤回就可以了。
有些题目上述两种方法都适用,只是途径不同,例如:模拟赛题目,既可以展开成 \((depth-t)\) - \(dfn\) 图用二维数点做,也可以直接 dfs 然后树状数组维护 \(t-depth\) 。
两个点各在(或不在)某个子树内
摊成dfn序,然后会发现如果把两个点的编号表示为一个二元组,在平面直角坐标系上是一个矩形(或是2、4个矩形,若为反子树)。二维数点即可。
给定有根树上任意两点距离,求结构
注意到一条树边比所有经过这条树边的路径长(正权图可用),因此使用 prim 求最小生成树即可得到树的结构。复杂度 \(m\log m\) 。
计算双关键字最小生成树
题目:
给定两个系数 \(A,B\) ,每个边有两个关键字 \(a_i,b_i\) ,求生成树 \(T\) 的 \(A\times \max\limits_{i\in T}a_i+B\times \max\limits_{i\in T}b_i\) 的最小值。
\(1\le n\le 400,1\le m\le 50000\)
解法:
将边按照第一关键字从小到大排序,顺序插入。每次插入时,插入 vector ,然后对 vector 中现有的边跑一遍第二关键字的最小生成树,最后把多余的那条边 erase 掉。这样维持 vector 中只有 \(n\) 条边,因此复杂度为 \(O(mn\log n)\) 。
树的直径
树的直径定义为树上最长的一条路径。(可能有多条)
性质
- 任何点到直径上一点的距离不超过直径的任意一端到这一点的距离。(否则它就是直径的端点了)
- 到任何一点最远的点一定是直径的两个端点之一。(设这个点叫 \(x\) ,离它最远的点叫 \(y\) ,这个点到直径的最短路径与直径的焦点是 \(z\) 。那么 \(y\) 肯定不在 \(x-z\) 的路径上。如果 \(y\) 不在直径上,那么 \(dis(y,z)>dis(x,直径一端)>dis(y,直径一端)\) ,根据性质1,不可能。因此 \(y\) 在直径上,而直径上距离 \(x\) 最远的点只能是两端点之一)
- 合并两颗树时,新的直径的两端点一定在原来两棵树的四个直径端点中选取。
求解方法
两遍 DFS :从任意一个点跑一遍 dfs ,记距离最远的点为 \(s\) 。再从 \(s\) 出发跑一遍 dfs ,记距离最远的点为 \(t\) 。那么 \(s\) 到 \(t\) 的路径就是一条直径。原理依据“直径的性质2”。
例题
一本通OJ:运用了“两个树通过增加一条边合并后,新树的直径的端点一定属于原来两树的直径的端点”这一条性质,原理依据“直径的性质1”。
Dijkstra 算法
适用性
无负权单源最短路,和所有等价的问题。
部分无负权多源最短路,但是我不清楚。
请注意有负权的图和所有等价的问题(例如边加上某个权值或者边权取反)都不能解决(因为贪心证明的正确性依赖边权非负)
复杂度
各种变种的复杂度见这个帖子:https://blog.csdn.net/qq_50332374/article/details/123803892
简单来说
| 变体名称 | 复杂度 |
|---|---|
| 朴素dijkstra | \(O(n^2)\) |
| 优先队列优化dijkstra | \(O((n+m)\log m)\) |
| 二叉堆优化dijkstra | \(O((n+m)\log n)\) |
| 斐波那契堆优化dijkstra | \(O(m+n\log n)\) |
拓扑排序
适用于DAG。
请注意,开始的时候要将所有入度为0的点入队,否则他们的出度没有被统计,导致有些点永远也没有遍历到。
如果把所有入度为0的点入队不方便,考虑把没有的点删掉。
判环 / 构造DAG
想要判断一个有向图中是否有环,只需跑一遍拓扑排序看看有没有没访问到的点。
你可以通过决策边的朝向来构造一个 DAG,如何做到?只需按照题中限制算出每个点的拓扑序,按着拓扑序决定边的顺序。
拓扑排序DP
最常见的应用(据我所知,是唯一的应用)。请注意这是个DP,所以仔细分析你的转移方程有没有问题。(如果是缩点+topoDP题,需要调试的部分会非常多,所以首先要确保你的转移是正确的)
强连通分量
强连通分量,又称SCC,是指有向图中的一个极大的子图,其中每两个点都互相连通。一个有向图可以被分为若干个不相交的强连通分量。
Tarjan
Tarjan是用dfs求scc(强连通分量)的算法。它还可以求割点、割边、点双连通分量、边双连通分量(无向图中)等。
Tarjan的大致步骤:
- dfs,过程中求dfn。把点入栈。
- 从边的v往u转移low。low表示从这个点出发,通过若干条树边和至多一条非树边能达到的最小dfn。如果这条边是树边,用其low更新我的low,否则如果v在栈里,用其dfn更新我的low。
- 如果low等于dfn,说明我这个点代表了一个scc。把我和我上面的点出栈,划分到我这个scc里。
注意易错点:
- 如果v不在栈中,不用v更新我的low(因为v已经有归属了)。
- 在low=dfn时,要把我和我上面的全部出栈,不要只把
low[st.top()]=dfn[x]的点出栈,否则会错。 - 我来解释一下上一点,考虑
1<->2<->3这种情况。此时,\(low_3=dfn_2,low_2=dfn_1,low_1=dfn_1\) 但是 \(1,2,3\) 都在同一个强连通分量中,因此要照着上面的做法。可能有人会问:为什么要区分这条边是树边还是非树边,都用low来更新我的low,然后出栈时只把 \(low_i=dfn_u\) 的 \(i\) 出栈不行吗?答案是:理论可行,但是他的low可能还没有求好(如果他是我的祖先的话),此时就不对了。
一个小trick
Tarjan缩点后的点的排列顺序是逆拓扑序,所以(如果要对新图跑DP的话)不需要对新图进行拓扑排序。当然为了避免出现意外还是写一遍比较好。
Korasaju
利用多次dfs求强连通分量的算法,泛用性没有Tarjan那么广,可以说是为了SCC造出来的算法。
步骤
- 进行一遍dfs,记录后序。
- 重复执行,选取还没分配SCC的后序遍历最大的点,从这个点出发在反图上做一遍dfs,这个点能到的点就是同一个SCC的。
- 如果还有点没分配SCC,就重复步骤2.
感性证明
后序遍历中,编号最大的点是比较先访问的,因此可以到达很多点。在反图上跑dfs就相当于看看哪些点能到达它。注意反图上跑dfs不会到达一个自己访问不到的点,因为如果这种情况出现了,那么最开始的dfs就会顺着这条边过来,而这个点就不会是现在后序遍历中最大的了。
2-SAT
介绍就自己看 OI-wiki 吧。
关键在于,如果随意地添加有向边是很容易导致各种各样的矛盾的,所以 2-SAT 的正确性有待深刻的证明。(也就是在 2-SAT 的问题形式下,为什么用这种方法做就可以)可惜 OI-wiki 上没有给出证明。
一些简单的证明尝试: 首先,关于无解,要么是同一变量的两个点出现在了同一scc中,要么是 \(1T\rightarrow 1F\rightarrow 2T\rightarrow 2F\),而后者必定不会出现(因为肯定有 \(2F\rightarrow 1T\)。其次,关于为什么这么做就能得到解:感性理解吧,我也不会证明。
Floyd 算法
这种算法具有很好的数学性质。
判断连通性
离线多次查询连通性,用floyd预处理就对了。
如果加入了一个点,那么可以直接用floyd \(O(n^2)\) 转移。(注意循环顺序)
传递闭包
利用floyd转移过程传递某些信息(如祖先有哪些)。
最小生成树
性质
对于一个图的任意 MST(最小生成树),构成它的边的权值集合全都是相同的。比如都是 \(\{1,1,1,2,2,3\}\),没有 \(\{1,1,1,1,3,3\}\),根据 Kruscal 的过程显然。
https://www.luogu.com.cn/problem/CF160D
与最短路的关系
prim 与 dijkstra 只有一小点差距(priority_queue 比较的关键字不同)。
因此,可以证明:若边权都形如 \(2^k\) 且互不相同,则两点间最短路一定在最小生成树上。
【2021年北京省队集训习题】
有一个矩阵,每个格点上有个权,权值是把该点染黑的代价。
若已经有两行两列交出的四个点中的三个为黑(意思是,这四个点是一个矩阵的四个角),则可以免费染黑第四个,问把这个矩阵全染黑最小代价。
复杂度要求:矩阵边长的平方。
【方法】:用类似prim的方法,从最小的点开始按下方构建最小生成树。
【证明】:
将点(n,m)表示为n到m间的边。然后构建最小生成树。最小生成树的每一层都只有一种点(列点或行点)。
最小生成树中的每一条边都是正解中要染黑的点。
最小生成树上连续的三条边(如1—2—3)是一个矩阵的三个角,因此可以免费染黑第四个角。所以最小生成树上的三条边可以合并成一条边,然后继续合并。
由于树上两点之间只有一条途径,且由于我们要表示的是一个真实存在的点,那么最后的边就必须是一个行点和一个列点之间的边,又由于行点和列点不在同一层,因此他们两个点在树上的途径有奇数个边。三道边缩小成一道,最后会一直缩小成一条边。
因此最后得出结论:任何不在最小生成树里的边(格点)都可以免费得到。

浙公网安备 33010602011771号