写的很良心的一篇博客。。。
这是一道很基础的树链剖分的题,其中涵盖了线段树的一些操作(区间求和,区间修改)
https://www.luogu.org/problemnew/show/P3384
题目描述
如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和
输入输出格式
输入格式:
第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。
接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。
接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)
接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:
操作1: 1 x y z
操作2: 2 x y
操作3: 3 x z
操作4: 4 x
输出格式:
输出包含若干行,分别依次表示每个操作2或操作4所得的结果(对P取模)
输入输出样例
输入样例#1:
5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3
输出样例#1:
2
21
说明
时空限制:1s,128M
数据规模:
对于30%的数据: N \leq 10, M \leq 10N≤10,M≤10
对于70%的数据: N \leq {10}^3, M \leq {10}^3N≤103,M≤103
对于100%的数据: N \leq {10}^5, M \leq {10}^5N≤105,M≤105
写在前面
要想做对这道题,你需要掌握的知识有:
- 前向星存邻接表or vector
- 树链剖分求LCA
- 线段树的区间修改
- 线段树的区间求和
具体操作
那么一段一段的看吧
首先存储部分比较简单,我就简单讲一个vector,vector是个动态数组,具体实现在我之前的博客中讲到过-->vector
vector<int> q[N];
for(i=1;i<n;++i){
int x,y;
x=read();y=read();//读边
q[x].push_back(y);
q[y].push_back(x);
}
接着树链剖分的预处理
需要存储7个量
fa[i] i在树中的father节点
son[i] i在树中的重儿子
size[i] 以i为根的子树的总节点数(包括i自己)
dep[i] i在树中的深度(求lca时有用)
idx[x] 序列中x位置对应的树中节点编号
pos[i] i在序列中的位置(和idx互逆)
top[i] i所在的重链的顶端节点编号
两种方法,一种是两遍dfs,另一个是用bfs+队列
bfs+队列
void init(int sta){
//把树剖分成一条条的链,组合在一起就变成序列了,然后就可以用线段树进行维护
int i,j,k;
que[qn=1]=sta;
dep[sta]=1;
for(k=1;k<=qn;++k){
int u=que[k];
sum[u]=a[u];size[u]=1;
for(i=0;i<q[u].size();++i)
{
int v=q[u][i];
if(v==fa[u]) continue;
fa[v]=u;dep[v]=dep[u]+1;
que[++qn]=v;
}
}
for(i=qn;i>=2;--i){//不能执行到i=1,会RE的 ,fa[que[1]]=-1,所以sum,size数组下标越界
int u=que[i],v=fa[u];
sum[v]+=sum[u];
size[v]+=size[u];
if(size[u]>size[son[v]]){
son[v]=u;
}
}
for(i=1;i<=qn;++i){
int u=que[i];
if(top[u]) continue;
for(int v=u;v;v=son[v]){
idx[pos[v]=++tot]=v;
top[v]=u;
}
}
}
两遍dfs
但由于这道题有一个修改子树的值和求子树的和的操作。
所以为了方便处理我们得把同一棵子树里的节点放在序列连续的一段里,那么就只能用dfs了(如果觉得不好理解,自己手动推,模拟代码的实现过程,很多时候你不理解的地方在推的过程中就会恍然大悟)
void dfs1(int u)//第一遍dfs求fa,size,son,dep
{
size[u]=1;
int i,j,k;
for(i=0;i<q[u].size();++i){
int v=q[u][i];
if(v!=fa[u]){
fa[v]=u;dep[v]=dep[u]+1;
dfs1(v);
size[u]+=size[v];
if(size[v]>size[son[u]]) son[u]=v;
}
}
}
void dfs2(int u){//第二遍dfs求top,idx,pos
if(son[u]!=0){//如果有重儿子,首先搜索重儿子,保证重链在序列中是连续的
int v=son[u];
idx[pos[v]=++tot]=v;
top[v]=top[u];
dfs2(v);
}
int i,j;
for(i=0;i<q[u].size();++i)
{
int v=q[u][i];
if(v!=fa[u]&&v!=son[u]){// 是&&,而不是||,注意逻辑关系
idx[pos[v]=++tot]=v;
top[v]=v;
dfs2(v);
}
}
}
(这个预处理也真是麻烦)
预处理完后,在处理出来的序列上做线段树
先build一个线段树出来(我写的有点麻烦,一般大佬都喜欢用宏命令来干(e.g.#define ,const等))
void build(int k,int l,int r){
if(l==r){
sum[k]=a[idx[l]];
if(sum[k]>=p) sum[k]-=p; //这样取模比用%快
return ;
}
int mid=l+r>>1;
build(2*k,l,mid);
build(2*k+1,mid+1,r);
sum[k]=sum[2*k]+sum[2*k+1];
sum[k]%=p;
}//在序列上建立线段树
然后就操作咯
对于操作1将树从x到y结点最短路径上所有节点的值都加上z
如果直接在树上操作,我们要树链剖分干什么,树链剖分就是将树分为一条条的链,把树上的操作变成序列上的,然后就可以用我们熟悉的小清新线段树了。还记得我们刚刚预处理的部分吗,现在就派上大用场了。
x所在重链的顶端如果和y的top不一样,说明他们不在一条重链上,那么就把深度更深的往上跳,并且对于这条重链对应的在线段树上进行区间修改,直到x,y在同一条重链上
void modify(int k,int l,int r,int x,int y,int z){
if(x<=l&&r<=y){
laz[k]+=z;
sum[k]=(sum[k]+(r-l+1)*z)%p;
return ;
}
int mid=l+r>>1;
if(laz[k]) pushdown(k,r-l+1);//标记下传,线段树区间修改的必备操作
if(x<=mid) modify(2*k,l,mid,x,y,z);
if(y>mid) modify(2*k+1,mid+1,r,x,y,z);
sum[k]=(sum[2*k]+sum[2*k+1])%p;//每次修改后别忘了更新
}
void modifypath(int u,int v,int k){//划分为几条重链
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
modify(1,1,n,pos[top[u]],pos[u],k);
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
modify(1,1,n,pos[u],pos[v],k);
}
然后操作2计算树从x到y结点最短路径上所有节点的值之和
和刚刚的操作一模一样,将x~y划分成几条重链,然后计算每个区间的和
int querysum(int k,int l,int r,int x,int y){
if(x<=l&&r<=y){
return sum[k];
}
int res=0,mid=l+r>>1;
if(laz[k]) pushdown(k,r-l+1);
if(x<=mid) res=(res+querysum(2*k,l,mid,x,y))%p;
if(y>mid) res=(res+querysum(2*k+1,mid+1,r,x,y))%p;
return res;
}//询问x~y区间的和
int pathsum(int u,int v){
int sum=0;
while(top[u]!=top[v]){//判断是否在同一重链
if(dep[top[u]]<dep[top[v]]) swap(u,v);//每次跳深度更深的
sum=(sum+querysum(1,1,n,pos[top[u]],pos[u]))%p;//querysum就是小清新线段树干的事
u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
sum=(sum+querysum(1,1,n,pos[v],pos[u]))%p;
return sum;
}
最后操作3,4其实和1,2差不多,只是多了一个转化的过程
输入的是某个根,而要在寻找它的子树是很方便的,因为我们dfs就考虑了这个部分
x这个根在序列中的位置是pos[x],而包含它整棵子树的区间 右端点 的位置则是pos[x]+size[x]-1
然后对于3,就modify(1,1,n,pos[x],pos[x]+size[x]-1,k) //k为要添加的值
对于4,就querysum(1,1,n,pos[x],pos[x]+size[x]-1)
最后大功告成~~~AC的完整代码我就不放上来了,大家消化后自己敲吧,(毕竟主要的代码已经分块放上来了)
备注:线段树的大小开4倍哦,不然会RE