别样的树分治——重链分治
树重链分治
引入
在模拟赛中遇到了一道树上问题,经题意转化后需要解决查询树上距离为
我们考虑点分治为什么效率低,原因是每轮只取出一个点取出后删掉,但如果我们一次取出一整条链上的点那就不一样了。
为了维持点分治的复杂度,我们每次挑选从根开始的一条重链,由于重心一定在重链上,所以复杂度不会劣于普通点分治。
流程
以
- 对于当前连通块
U ,找出当前连通块所对应的一条从根到叶子的重链D_U 。 - 我们按从上到下的顺序遍历这条链,获取链头到当前节点
u 的路径长度pre_u ,查询桶中k-pre_u 的数量,将pre_u 加入桶。 - 遍历
u 不在链上的儿子v ,进入v 的子树,获取对于子树节点x 到u 的距离d_u ,查询桶中=k-d_x-pre_u 的数量。 - 将
v 子树内所有点的d_x+pre_u 加入桶。 - 按此流程处理完整条链,并删除链上的所有节点,向子结点递归。
代码示例
bool del[N];
int sz[N],hs[N],pre[N];
void dfs(int u,int last){
sz[u]=1;hs[u]=0;
for(auto i:g[u]){
if(i==last||del[i])continue;
dfs(i,u);
if(sz[i]>sz[hs[u]])hs[u]=i;
sz[u]+=sz[i];
}
}
int t[N<<1],mem[N],ed;
ll ans=0;
void getval(int u,int last){
pre[u]=pre[last]+(u<=n);
if(u<=n){
ans+=t[L-pre[u]+N];
mem[++ed]=u;
}
for(auto i:g[u])if(i!=last&&!del[i])getval(i,u);
}
void solve(int u){
dfs(u,0);
for(int i=u;i;i=hs[i])del[i]=1;
int fa=0;
for(int i=u;i;i=hs[i]){
pre[i]=pre[fa]+(i<=n);
if(i<=n){
ans+=t[L-pre[i]+N];
t[N-pre[fa]]++;
}
for(auto v:g[i]){
if(del[v])continue;
ed=0;
getval(v,i);
for(int j=1;j<=ed;j++){
int x=mem[j];
t[pre[x]-pre[i]-pre[fa]+N]++;
}
}
fa=i;
}
for(int i=N-sz[u];i<=N+sz[u];i++)t[i]=0;
for(int i=u;i;i=hs[i]){
for(auto j:g[i]){
if(!del[j])solve(j);
}
}
}
进一步分析
回顾流程我们发现实际上链分治的流程包含了点分治,意味着链分治往往不如点分治灵活好写,仅用于优化时间。
实际上我们无需将这个过程类似点分治动态找重心,可以在一开始就进行重链剖分,其余步骤不变,这样我们便可以发现三条性质:
- 整个分治的层数虽然也是对数级别,但是常数几乎等于跳重链的时间常数,这是极其优秀的时间了。
- 链分治也可以改写为类似于点分树的动态结构,当我们修改一个点时只需要向上跳重链并更新沿途的重链即可,但考虑到链上部分的更新较为复杂(例如用线段树维护链上最大子段和等查询)并且实现相比静态较为困难,表现往往不如普通点分树。
- 动态链分治支持路径的修改与查询,常规的树链剖分就是一种动态链分治。
后记
重链分治是作者在训练中无意发现的一种算法,可能有前人提出过也有可能较为 useless,但是由于链分治可以有效针对常规卡点分治的数据点(例如树高较高,每层节点少),在紧急情况下或有妙用(?。