问题
在一棵树上维护路径信息, 要支持线段树上的所有操作.
轻重
一个节点中, 子树的大小最大的子节点称重儿子, 连接节点和重儿子的边称重边, 重边连成的链称重链.
与之相对的, 除重儿子以外, 其余儿子称轻儿子, 轻边, 轻链的定类似.
线段树
DFS树, 过程中优先遍历重儿子所在的子树, 并且记录DFS序. 容易看出, 一条重链上的节点的DFS序连续且由上到下递增.
以DFS序为索引建立线段树, 就可以方便高效地维护重链的信息了.
分割
将路径拆分成几条重链, 然后分别在线段树上操作.
特别地, 一个点被看作是一个长度为零的重链.
子树
由于在线段树上的次序是根据DFS序排的, 所以一棵子树的所有点在线段树上一定是一个以子树根为左边界的连续区间.
所以只要根据子树根和子树大小就能得到线段树上的待操作区间左右边界, 并且用一次线段树操作就能进行修改和查询.
效率
显然, 一条轻边的两端为两条重链 (可能长度为零), 而线段树上的操作数量取决于路径上重链的数量.
试着使一条路径上的重链尽可能多, 如果需要包含一个 \(2k - 1\) 条轻链的路径, 节点数最少的树将是深度为 \(k+1\) 的完全二叉树 (根节点深度为 \(1\)), 节点数为 \(2^{k+1} - 1\), 所以, 一棵含有 \(n\) 个节点的树在被轻重链剖分后, 路径上轻边的数量是 \(O(logn)\) 级别的, 重链数量比轻边数量多 \(1\), 所以重链数和线段树操作数都是 \(O(logn)\) 量级的.
一次线段树操作复杂度为\(O(logn)\), 所以总复杂度为\(O(mlog^2n)\).
实现
线段树
略
邻接表存树
struct Edge;
struct Node {
unsigned int Siz /*子树大小*/, Dep /*深度(根为1)*/, DFSr /*DFS序*/;
unsigned int Val /*权值(卫星数据)*/;
Node *Fa /*父亲*/, *Top /*所在的重链顶端(即深度最小的点)*/, *Hvy /*重儿子*/;
Edge *Fst /*邻接表的一部分, 首条边*/;
} N[200005];
struct Edge {
Edge *Nxt /*邻接表, 下一条边*/;
Node *To /*邻接表, 终点*/;
} E[400005], *cnte(E) /*栈顶*/;
Lnk() 略
DFS1
处理 Siz, Hvy, Dep, Fa
void Bld(Node *x) {
if (x->Fa) { //不是根
x->Dep = x->Fa->Dep + 1; //深度比父亲大一
} else {
x->Dep = 1; //根的深度为1
}
x->Siz = 1; //初始化Siz
Edge *Sid(x->Fst); //遍历儿子
while (Sid) {
if (Sid->To != x->Fa) { //是儿子
Sid->To->Fa = x; //儿子的父亲是自己
Bld(Sid->To); //递归子树
if (!(x->Hvy)) {//无重儿子
x->Hvy = Sid->To;//当前这个做目前重儿子
} else {
if (x->Hvy->Siz < Sid->To->Siz) {//更重
x->Hvy = Sid->To;//更新重儿子
}
}
x->Siz += Sid->To->Siz;//累计Siz
}
Sid = Sid->Nxt;//继续遍历
}
return;
}
DFS2
处理 DFSr, Top
void DFS(Node *x) {
x->DFSr = (++cntd); //标记DFS序
Edge *Sid(x->Fst);
if (x->Hvy) { //优先走重儿子
x->Hvy->Top = x->Top; //与父同链, 链顶与父相同
DFS(x->Hvy); //递归子树
} else {
return; //叶子
}
while (Sid) {
if (Sid->To != x->Fa && Sid->To != x->Hvy) { //普通儿子
Sid->To->Top = Sid->To; //重链链顶为自己
DFS(Sid->To); //递归子树
}
Sid = Sid->Nxt; //继续遍历
}
return;
}
路径操作
路径修改和查询
修改
void LnkChg(Node *x, Node *y) {
while (x->Top != y->Top) { //不在同一重链上
if (x->Top->Dep < y->Top->Dep) {
swap(x, y); //保证x所在的链顶深度较大
}
yl = x->Top->DFSr; //线段树上操作左边界(x所在链顶)
yr = x->DFSr; //线段树上操作右边界(x)
SgChg(SgN, 1, n); //线段树上操作
x = x->Top->Fa; //路径上删去从x到x所在链顶这一段
}
if (x->Dep < y->Dep) { //这时x, y在同一链上
yl = x->DFSr; // x为左边界
yr = y->DFSr; // y为右边界
return SgChg(SgN, 1, n); //进行最后一次线段树操作
} else {
yl = y->DFSr; // y为左边界
yr = x->DFSr; // x为右边界
return SgChg(SgN, 1, n); //同上
}
return;
}
查询
unsigned int LnkQry(Node *x, Node *y) {
unsigned int Tmp(0); //累计结果
while (x->Top != y->Top) { //其它的同上
if (x->Top->Dep < y->Top->Dep) {
swap(x, y);
}
yl = x->Top->DFSr;
yr = x->DFSr;
Tmp += SgQry(SgN, 1, n);
x = x->Top->Fa;
}
if (x->Dep < y->Dep) {
yl = x->DFSr;
yr = y->DFSr;
Tmp += SgQry(SgN, 1, n);
} else {
yl = y->DFSr;
yr = x->DFSr;
Tmp += SgQry(SgN, 1, n);
}
return Tmp % Mod; //减少取模次数
}
子树操作
修改
void SonChg(Node *x) {
yl = x->DFSr; //子树根
yr = x->DFSr + x->Siz - 1; //树根加大小减一就是右边界
return SgChg(SgN, 1, n); //线段树操作
}
查询
unsigned int SonQry(Node *x) { //同上
yl = x->DFSr;
yr = x->DFSr + x->Siz - 1;
return SgQry(SgN, 1, n); //线段树操作自带取模
}
总结
在比赛中使用树链剖分是一个冒险的决定, 码量大, 易出错, 效率不高(log带平方). 但是不可否认, 树链剖分是一个功能像线段树一样强大的方法, 是个练习代码能力的好数据结构. (又写了 \(10\) 天)
由于联赛中大概率用不到, 所以建议作为联赛和省选内容的过渡.