Party for You Started(中国计量大学校赛)

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;
}

以上是我自己对树链剖分的理解,刚刚接触,如果有错误希望指出,希望下次遇到类似的题目能过.不要再垫底了,呜呜呜

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值