【模板】树链剖分思想及模板

最近临近NOIP,想想自己还有啥NOIP考的东东不会,后来发现,树链剖分算是一个很容易想到的暴力,但其实在NOIP中考的东东如果能用树剖,基本上都能用倍增LCA求解,而且倍增复杂度更优

进入正题

先引入一道例题:

题目大意:给定一棵树,有m次操作,每次操作有两种可能
1 1 u v v 之间的节点去全部加上一个数c
2 2 询问x号点的权值

这一看就是线段树模板题啊,但是变成了树上操作,那么问题就只剩下如何维护树上线段树,对于这个问题,好像仍没有什么高效解法,只剩下一个较为高效的方法,名曰:树链剖分

顾名思义,树链剖分就是一个将树剖分成链的操作,由于线段树中只能维护线性结构,但树型结构是由多条链组成的,所以我们将一棵树拆成数条链,并将这些链以任意顺序放进线段树中(等会儿会提到为啥是任意顺序),再维护线段树即可

先提提怎么更改 u u v的路径,也是把路径拆成与树拆成的链一样的链(也就是树中拆了哪些链,路径拆成哪些链),再对每条链进行线段树上修改

但随意拆树会导致链数量较多,维护起来较为分散,导致会进行较多次数的线段树区间修改,所以我们使用神奇的(神奇海螺)重边

下面引入概念:

重儿子: 当前节点的所有儿子节点中,以重儿子节点为子树的节点数最多
重边: 所有节点与其重儿子连成的边
重链: 以一个点开始,递归找到的所有重边组成的链
——摘自《神奇海螺百科》

上面的概念刚接触可能有一点点点点点点不好理解,多看几遍就好了

这样的结果就是所有的链的长度都尽可能地相近(迷?)

反正这样将路径分解出来的链尽可能少

这样我们就需要将重链求出来,将重链求出来就算预处理完毕了

至于求重链的方法,需要俩DFS,第一遍求重儿子重边,第二遍求重链

下面是两次DFS的代码:(求重儿子重边)

void dfs(int x,int father,int deep){
    depth[x]=deep;                      //记录深度
    dad[x]=father;                     //记录粑粑
    siz[x]=1;                         //记录以当前节点为根节点的子树节点数量
    for(rg int i=head[x];i;i=a[i].nxt)if(a[i].v!=father){
        dfs(a[i].v,x,deep+1);
        siz[x]+=siz[a[i].v];
        if(son[x]==-1||siz[a[i].v]>siz[son[x]])son[x]=a[i].v;  //找重儿子
    }
}

下面是第二次DFS:(求重链)


void dfs1(int x,int tp){     //tp是当前节点所在重链的最上层节点
    top[x]=tp;
    tip[x]=++tot;            //记录当前节点在线段树中的位置 :树节点->线段树
    rak[tip[x]]=x;           //记录线段树中相应位置的节点  :线段树->树节点
    if(son[x]!=-1)
        dfs1(son[x],tp);     //先处理当前的重链
    for(rg int i=head[x];i;i=a[i].nxt)
        if(a[i].v!=dad[x]&&a[i].v!=son[x])
            dfs1(a[i].v,a[i].v);//再处理以当前节点的非重儿子为顶的重链(其他重链)
}

现在可以回答前面的问题了,看完代码,发现链与链之间并没有什么关联,而且在线段树上的查询修改啥的都不会跨链,所以链之间在线段树上的相对位置是随意的,只要保证一条链中元素相对位置对应即可

然后就是如何将路径拆分了

由于同一条重链在线段树中的位置都是连续的,所以对于从 u u v之间经过的所有重链我们都要进行一次线段树上区间更改

下面讨论如何修改路径

x x y在同一重链中,即可直接在线段树中维护(一条重链在线段树中的位置是连续的)

至于如何快速拆分路径,我们就可以用到刚才第二次DFS时求出的 top t o p 数组加速拆分

至于如何拆分,就是若俩点不在一条重链中,将其中一个点拉到当前重链顶节点的爹爹
(比喻起来重链就是高速公路,非重链就是国道)

作为OIER看代码会更容易理解:

void add(int x,int y,int C){
    while(top[x]!=top[y]){                        //将俩节点提到同一重链中
        if(depth[top[x]]<depth[top[y]])swap(x,y);
        update(tip[top[x]],tip[x],C,1,n,1);    //将当前经过的的重链进行线段树维护
        x=dad[top[x]];    //神奇地加速寻找,直接提到重链顶端的爹爹,特别像高速公路
    }
    if(depth[x]>depth[y])swap(x,y);
    update(tip[x],tip[y],C,1,n,1);  //当前情况下俩点在同一重链中,直接线段树维护
}

看完这段代码,发现一条路径会被分为 log2n l o g 2 n 条重链(至于为什么,我也不会证,只能感性的想想,如果实在不能理解,为什么不问问神奇海螺呢?)神奇海螺表示:问度娘

