【算法学习】树链部分
说句闲话
这个东西已经20多天没写了,感觉已经忘没了,非常糟糕,只好赶快补一补,确实还是得多打代码,不然都忘光就丸辣。
前言
树链问题通常是关于树上路径的操作,将路径拆分成一条条链,然后用线段树维护链的权值。
注:本题解并不适用于毫无基础的oier,只是做简单讲解,想了解具体定义请自行查阅。
树链模板题
我们一下都围绕这道题讲述操作,我们需要实现:
-
两点路径权值加;
-
两点路径权值和;
-
节点子树权值加;
-
节点子树权值和;
2次dfs成链
第一次
我们先用第一次 dfs 找到每个节点的以下东西(不懂先放着,继续向下看):
-
节点深度:为了查路径时确定哪个作为起点终点及先动深度更深的点。
-
节点大小:一个子树是一个连续的dfs序,为了操作整颗子树。
-
父节点:当达到链顶时为了转移到另一个链上。
-
重儿子:为了形成重链。
void dfs1(int x,int f,int d){//当前节点,父节点,深度
deep[x]=d;//深度
siz[x]=1;//大小
fa[x]=f;//父节点
int mxson=-1;//重儿子大小
for(int y:v[x]){
if(y==f)
continue;
dfs1(y,x,d+1);
siz[x]+=siz[y];
if(siz[y]>mxson){//比当前已有重儿子更大,更新
son[x]=y;
mxson=siz[y];
}
}
}
第二次
这一次我们要形成树链,优先遍历重链,记录每个点以下内容:
-
dfs序赋新节点编号:为了线段树的节点标号。
-
新节点编号的权值储存:初始值转移过来。
-
记录链顶:为了求路径和,你先别急了解他。
void dfs2(int x,int topf){//当前节点,链顶节点
id[x]=++cnt;//dfs序
w[cnt]=aa[x];//初始化的值转移过来
top[x]=topf;//更新链顶
if(!son[x]){//叶子节点滚!
return;
}
dfs2(son[x],topf);//有点遍历重儿子形成重链
for(int y:v[x]){
if(y!=fa[x]&&y!=son[x]){
dfs2(y,y);//轻儿子链顶是自己
}
}
}
处理链
子树处理
因为优先遍历重儿子,所有重儿子上 dfs 序是连续,仔细想一下遍历过程就可以发现同一子树上的编号也是连续的,所以子树操作就变成了线段树区间加区间和。
具体如下:(省略了线段树操作)
change(1,1,n,id[x],id[x]+siz[id[x]]-1,k);
query(1,1,n,id[x],id[x]+siz[id[x]]-1);
路径处理
这个可以模拟一下,假设我们求 \(4,7\) 的路径上和,他们不在同一条链上,我们找到链顶深度更深的节点操作(7所在的链),那 \(7->6\) 这条路径上的权值直接加和,因为重链 dfs 序连续,所以还是线段树区间加和,此时我们节点要到** \(6\) 的父节点 \(1\) 上**,此时发现 \(1,4\) 在同一条重链上,那 \(1,4\) 的权值直接加和即可,还是线段树区间操作,此时路径操作就呼之欲出了!!!
int sumpath(int x,int y){
int ans=0;
while(top[x]!=top[y]){//不在同一条链上
if(deep[top[x]]<deep[top[y]]){//链顶更深!!!
swap(x,y);//交换
}
ans+=query(1,1,n,id[top[x]],id[y]);//链顶到当前节点
x=fa[top[x]];//到链顶的父节点上
}
if(deep[x]>deep[y]){//深度小到大的取值加和
swap(x,y);
}
ans+=query(1,1,n,id[x],id[y]);
return ans;
}
树链部分lca
可以发现我们不断地在链上向上跳,这于lca的操作非常相似,所以他是可以求 lca 的,我们不断向上跳到同一条链上,深度更浅的那个点就是两个节点的 lca。
int sumpath(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]]){
swap(x,y);
}
x=fa[top[x]];
}
if(deep[x]>deep[y]){
return y;
}
return x;
}
边权如何
P4315 月下“毛景树”
如果树的权值到边上成为边权呢,其实还是要转成点权,我们一个点对应一条边,那从根节点出发的边的边权就可以给他的儿子,这样就转成点权了。
当然不可避免的有很多问题,如最后在同一条链上时,要用changesum(1,1,n,id[x]+1,id[y],k);
因为根节点的权值是不算在内的。
单边修改时也要查找边对应的点x=id[e[x*2-1].to]<id[e[x<<1].to]?e[x<<1].to:e[x*2-1].to;
例题
P4427 [BJOI2018] 求和
树上差分的思想,预处理前缀和,从根到该节点k次方的总和,答案需要减去 lca 的路径