模板——树上算法
模板——树上算法
LCA,树的一些性质,点分治,树上莫队,DFS 序与欧拉序,笛卡尔树,树上启发式合并,Kruskal 重构树,树链剖分。
LCA
- \(O(n\log n)\) 预处理,\(O(\log n)\) 求答案。也可以求出欧拉环游序转化为 RMQ 问题,单次求答案变为 \(O(1)\)。
1 | void dfs(ll root, ll dad) { |
- 离线 \(O(n)-O(1)\) Tarjan 求 LCA。
1 |
|
树的一些性质
直径
树上任意两节点之间最长的简单路径即为树的「直径」。
- 一棵树可以有多条直径,他们的长度相等。
- 可以用树形 DP 的方法在线性时间求出树的直径。
上面的性质在存在负权时依然成立。
- 可以用两次 DFS 的方法在线性时间求出树的直径。
- 树上所有直径都必定交于同一点(或同一条边),这个点(或边)被称为树的中心。
- 合并两棵树,新树直径端点一定属于原来两棵子树直径端点的集合。
- 对于树上任意一点 \(x\),离它最远的点一定是直径的某个端点。
中心
在树中,如果节点 \(x\) 作为根节点时,从 \(x\) 出发的最长链最短,那么称 \(x\) 为这棵树的中心。
- 树的中心不一定唯一,但最多有 2 个,且这两个中心是相邻的。树的中心一定位于树的直径上。
- 树上所有点到其最远点的路径一定交会于树的中心。
- 当通过在两棵树间连一条边以合并为一棵树时,连接两棵树的中心可以使新树的直径最小。
重心
三个等价定义:
- 在树中删去结点 \(v\) 后,得到的图中每个连通分量的大小均不超过原树结点数的一半。
- 在所有删去某个结点后得到的最大连通分量大小中,删去结点 \(v\) 时所得到的值最小。
- 树中所有结点到某个结点的距离和中,到结点 \(v\) 的距离和最小。
性质:
- 树的重心如果不唯一,则恰有两个。这两个重心相邻。而且,删去它们的连边后,树将变为两个大小相同的连通分量。
- 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
- 把两棵树通过一条边相连得到一棵新的树,那么新树的重心在连接原来两棵树的重心的路径上。
- 一棵有根树的重心一定在根结点所在的重链上。
- 一棵树的重心一定是:根结点的 重子结点对应子树的 重心的 祖先。
点分治
只要路径信息能由“端点到分治中心”的摘要合并得到,并且能在每层快速查询/插入这些摘要,那么点分治就能处理整棵树所有简单路径的信息。算法框架如下:
1 | solve(当前连通块): |
注意在重新选择根节点之后一定要重新计算子树的大小。
1 |
|
树上莫队
普通树上莫队
利用下面的欧拉序性质,转化为普通莫队。
1 |
|
DFS 序与欧拉序
DFS 序
对树做
DFS,每个节点第一次被访问时,记录下它的编号,得到的序列就是
DFS 序。通常用 dfn[u] 表示节点
u 是第几个被访问的。
- 一棵子树的所有节点,在 DFS
序中恰好构成一段连续的区间。设
sz[u]为以u为根的子树大小,那么子树u对应区间:[dfn[u], dfn[u] + sz[u] - 1]。 u是v的祖先,当且仅当:dfn[u] <= dfn[v] <= dfn[u] + sz[u] - 1。
欧拉括号序
对树做
DFS,每次进入一个节点时记录一次,离开这个节点时再记录一次,得到的序列就是欧拉括号序。每个节点会出现两次,整个序列长度是
2n。
设 st[u] 节点 u 第一次出现的位置,ed[u]
节点 u 第二次出现的位置。
- 子树仍然是一段区间。子树
u在欧拉序中对应:[st[u], ed[u]]。这个区间里每个节点出现了两次。 - 路径可以表示成区间。对于树上路径
(u, v),假设st[u] <= st[v],令p = LCA(u, v):- 如果
p == u,路径对应区间[st[u], st[v]]。 - 如果
p != u,路径对应区间[ed[u], st[v]],再加上p。 - 区间内出现两次的节点,不在路径上;出现一次的节点,在路径上。
- 如果
欧拉环游序
对树做
DFS,每次经过一个节点时都记录一次,得到的序列就是欧拉环游序。整个序列长度是
2n-1。
对树做
DFS,每次到达一个节点就记录它的编号,同时记录它的深度。这样得到两个序列:euler[]
节点编号序列,长度 2n-1;depth[]
对应节点的深度,长度也是 2n-1。
同时记录每个节点第一次出现在 euler[]
中的位置 first[u]。
对于任意两个节点 u 和 v,假设
first[u] <= first[v],那么:
u 和 v 的 LCA,就是
euler[first[u] .. first[v]]
这段区间中,深度最小的那个节点。也就是说,LCA
问题变成了 区间最小值查询(RMQ)。
笛卡尔树
笛卡尔树是一种特殊的二叉树,每一个节点由一个键值二元组构成,并且同时满足以下两个性质:
- 键——二叉搜索树(BST)性质:对于树中的每一个节点,其左子树中的所有节点的值都小于该节点的值;其右子树中的所有节点的值都大于该节点的值。这保证了树在中序遍历时会按照输入序列的顺序访问元素。
- 值——堆性质:如果我们将笛卡尔树看作是一个最大堆或最小堆,则对于每个节点来说,它的值要么不小于或者不大于其子节点的值。具体采用哪种堆取决于应用场景。
经常用于最值维护、最近公共祖先问题。例如柱状图找面积最大的矩形。Treap 维护的也是笛卡尔树。
1 | int cartesian_build(int n) { |
树上启发式合并
例如计算 \(\sum_{d(x)=d(y),1\le x<y\le n}d(y)-d(lca(x,y))\)。
\(O(n\log n)\) 版:
1 | vector<ll> e[N]; |
Kruskal 重构树
原图中两个点之间的所有简单路径上最大边权的最小值 = 最小生成树上两个点之间的简单路径上的最大值 = Kruskal 重构树上两点之间的 LCA 的权值。
也就是说,到点 x 的简单路径上最大边权的最小值 \(\leq val\) 的所有点 y 均在 Kruskal 重构树上的某一棵子树内,且恰好为该子树的所有叶子节点。
注意重构树新建了节点,初始化范围为 \(n+m\)。
1 | sort(h + 1, h + 1 + m, cmp);//按照边权排序 |
树链剖分
把一棵树上的节点重新编号,使得“树上的一条路径”和“一棵子树”都能变成数组上的连续区间。然后就可以用线段树、树状数组等维护区间加、区间求和、区间最大值。
从某个节点开始,一直沿着重儿子往下走,形成一条链,叫重链。每条重链有一个链顶
top。
我们做第二次 DFS
时,优先遍历重儿子,得到的节点编号顺序叫 DFS 序,记为
dfn[u]。
这样会得到两个重要性质:
- 同一条重链上的节点,
dfn是连续的。 - 任意一棵子树内的节点,
dfn也是连续的。- 子树
u对应区间:[dfn[u], dfn[u] + sz[u] - 1]
- 子树
所以树上问题就变成了数组区间问题。
只有链可以直接使用倍增,只有子树可以直接使用 dfs 序。
1 |
|
方向合并:某些题目信息是不可交换的。在树链剖分跳链时,由于 dfn 顺序和真实路径方向可能相反,必须把某些区间的信息“翻转”后再按正确顺序合并。
1 | Info query_path(int u, int v) { |