然而每次对重链的线段树维护的复杂度是 O(log2n) O ( l o g 2 n ) ,所以m次操作的复杂度为 O(mlog22n) O ( m l o g 2 2 n ) ,再加上之前的预处理和线段树建树,总复杂度是 O(n+mlog22n+nlog2n) O ( n + m l o g 2 2 n + n l o g 2 n )

然鹅比LCA倍增的复杂度 O(nlog2n+mlog2n) O ( n l o g 2 n + m l o g 2 n ) 的复杂度高到不知道哪里去了,所以我还是选择倍增LCA (话说我为甚么要学树链剖分)

看模板很容易理解:(以下摘)

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iomanip>
#include<algorithm>
#include<cstdlib>
#include<climits>

using namespace std;
#define cl(x) memset(x,0,sizeof(x))
#define rg register
#define cl1(x) memset(x,-1,sizeof(x))

template <typename _Tp> inline void read(_Tp &x){
    char c11=getchar();x=0;bool booo=0;
    while(c11<'0'||c11>'9'){if(c11=='-')booo=1;c11=getchar();}
    while(c11>='0'&&c11<='9'){x=x*10+c11-'0';c11=getchar();}
    if(booo)x=-x;
}

const int maxn=50050;
int n,q;
int siz[maxn],depth[maxn],dad[maxn],son[maxn],tot=0,s[maxn];
int top[maxn],tip[maxn],rak[maxn],sum[maxn<<2],lazy[maxn<<2];
struct node{int v,nxt;}a[maxn<<1];
int head[maxn],p=0;

inline void add_edge(int,int);

void init();

void dfs(int x,int father,int deep){
    depth[x]=deep;
    dad[x]=father;
    siz[x]=1;
    for(rg int i=head[x];i;i=a[i].nxt)if(a[i].v!=father){
        dfs(a[i].v,x,deep+1);
        siz[x]+=siz[a[i].v];
        if(son[x]==-1||siz[a[i].v]>siz[son[x]])son[x]=a[i].v;
    }
}

void dfs1(int x,int tp){
    top[x]=tp;
    tip[x]=++tot;
    rak[tip[x]]=x;
    if(son[x]!=-1)
        dfs1(son[x],tp);
    for(rg int i=head[x];i;i=a[i].nxt)
        if(a[i].v!=dad[x]&&a[i].v!=son[x])
            dfs1(a[i].v,a[i].v);
}

inline void pushup(int x){sum[x]=max(sum[x<<1],sum[x<<1|1]);}

inline void pushdown(int x,int len){
    if(lazy[x]){
        lazy[x<<1]+=lazy[x];
        lazy[x<<1|1]+=lazy[x];
        sum[x<<1]+=(len-(len>>1))*lazy[x];
        sum[x<<1|1]+=(len>>1)*lazy[x];
        lazy[x]=0;
    }
}

void build(int l,int r,int x){
    lazy[x]=0;
    if(l==r){sum[x]=s[rak[l]];return ;}
    int mid=(l+r)>>1;
    build(l,mid,x<<1);
    build(mid+1,r,x<<1|1);
    pushup(x);
}

int query(int l,int r,int x,int X){
    if(l==r)return sum[x];
    pushdown(x,r-l+1);
    int mid=(l+r)>>1;
    int temp=0;
    if(X<=mid)  temp=query(l,mid,x<<1,X);
    else    temp=query(mid+1,r,x<<1|1,X);
    pushup(x);
    return temp;
}

void update(int L,int R,int ADD,int l,int r,int x){
    if(L<=l && r<=R){
        lazy[x]+=ADD;
        sum[x]+=ADD*(r-l+1);
        return ;
    }
    pushdown(x,r-l+1);
    int mid=(l+r)>>1;
    if(L<=mid)  update(L,R,ADD,l,mid,x<<1);
    if(mid<R)   update(L,R,ADD,mid+1,r,x<<1|1);
    pushup(x);
}

void add(int x,int y,int C){
    while(top[x]!=top[y]){
        if(depth[top[x]]<depth[top[y]])swap(x,y);
        update(tip[top[x]],tip[x],C,1,n,1);
        x=dad[top[x]];
    }
    if(depth[x]>depth[y])swap(x,y);
    update(tip[x],tip[y],C,1,n,1);
}

int main(){
    while(~scanf("%d",&n)){
        init();
        dfs(1,0,0);
        dfs1(1,1);
        build(1,n,1);
        char ps[10];
        while(q--){
            scanf("%s",ps);
            int A,B,C;
            if(ps[0]=='Q'){read(A);printf("%d\n",query(1,n,1,tip[A]));}
            else {read(A);read(B);read(C);if(ps[0]=='D')C=-C;add(A,B,C);}
        }
    }
    return 0;
}

void init(){
    read(n);++n;read(q);tot=0,p=0;
    cl(head);cl1(son);cl(lazy);
    for(rg int i=1;i<=n;++i)read(s[i]);
    int A,B;
    for(rg int i=1;i<n;++i){
        read(A);read(B);
        add_edge(A,B);add_edge(B,A);
    }
}

inline void add_edge(int u,int v){
    a[++p].v=v;
    a[p].nxt=head[u];
    head[u]=p;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值