Party for You Started(中国计量大学校赛)
树链剖分+线段树维护区间
题意: 输入n,m,表示n个结点,接下来输入a[i]表示第i个结点的父节点为a[i],m次询问,每次输入u,x,v,输出从v节点到n结点的权值,再修改u结点的权值。
比赛的时候觉得题意挺好的懂的,当时只想着dfs暴力修改,果然TLE了。比赛结束后才知道是道树链剖分裸题,当晚就去学了,找了一篇博客,请教了实验室学长,差不多花了2个小时,总算理解了树链剖分,贴一下dalao的博客。
说一下自己的理解吧,树链剖分首先要理解重儿子、轻儿子、重边、轻边、重链、轻链,主要变量有father[]、dis[]、size[]、son[]、rank[]、top[]、id[]数组,分别表示改结点父亲结点,该结点深度,以该结点为跟子树的结点数,该结点的重儿子。
最重要的就是id数组,我的理解的他的作用是将一棵树的结点,按重链的顺序标号,所以一条重链上的id是连续的,所以线段树维护的是id数组,区间求和可看之后get_sum函数,这要好好理解(我太菜,看了快一小时)。
top数组表示该结点所在重链上的起点,作用是区间求和时用来加速,从一条重链跳到另外一条重链,因为top可以直接跳转到该重链的起始结点,轻链没有起始结点之说,他们的top就是自己
关键数组得到方式:两个dfs
void dfs1(int u,int fa,int depth) //当前节点、父节点、层次深度
{
f[u]=fa;
d[u]=depth;
size[u]=1; //这个点本身size=1
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(v==fa)
continue;
dfs1(v,u,depth+1); //层次深度+1
size[u]+=size[v]; //子节点的size已被处理,用它来更新父节点的size
if(size[v]>size[son[u]])
son[u]=v; //选取size最大的作为重儿子
}
}
void dfs2(int u,int t) //当前节点、重链顶端
{
top[u]=t;
id[u]=++cnt; //标记dfs序
rk[cnt]=u; //序号cnt对应节点u
if(!son[u])
return;
dfs2(son[u],t);
/*我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续,
一个点和它的重儿子处于同一条重链,所以重儿子所在重链的顶端还是t*/
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(v!=son[u]&&v!=f[u])
dfs2(v,v); //一个点位于轻链底端,那么它的top必然是它本身
}
}
个人感受:树链剖分最难难在理解,怎么从一颗树联系到数组的区间?树上路径的权值和怎么从得到的id数组中得到? 可以看以下代码
//仔细看,最好根据博客连接里的图,难在理解
int sum(int x,int y)
{
int ans=0,fx=top[x],fy=top[y];
while(fx!=fy) //两点不在同一条重链
{
if(d[fx]>=d[fy])
{
ans+=query(id[fx],id[x],rt); //线段树区间求和,处理这条重链的贡献
x=f[fx],fx=top[x]; //将x设置成原链头的父亲结点,走轻边,继续循环
}
else
{
ans+=query(id[fy],id[y],rt);
y=f[fy],fy=top[y];
}
}
//循环结束,两点位于同一重链上,但两点不一定为同一点,所以我们还要统计这两点之间的贡献
if(id[x]<=id[y])
ans+=query(id[x],id[y],rt); //同一条重链里的id下标表示连续的
else
ans+=query(id[y],id[x],rt);
return ans;
}
好好理解了上述代码,应该就知道树链剖分的原理了,类似将树上的路径分段连续相加,对于中国计量校赛A题,因为询问到根结点的距离,所以上述代码中的y结点一定在x结点上方,所以可以有以下更改
ll get_sum(int x){ //相当于分段加连续区间
ll ans=0;
while(x!=0){ //询问到根结点结束
ans+=Query(id[top[x]],id[x],1,n,1); //ans加上从该结点到该结点所在重链头结点的值
x=f[top[x]]; // [该重链起点,该结点]之和已经算过,现在要求该重链起点的父节点到根结点权值
}
return ans;
}
重要的数组从几个简单的dfs中可以得到,建议手动模拟,加深理解,得到的id数组用线段树维护,学长告诉我,理解树链剖分之后难点就在于各种线段树了,下面贴下计量校赛A题代码:
#include<iostream>
#include<vector>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<set>
#include<algorithm>
#include<queue>
#include <map>
#include<stack>
#include<list>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll,ll> P;
const int maxn=55;
const ll mod=1e9+7;
struct edge{ //链式前向星
int next,to;
}e[2*maxn];
int rt,n,m,r,a[maxn],cnt,head[maxn],f[maxn],d[maxn],size[maxn],son[maxn],rk[maxn],top[maxn],id[maxn];
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
ll Sum[maxn<<2],Add[maxn<<2];//Sum求和,Add为懒惰标记
ll A[maxn];//存原数组数据下标[1,n]
//(1)建树:
//PushUp函数更新节点信息 ,这里是求和
void PushUp(int rt){
Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];
}
//Build函数建树
void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号
if(l==r) {//若到达叶节点
Sum[rt]=A[l];//储存数组值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新信息
PushUp(rt);
}
//(2)点修改:
//假设A[L]+=C:
void Update(int L,ll C,int l,int r,int rt){//l,r表示当前节点区间,rt表示当前节点编号
if(l==r){//到叶节点,修改
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根据条件判断往左子树调用还是往右
if(L <= m) Update(L,C,l,m,rt<<1);
else Update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子节点更新了,所以本节点也需要更新信息
}
//首先是下推标记的函数:
void PushDown(int rt,int ln,int rn){
//ln,rn为左子树,右子树的数字数量。
if(Add[rt]){
//下推标记
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;
Sum[rt<<1|1]+=Add[rt]*rn;
//清除本节点标记
Add[rt]=0;
}
}
//(4)区间查询:
//询问A[L,R]的和
//然后是区间查询的函数:
ll Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){
//在区间内,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记,否则Sum可能不正确
PushDown(rt,m-l+1,r-m);
//累计答案
ll ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}void init(){
memset(head,-1,sizeof(head));
cnt=0;
}
void add_edge(int u,int v){
e[cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt++;
}
void dfs1(int u,int fa,int depth) //当前节点、父节点、层次深度
{
f[u]=fa;
d[u]=depth;
size[u]=1; //这个点本身size=1
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(v==fa)
continue;
dfs1(v,u,depth+1); //层次深度+1
size[u]+=size[v]; //子节点的size已被处理,用它来更新父节点的size
if(size[v]>size[son[u]])
son[u]=v; //选取size最大的作为重儿子
}
}
void dfs2(int u,int t) //当前节点、重链顶端
{
top[u]=t;
id[u]=++cnt; //标记dfs序
rk[cnt]=u; //序号cnt对应节点u
if(!son[u])
return;
dfs2(son[u],t);
/*我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续,
一个点和它的重儿子处于同一条重链,所以重儿子所在重链的顶端还是t*/
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(v!=son[u]&&v!=f[u])
dfs2(v,v); //一个点位于轻链底端,那么它的top必然是它本身
}
}
ll get_sum(int x){ //相当于分段加连续区间
ll ans=0;
while(x!=0){
ans+=Query(id[top[x]],id[x],1,n,1);
x=f[top[x]];
}
return ans;
}
int main()
{
scanf("%d %d",&n,&m);
int root;
init();
for(int i=1;i<=n;i++){
int x;
cin>>x;
if(x==0) root=i;
else add_edge(i,x),add_edge(x,i);
}
cnt=0,dfs1(root,0,1),dfs2(root,root);
cnt=0,Build(1,n,1);//对id数组建树
while (m--){
int u,v;
ll x;
scanf("%d %lld %d",&u,&x,&v);
cout<<get_sum(v)<<'\n';
Update(id[u],x,1,n,1); //要求修改u结点权值,对应维护的数组为id[u]
}
return 0;
}
以上是我自己对树链剖分的理解,刚刚接触,如果有错误希望指出,希望下次遇到类似的题目能过.不要再垫底了,呜呜呜