树上倍增
树上倍增其实可以类比为树上二分,不断的去逼近答案。也没有什么好说的直接看题吧。
例 \(1.1\):Tree - 洛谷
最近公共祖先 LCA
求法大致有三种:倍增 \(lca\),\(Tarjan~lca\)(离线),树剖 \(lca\) 。
这里用的是树剖 \(lca\) (好写好调,常数还小)。
一些概念:
-
重儿子:节点 \(x\) 的儿子中子树最大的。
-
重边:节点 \(x\) 连向其重儿子的边。
-
重链:重边构成的链。
过程:如果两点在同一条链上,返回深度较小的点。若两点不在同一条链上,找到所在重链上端点深度较大的节点更新为其所在重链端点的父节点,直到两点到达同一条链上。
例题 \(2.1\):【模板】最近公共祖先(LCA) - 洛谷
Kruskal 重构树
一些约定:
-
记点 \(u,v\) 之间的边权为 \(w(u,v)\)
-
在无歧义的情况下,点 \(x\) 的点权记为 \(w_x\)
-
元素 \(x\) 所在集合的代表元素记为:\(rep(x)\)
回忆 \(Kruskal\) 算法求最小生成树的过程:按照边权排序,顺次连接边,并查集判环。
\(Kruskal\) 重构树的建树:
-
在每次连边的时候,新建一个节点 \(x\),将 \(w(u,v)\) 作为 \(x\) 的点权。
-
取消 \(u,v\) 之间的连边,将 \(rep(u),x\) 和 \(rep(v),x\) 之间连接一条有向边。
-
更新 \(rep(u)=rep(v)=x\)。
重复以上过程直到选出的边构成原图的一个生成树。
一些性质:
-
\(Kruskal\) 重构树是一个二叉堆(显然加入的节点权值具有单调性)。
-
原图上的所有节点是重构树的叶子。(考虑重构树的建树过程不难发现)
Kruskal 重构树的应用
- 两点之间的最短路上的最大边权。
给定两点,它们之间的最短路径上的最大值为它们在重构树上 \(LCA\) 的点权。
证明:考虑建树的过程,最后加的有关 \((u,v)\) 边一定是最短路径上的最大值,也是 \(u,v\) 的 \(lca\) 节点。
- \(Kruskal\) 重构树与倍增
考虑这样一个问题:从原图的节点 \(u\) 出发,经过边权不大于 \(d\) 的边,能够到达的所有点。
由于重构树满足二叉堆的性质,那么可以用倍增求出深度最浅的 \(w(x)\le d\) 节点 \(x\),那么 \(x\) 的子树内的所有叶子结点,就是原图上节点 \(u\) 能到达的所有点。
需要注意的一点是,在建树过程中,排序既可以从大到小排序,也可以从小到达排序,这也相应的影响了重构树的一些性质。
例 \(3.1\):[NOI2018] 归程 - 洛谷
根据海拔建立 \(Kruskal\) 重构树(小根堆),每次询问找到节点 \(x\) 满足在 \(x\) 的子树中每个点互相可达。
由于车不会再被使用,于是倍增求出深度最浅的满足 \(w(x)<p\) 的节点,\(x\) 的子树的叶子结点,就是所有可达节点。
想到这里,不难想到 \(Dijkstra\) 预处理最短路,树形 \(DP\) 搞出以 \(x\) 为根的子树中到达节点 \(1\) 的最短路径。
这样就可以做到 \(O(n\log n)\) 的复杂度。
长链剖分
不同于重链剖分,长链剖分将子树中深度最深的作为重儿子。然后用类似于重链剖分的方式把树分成了若干条不相交的链。
长链剖分的性质,更好地帮助理解长链剖分:
-
一个点到根的路径,最多经过 \(\sqrt n\) 条轻边。
-
一个节点到它所在长链底端的路径为到它子树内节点中最长的一条。
-
任意一个点的 \(k\) 级祖先所在链的链长一定大于等于 \(k\) 。
-
轻边的长度不会大于 \(1\) 。
例题 \(4.1\):[模板] 树上 k 级祖先 - 洛谷
显然可以用树上倍增做到 \(O(n\log n)\) 预处理,\(O(\log n)\) 查询,但这还不够优秀。
利用倍增预处理得到每个节点 \(u\) 的 \(2^k\) 级祖先。然后对于每一条链,设其长度为 \(len\),向上向下分别预处理出 \(len\) 个祖先和儿子。另外还要处理出每个数的二进制最高位 \(h_i\) 。
对于每次查询节点 \(u\),利用倍增向上找到 \(u\) 的 \(2^{h_u}\) 祖先节点,此时 \(u\) 所在链的长度一定 \(\ge 2^{h_u}>\) 剩下未跳的节点数。由于我们预处理了此时 \(u\) 所在长链顶端往上,往下 \(len\) 个点,于是便可以 \(O(1)\) 找到 \(u\) 的 \(k\) 级祖先。
长链剖分一个主要应用就是优化与深度相关的 \(DP\) 。
例题 \(4.2\):Problem - 1009F - Codeforces
容易得出普通 \(DP\) 转移 \(f(u,j)=\sum{f(son,j-1)}\) ,考虑优化。
利用长链剖分将树剖成一条一条的链,对于某个节点 \(x\) ,转移时,直接继承它重儿子的信息,即指针直接赋值(优化所在),暴力合并轻儿子的信息。
复杂度分析:每个点只会在它所在链的顶端被统计或者暴力合并一次,所以时间空间复杂度都是 \(O(n)\) 级别的。
Submission #172037369 - Codeforces
例题 \(4.2\):[POI2014]HOT-Hotels 加强版 - 洛谷
观察发现,满足题意的 \((i,j,k)\) 都可以表示为距 \((i,j)\) 的最近公共祖先距离相等的 \((i,j,k)\) 。
首先设计出二维的 \(DP\) 转移(有点小复杂),设 \(f(u,j)\) 表示 \(u\) 的子树内与 \(u\) 距离为 \(j\) 的点的个数,容易得出转移方程:\(f(u,j)=\sum{f(son,j-1)}\) 。
发现无法满足题述要求,再设 \(g(u,j)\) 表示在 \(u\) 的子树内满足 \(dis(lca(x,y),x)=dis(lca(x,y),y)=dis(lca(x,y),u)+j\) 的无序点对 \((x,y)\) 的个数。
即再添加一条长度为 \(j\) 的链就可以与点对 \((x,y)\) 产生贡献。
转移方程:
对于此类不是很好用儿子直接表示父亲的树形 \(DP\),考虑在通过已经合并的答案来进行转移。
-
\(lca(x,y)\neq u\),直接由 \(v\) 贡献即可:\(g(u,j)=\sum_{v\in son_u}{g(v,j+1)}\) 。
-
\(lca(x,y)=u\) ,\(g(u,j)=\sum{f(u,j)*f(v,j-1)}\)
对于 \(2.\) 中的 \(f(u,j)\) 表示的是已经合并的子树的答案,所以 \(f\) 的转移应在 \(g\) 之后。
统计答案时: \(ans\leftarrow \sum{f(u,j)*g(v,j+1)+g(u,j)*f(v,j-1)}\)
\(f(u,j)*g(v,j+1)\) :即从已经转移的子树中选择一条长度为 \(j\) 的链,再从当前子节点子树中抓取一对 \((x,y)\) 配对。
\(f(v,j-1)*g(u,j)\) 表示从当前子节点的子树中选择一条长度为 \(j-1\) 的链,然后从已经转移的子树中抓取一对 \((x,y)\) 配对。
这是 \(O(n^2)\) 的 \(dp\) (评测记录(70pts) - 洛谷),联想到长剖优化,优化后发现转移前漏算了贡献 \(g(u,0)\) ,即开始从已有子树转移时,未计算已继承重儿子子树内部与 \(u\) 所产生的贡献。加上即可。
像 \(g[son[u]]=g[u]-1\) 这样的转化关系需要预分配两倍的空间。
对于长剖指针的一些理解:
\(f[son[u]]=f[u]+1\):将 \(f[son[u]]\) 的指针预先设为 \(f[u]+1\),这样在之后修改 \(f[son[u][i]\) 时就是修改 \(f[u][i+1]\) 了,这是因为查询某个位置如 \(f[son[u]][p]\) 的值的时候是根据 \(f[son[u]]\) 向后跳来的,即 \(f[son[u]]+p\),而此时 \(f[son[u]]\) 被赋值为 \(f[u]+1\) 就相当于 \(f[u]+1\) 向后跳 $p $ 格得到 \(f[u][p+1]\) ,如此修改的自然是 \(f[u][p+1]\) 指向的值了。
标签:重构,洛谷,长链,树论,son,lca,节点 From: https://www.cnblogs.com/mklzc/p/16796060.html