第三种树链剖分: 长链剖分
长链剖分
树链剖分可以分为三种, 分别是重链剖分, 动态树 (实链剖分) 和长链剖分 (Long Path Decomposition).
长链剖分的规则是将子树包含最深的后代的儿子设为 "长儿子", 然后将到长儿子的边称 "长边", 长边组成的链称 "长链", 其余的边都是 "短边". 和重链剖分类似, 长链的长度定义为它包含的节点数, 或是长边数 .
前两种剖分方式, 在绝大多数题目中可以相互代替, 但是长链剖分的应用却和它们不同.
路径短边数量
考虑如何使用最少的节点数, 构造一棵树, 让它在长链剖分后, 根到某个叶子的路径上, 短边数量最多.
依然是重链剖分的构造方式, 从边界情况开始考虑:
- 根到叶子最多有 个短边, 最少需要 个点.
连点都不用就构造完了, 没什么好解释的.
- 根到叶子最多有 个短边, 最少需要 个点.
一个根, 左右儿子都是单点, 这时, 两个儿子必有一个是长儿子, 另一个是短儿子, 有一个短边.
- 根到叶子最多有 个短边, 最少需要 个点.
在上一棵树的基础上, 在根上接一个父亲作为新的根, 在这个根下面接一个长度为 的垂直的链作为长链, 则旧根可以做新根的短儿子.
- 根到叶子最多有 个短边, 最少需要 个点.
发现以这种构造方式, 每次可以从根到叶子最多 个短边的树的基础上, 接一个根节点, 再接一条长度为 的长链. 这样, 所需的点数便成了等差数列求和, 所以得到式子 .
举个例子, 这是一棵到叶子最多有 个短边的树, 短边分别是: 6-3
, 3-2
, 2-0
.
因此, 对于一棵 个节点的树, 根到叶子路径上最多的短边数量为 .
主要应用: 求树上第 级祖先
由于长链剖分的特殊性质: 一个长链链顶的子树的深度就是这个长链的长度. 所以可以保证一个点 的 级祖先所在的长链长度至少有 . 这个结论很好证明, 只要假设这个长链长度小于 , 则 点到它的 级祖先的路径必然是长度为 的长链.
对于剖分后的每个点, 分配一个 DFS 序, 保证在长链上的相邻的点的 DFS 序相邻.
对于一棵 个点的树, 可以建立一个长度为 的数组 , 对于每个长链, 设它的长度为 设它链顶的 DFS 序为 , 则 的第 的位置, 依次存 的第 级祖先.
再记录一个数组 , 使得 表示 DFS 序为 的点的编号. 因为长链剖分的性质, 对于一个长链链顶 (它的 DFS 序为 ), 的第 位表示它的 级长链上后代.
接下来, 和倍增求 LCA 一样, 求每个节点的 级祖先, 预处理复杂度 .
另外要提前预处理对于 的 .
这时查询任意点 的第 级祖先, 首先 找到 的第 祖先 满足 .
这时, 可以保证 所在的长链长度至少为 . 而因为 , 所以 , 这时分类讨论:
-
目标在 所在的长链上
这时 查询 链顶的 数组即可.
-
目标在 所在的长链顶之上
所以目标一定在 链顶的 中可以 查询.
实现
长链剖分和重链剖分类似, 分两次 DFS, 求出所需的几个值. 本题需要初始化几个数组, 然后 回答询问即可.
unsigned Seed, Log[500005], Bin[20], Hd(0), List[500005], InList[500005], m, n, Cnt(0), A, B, C, D, t, Ans(0), Tmp(0);
inline unsigned Rand(unsigned x) {
x ^= x << 13, x ^= x >> 17, x ^= x << 5;return Seed = x;
}
unsigned long long Prt(0);
char Short(1);
struct Node {
Node *Fa[20], *Son, *Bro, *LonSon, *Top;
unsigned Len, DFSr, Deep;
}N[500005], *Root, *Top[500005];
void Link(Node *x, Node *y) {
y->Bro = x->Son, x->Son = y;
}
void DFS1(Node *x) {
Node *now(x->Son);
x->Len = 1;
while(now) {
now->Deep = x->Deep + 1;
now->Fa[0] = x;
for (register unsigned i(0); now->Fa[i]; ++i) {
now->Fa[i + 1] = now->Fa[i]->Fa[i];
}
DFS1(now);
if(now->Len + 1 > x->Len) {
x->Len = now->Len + 1;
x->LonSon = now;
}
now = now->Bro;
}
}
void DFS2(Node *x) {
if(Short) Top[++Hd] = x, x->Top = x;
Node *now(x->Son);
x->DFSr = ++Cnt;
InList[Cnt] = x - N;
if(x->LonSon) {
Short = 0;
x->LonSon->Top = x->Top;
DFS2(x->LonSon);
Short = 1;
}
while(now) {
if(now != x->LonSon) {
DFS2(now);
}
now = now->Bro;
}
}
signed main() {
n = RD(), m = RD(), Seed = RD();
for (register unsigned i(1); i <= n; ++i) {
A = RD();
if(!A) {
Root = N + i;
continue;
}
Link(N + A, N + i);
}
Root->Deep = 1;
DFS1(Root), DFS2(Root);
for (register unsigned i(1), L, R; i <= Hd; ++i) {
register Node *x(Top[i]);
L = x->DFSr, R = L + x->Len - 1;
for (register unsigned j(L); j <= R; ++j) {
List[j] = x - N;
if(x->Fa[0]) x = x->Fa[0];
else break;
}
}
for (register unsigned i(1), j(0); i <= n; i <<= 1, ++j) {
Log[i] = j, Bin[j] = i;
}
for (register unsigned i(3); i <= n; ++i) {
Log[i] = max(Log[i], Log[i - 1]);
}
for (register unsigned i(1), Delta; i <= m; ++i) {
A = ((Rand(Seed) ^ Ans) % n) + 1;
B = (Rand(Seed) ^ Ans) % N[A].Deep;
if(!B) {Ans = A, Prt ^= (unsigned long long)i * Ans; continue;}
register Node *x(N[A].Fa[Log[B]]);
B -= Bin[Log[B]];
Delta = x->Top->Len - x->Len;
if(Delta >= B) {
Ans = InList[x->Top->DFSr + Delta - B];
} else {
Ans = List[x->Top->DFSr + B - Delta];
}
Prt ^= (unsigned long long)i * Ans;
}
printf("%llu\n", Prt);
return Wild_Donkey;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具