先来说一下树链剖分能解决什么样的问题:
假如我们有一棵树,树上每个节点都有对应的权值,我们想要修改u到v上所有点的权值,使其加k,这我们应该怎样实现呢?假如在一段区间上进行修改我们知道这个可以用线段树来实现,可是现在这是在树上,无法直接用线段树进行修改,我们就要想办法把树按照某种规则变成一个序列,而这就是树链剖分所要讲述的东西:
树链剖分的核心就是把一棵树上所有的节点转化为一个序列,当然树的前序遍历中序遍历以及后序遍历都可以实现这一目的,但是我们今天所讲的是另外一种转换方式,如果我们能把树中任意两个节点之间的路径转换为我们所求序列中几段连续的区间我们不就可以用线段树来实现区间修改以及查询了吗?而树链剖分的实现方式可以保证我们把树中任意两点之间的路径转换为不超过logn段序列里面的连续区间,其中n为树的节点个数
下面先对树链剖分中的一些基本概念进行解释:
每一个非叶子节点均有一个重儿子和轻儿子,两者是以以儿子为根的子树中节点个数来区分的,这就要求我们一定要先求出一每个节点为根的子树中的节点个数,子树中包含节点数多的儿子就是重儿子,另一个就是轻儿子,如果两个儿子子树中的节点数目一样多,那么随机选择一个儿子节点作为重儿子即可,重边是重儿子与其根节点之间的连边,图中连边除了重边均是轻边,重链就是重边组成的极大连边,也就是说如果两个重边有公共端点,那么他们一定是在一个重链里面。
有了上述定义之后,我们可以发现任何一个节点都会在一个重链里面,而且只会在一个重链里面,即使他是个叶子节点且不是重儿子,那么默认他自己就是一个重链,而且重儿子是在与其父节点连边的重链里面,而轻儿子是在以其为始点向下的重链里面,换句话说轻儿子是重链的开头。
据图分析:2和3是1的两个节点,由于以2为根的子树中节点个数更多,所以2就是重儿子而3就是轻儿子,同理可得4,6,9,12也是重儿子,而10和11谁是重儿子都可以,由重边的定义我们可以知道1和2之间的是重边,2和4之间的是重边,4和9之间的是重边,9和12之间的是重边,3和6以及6和10之间的也是重边,至于重链,1-2-4-9-12是一条重链,3-6-10是一条重链,5,7,8,11各是一条重链。
有了上面的基础,我们来说一下树链剖分是按照什么原则将一棵树转变为序列的,就是优先遍历当前节点的重儿子,其次遍历当前节点的轻儿子,然后依次重新给树中的节点进行重新编号,依旧拿上面那幅图来举例,其dfs序就是1-2-4-9-12-8-5-3-6-10-11-7,按照这样一个原则进行dfs序构造我们就容易发现一个性质,就是位于一条重链上的节点编号是连续的,这也是我们之后可以用其进行区间修改的一个基础。
我们还需要让每个节点存储他所在重链的头节点,也就是重链中深度最小的节点的编号,这里我们又可以发现所有重链的头节点都不是重儿子,换句话说,轻儿子所在重链的头节点一定是自身,有了这个性质,我们就不难发现标记每个节点所在重链头节点是通过dfs实现的,这个比较容易实现,详情可以见代码。
最后我们就需要讲一下如何将两个节点之间的路径转换为若干段构造序列中的连续区间了。其中每一段区间都是一条重链的子集,这里的实现方式就有点像求最近公共祖先的实现方式了,就是让两个节点一次一次地往上爬,每次都是爬一个重链长度,直至两个节点在一个重链里面,这样我们就可以把两个点之间的路径分成若干条重链,然后由于重链节点之间的编号都是连着的,我们就可以直接用线段树进行区间修改和查询,这样对于每次修改我们对应的复杂度都是n(logn)^2,具体实现方式如下:
我们依次比较u和v的所在重链头节点深度,每次就让所在重链头节点深度较大的点爬至该重链头节点的父亲节点处,然后我们可以对其所爬过的这一条重链进行修改或者查询,重复进行上面操作直至两个节点在同一条重链上,这个时候我们就可以直接通过修改重链的部分来对最后的一部分区间进行修改和查询,这样我们就完成了对整个路径上的修改和查询。
下面给出一道题目,其中涉及到修改和查询以某个节点为根的子树中的所有节点值,这个我在之前的某道题目中涉及到过这类问题的处理方法,因为我们是通过dfs序进行编号的,这就会有一个比较好的性质,那就是以某个节点为根的子树中的所有节点的编号是连续的,而且编号最小值就是根节点的编号,编号最大值就是根节点的编号+该子树中节点总数-1,有了这个东西那么对于查询或者修改以某个节点为根的子树中的所有节点值就比较容易了,下面直接给出题目和相应代码:
输入样例:
5
1 3 7 4 5
1 3
1 4
1 5
2 3
5
1 3 4 3
3 5 4
1 3 5 10
2 3 5
4 1
输出样例:
16
69
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
using namespace std;
typedef long long ll;
const int N=4e5+10;
int h[N],e[N],ne[N],w[N],idx;
ll l[N],r[N],sum[N],lazy[N];
int sz[N],d[N],newid[N],fa[N],nw[N];//newid记录dfs序后节点编号,nw[i]记录dfs序后节点权值
int top[N];//top[i]为i所在重链上深度最低的点的编号
int son[N];//son[i]标记i节点的重儿子
int cnt;//记录dfs序编号
void add(int x,int y)
{
e[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
}
//先求出dfs序
void dfs1(int x,int father,int depth)
{
d[x]=depth;
fa[x]=father;
sz[x]=1;
for(int i=h[x];i!=-1;i=ne[i])
{
int j=e[i];
if(j==father) continue;
dfs1(j,x,depth+1);
sz[x]+=sz[j];
if(sz[son[x]]<sz[j]) son[x]=j;//记录x的重儿子
}
}
//记录每个节点所在重链中深度最小的点
void dfs2(int x,int t)//t为x所在重链的头节点
{
newid[x]=++cnt;top[x]=t;nw[cnt]=w[x];
if(!son[x]) return ;//没有儿子节点,有儿子节点就一定会有重儿子
dfs2(son[x],t);//先遍历每个节点的重儿子
for(int i=h[x];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa[x]||j==son[x]) continue;
dfs2(j,j);//轻儿子所在重链的头节点就是自身
}
}
//下面是线段树部分
void pushup(int id)
{
sum[id]=sum[id<<1]+sum[id<<1|1];
}
void pushdown(int id)
{
if(lazy[id])
{
sum[id<<1]+=(r[id<<1]-l[id<<1]+1)*lazy[id];
sum[id<<1|1]+=(r[id<<1|1]-l[id<<1|1]+1)*lazy[id];
lazy[id<<1]+=lazy[id];
lazy[id<<1|1]+=lazy[id];
lazy[id]=0;
}
}
void build(int id,int L,int R)
{
l[id]=L;r[id]=R;
if(L==R)
{
sum[id]=nw[L];
return ;
}
int mid=L+R>>1;
build(id<<1,L,mid);
build(id<<1|1,mid+1,R);
pushup(id);
}
void update_interval(int id,int L,int R,int val)
{
if(l[id]>=L&&r[id]<=R)
{
lazy[id]+=val;
sum[id]+=val*(r[id]-l[id]+1);
return ;
}
pushdown(id);
int mid=l[id]+r[id]>>1;
if(L<=mid) update_interval(id<<1,L,R,val);
if(mid+1<=R) update_interval(id<<1|1,L,R,val);
pushup(id);
}
ll query_interval(int id,int L,int R)
{
if(l[id]>=L&&r[id]<=R) return sum[id];
pushdown(id);
int mid=l[id]+r[id]>>1;
ll ans=0;
if(mid>=L) ans+=query_interval(id<<1,L,R);
if(mid+1<=R) ans+=query_interval(id<<1|1,L,R);
return ans;
}
void update_path(int u,int v,int val)//修改u到v之间的路径
{
while(top[u]!=top[v])
{
if(d[top[u]]<d[top[v]]) swap(u,v);//保证top[u]的深度大
//一定要注意更新区间左右边界大小值,深度小的节点编号小
update_interval(1,newid[top[u]],newid[u],val);
u=fa[top[u]];
}
//此时u和v在一个重链上面
if(d[u]<d[v]) swap(u,v);
update_interval(1,newid[v],newid[u],val);
}
ll query_path(int u,int v)//询问u到v之间的路径权值和
{
ll ans=0;
while(top[u]!=top[v])
{
if(d[top[u]]<d[top[v]]) swap(u,v);
ans+=query_interval(1,newid[top[u]],newid[u]);
u=fa[top[u]];
}
if(d[u]<d[v]) swap(u,v);
ans+=query_interval(1,newid[v],newid[u]);
return ans;
}
void update_tree(int u,int val)//修改以u为节点的子树上的权值
{
update_interval(1,newid[u],newid[u]+sz[u]-1,val);
}
ll query_tree(int u)//询问以u为节点的子树上的权值和
{
return query_interval(1,newid[u],newid[u]+sz[u]-1);
}
int main()
{
int n;
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++)
scanf("%d",&w[i]);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
dfs1(1,-1,1);//预处理出来所有节点的子树中节点个数以及重儿子
dfs2(1,1);//记录每个节点所在重链的头节点
build(1,1,n);
int q;
cin>>q;
while(q--)
{
int op,u,v,k;
scanf("%d",&op);
if(op==1)
{
scanf("%d%d%d",&u,&v,&k);
update_path(u,v,k);
}
else if(op==2)
{
scanf("%d%d",&u,&k);
update_tree(u,k);
}
else if(op==3)
{
scanf("%d%d",&u,&v);
printf("%lld\n",query_path(u,v));
}
else
{
scanf("%d",&u);
printf("%lld\n",query_tree(u));
}
}
return 0;
}