树剖笔记
树剖是一种解决树上问题的思想。重链剖分一般就是所说的树链剖分,简称树剖。
先思考问题一:如何实现查询树上两点间最短路径代价和?
容易发现可以维护所有点到根节点的距离和,再用倍增求 lca。不妨记两点分别为 \(x,y\),到根节点的距离为 \(dis\),即答案为 \(dis_x+dis_y-2\times dis_{lca(x,y)}\)。
问题二:如何实现树上区间加?
更容易地,我们能想到做树上差分,能够做到 \(O(1)\) 的修改。不做过多赘述。
拼起来?
这时会发现不太好做了,查询的话需要还原整个差分树,复杂度很高,不能接受。
真正优秀的算法是基于暴力的,我们思考暴力。
对于问题一,暴力的做法仍是向上跳,设 \(x\) 的深度大于 \(y\),\(x\) 一直向父亲节点跳,直到 \(x,y\) 存在于同一条链上,计算 \(x,y\) 之间的权值即可。至于如何计算权值,待会再讲。
对于问题二,那就是直接暴力扫了,不过多赘述。
树剖正是这种暴力的优化。
在介绍树剖之前,我们将不可避免地接触一些名词:
重子:设有一个节点,其所有子节点中,以子节点为根的子树节点数最大的那个子节点即为该节点的重子。
轻子:设有一个节点,其所有节点中,除重子外的节点均为轻子。
重边:某个节点与其重子的连边。
轻边:某个节点与其轻子的连边。
重链:仅由重边组成的一条链。
轻链:仅由轻边组成的一条链。
特别的,若某个点有多个重子,任选其一;若某个点不存在子节点,则不存在重子和轻子。
树剖本质上就是一种对暴力的优化,它将维护树上的一些条链各种信息。
以上面的问题为例。
我们先对每一个节点做编号,以重子优先做 dfs,新的节点编号即为 dfn。这样就可以保证重链上节点编号连续了。
当我们往上跳的时候,可以直接跳过所在重链,跳过的这一段正好编号连续,随便拿个数据结构就能维护。
操作时,若两点不在同一个重链上,则一定可以通过某种跳法使得两点并到同一条重链上。为了避免某个点跳完了而另一个点还没开始跳,我们总是希望他们深度差尽可能地小。即:总是让跳完后深度更深的那个点跳。
考虑不严谨的分析复杂度。暴力的复杂度会被链这种恶心的特殊形态卡飞,而引入重链后,会发现链上的话重链不会在这里浪费过多的时间,直接跳到分叉上面去。而跳的次数最坏是 \(O(\log n)\) 次。算上维护的数据结构的复杂度(这里默认是线段树)是 \(O(n\log n+q \log^2n)\)。
给出部分实现:
class tr_cut{
public:
int dep[1000086];//深度
int fa[1000086];//父亲节点
int sz[1000086];//子树大小
int hson[1000086];//重子
int id[1000086];//新的编号
int nw[1000086];//新的编号对应的权值
int top[1000086];//跳过所在重链后到达的位置
int point;//编号
void ini(){
memset(dep,0,sizeof dep);
memset(fa,-1,sizeof fa);
memset(sz,0,sizeof sz);
memset(hson,-1,sizeof hson);
memset(id,0,sizeof id);
memset(nw,0,sizeof nw);
memset(top,0,sizeof top);
point=0;
}
void dfs(int x,int fath){//第一次搜索,确定重子,顺手计算深度
fa[x]=fath;
dep[x]=dep[fa[x]]+1;
sz[x]=1;
int hs=0;
for(int i:e[x])
if(i!=fath){
dfs(i,x);
sz[x]+=sz[i];
if(sz[i]>sz[hs])
hson[x]=hs=i;
}
return;
}
void afs(int x,int tp){//第二次,按照重子优先编号,维护top
id[x]=++point;
nw[point]=a[x];
top[x]=tp;
if(hson[x]==-1)
return;
afs(hson[x],tp);
for(int i:e[x])
if(i!=fa[x] and i!=hson[x])
afs(i,i);
return;
}
}tr;
//封装好的树剖
顺便提一嘴,若对 \(x\) 的子树内进行修改,那么是对新编号中 \(id_x\) 到 \(id_x+sz_x-1\) 进行修改。这样考虑:以 \(x\) 为根的子树内的节点没有遍历完前不会离开该子树,而该子树内共有 \(sz_x\) 个节点,去除掉根节点即 \(id_x\) 到 \(id_x+sz_x-1\)。
写树剖要做好略大码量,花略长时间的准备