树链剖分

简述

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。
具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。
树链剖分(树剖/链剖)有多种形式,如重链剖分,长链剖分和用于 \(Link/cut\space Tree\) 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分”都指“重链剖分”。

重链剖分

重链剖分可以将树上的任意一条路径划分成不超过 \(O(\log n)\) 条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。
重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。

如:

  1. 修改树上两点之间的路径上所有点的值。
  2. 查询树上两点之间的路径上节点权值的和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)。

定义

  • 重儿子:对于每一个非叶子节点,它的儿子中 儿子数量最多的那一个儿子 为该节点的重儿子
  • 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子
  • 叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)
  • 重边:连接任意两个重儿子的边叫做重边
  • 轻边:剩下的即为轻边
  • 重链:相邻重边连起来的 连接一条重儿子 的链叫重链
  • 对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
  • 每一条重链以轻儿子为起点

code

namespace qwq{ //树剖部分
    //第一个dfs(init)记录父节点(fa)、深度(dep)、子树大小(sz)、重子节点(son)
    int dep[N],fa[N],son[N],sz[N];	
    il void init(int u,int pre){ 
        int mxson=-1;
        dep[u]=dep[pre]+1,fa[u]=pre,sz[u]=1;
        for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
            int v=edge::e[i].v;
            if(v==pre) continue;
            init(v,u),sz[u]+=sz[v];
            if(sz[v]>mxson) mxson=sz[v],son[u]=v;
        }
        return;		
    }
    //第二个dfs(getnum)记录所在链的链顶(top)、重边优先遍历的dfs序(dfn)
    int top[N],dfn[N],cnt,wi[N]; //点权(wi)可不用……
    il void getnum(int u,int tp){
        top[u]=tp,dfn[u]=++cnt,wi[cnt]=w[u];
        if(!son[u]) return;
        getnum(son[u],tp); //先处理重儿子
        for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
            int v=edge::e[i].v;
            if(v==fa[u]||v==son[u]) continue;
            getnum(v,v); //再处理轻儿子
        }
        return;
    }
} using namespace qwq;

应用

最近公共祖先

不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。
向上跳重链时需要先跳所在重链顶端深度较大的那个。

il int lca(int u,int v){
    while(top[u]!=top[v]){
        if(dep[top[u]]>dep[top[v]]) u=fa[top[u]];
        else v=fa[top[v]]; 
    }
    return dep[u]>dep[v]?v:u;
}

维护路径

链上的 DFS 序是连续的,可以使用线段树、树状数组维护。
每次选择深度较大的链往上跳,直到两点在同一条链上。
同样的跳链结构适用于维护、统计路径上的其他信息。

namespace operate{
    il void op1(int x,int y,int z){ //将树从x到y结点最短路径上所有节点的值都加上z。
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            add(1,1,n,dfn[top[x]],dfn[x],z),x=fa[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        return add(1,1,n,dfn[x],dfn[y],z);
    }
    il int op2(int x,int y){ //求树从x到y结点最短路径上所有节点的值之和
        int as=0;
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            as=(as+ask(1,1,n,dfn[top[x]],dfn[x]))%mod;
            x=fa[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        return as=(as+ask(1,1,n,dfn[x],dfn[y]))%mod;
    }
}

维护子树

子树中的结点的 DFS 序是连续的。
节点\(x\)子树所在连续区间为\([dfn[x],dfn[x]+sz[x]-1]\)
这样就把子树信息转化为连续的一段区间信息。

namespace operate{
    il void op3(int x,int z){ //将以x为根节点的子树内所有节点值都加上z
        return add(1,1,n,dfn[x],dfn[x]+sz[x]-1,z); 
    }
    il int op4(int x){ //求以x为根节点的子树内所有节点值之和
        return ask(1,1,n,dfn[x],dfn[x]+sz[x]-1);		
    }
}
code
#include<bits/stdc++.h>
#define il inline
#define cs const
#define ri register
using namespace std;
cs int N=1e5+5;
int n,m,root,mod,w[N];

namespace edge{
    int h[N];
    struct node{int v,nxt;}e[N*2];
    il void add(int u,int v,int id){
        e[id*2-1]={v,h[u]},h[u]=id*2-1;
        return e[id*2]={u,h[v]},h[v]=id*2,void();
    }
}

namespace qwq{ //树剖部分
    int dep[N],fa[N],son[N],sz[N];	
    il void init(int u,int pre){
        int mxson=-1;
        dep[u]=dep[pre]+1,fa[u]=pre,sz[u]=1;
        for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
            int v=edge::e[i].v;
            if(v==pre) continue;
            init(v,u),sz[u]+=sz[v];
            if(sz[v]>mxson) mxson=sz[v],son[u]=v;
        }
        return;		
    }
    int top[N],dfn[N],cnt,wi[N];
    il void getnum(int u,int tp){
        top[u]=tp,dfn[u]=++cnt,wi[cnt]=w[u];
        if(!son[u]) return;
        getnum(son[u],tp);
        for(ri int i=edge::h[u];i;i=edge::e[i].nxt){
            int v=edge::e[i].v;
            if(v==fa[u]||v==son[u]) continue;
            getnum(v,v);
        }
        return;
    }
} using namespace qwq;

namespace tree{	//线段树
    #define ls (rt<<1)
    #define rs ((rt<<1)|1)
    #define mid ((l+r)>>1)

    int tr[N<<2],lz[N<<2];
    il void pushup(int rt){
        return tr[rt]=(tr[ls]+tr[rs])%mod,void();
    }
    il void pushdown(int rt,int l,int r){
        if(!lz[rt]) return;
        lz[ls]=(lz[ls]+lz[rt])%mod;
        lz[rs]=(lz[rs]+lz[rt])%mod;
        tr[ls]=(tr[ls]+(lz[rt]*(mid-l+1))%mod)%mod;
        tr[rs]=(tr[rs]+(lz[rt]*(r-mid))%mod)%mod;
        return lz[rt]=0,void();
    }
    il void build(int rt,int l,int r){
        if(l==r) return tr[rt]=wi[l],void();
        build(ls,l,mid),build(rs,mid+1,r);
        return pushup(rt);
    }
    il void add(int rt,int l,int r,int ql,int qr,int k){
        if(ql<=l&&r<=qr){
            tr[rt]=(tr[rt]+(r-l+1)*k%mod)%mod;
            return lz[rt]=(lz[rt]+k)%mod,void();
        } pushdown(rt,l,r);
        if(mid>=ql) add(ls,l,mid,ql,qr,k); 
        if(mid<qr) add(rs,mid+1,r,ql,qr,k);
        return pushup(rt);
    }
    il int ask(int rt,int l,int r,int ql,int qr){
        if(ql<=l&&r<=qr) return tr[rt];
        pushdown(rt,l,r);int as=0;
        if(mid>=ql) as=(as+ask(ls,l,mid,ql,qr))%mod;
        if(mid<qr) as=(as+ask(rs,mid+1,r,ql,qr))%mod;
        return as;
    }

    #undef ls
    #undef rs
    #undef mid 
} using namespace tree;

namespace operate{
    il void op1(int x,int y,int z){ //将树从x到y结点最短路径上所有节点的值都加上z。
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            add(1,1,n,dfn[top[x]],dfn[x],z),x=fa[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        return add(1,1,n,dfn[x],dfn[y],z);
    }
    il int op2(int x,int y){ //求树从x到y结点最短路径上所有节点的值之和
        int as=0;
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            as=(as+ask(1,1,n,dfn[top[x]],dfn[x]))%mod;
            x=fa[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        return as=(as+ask(1,1,n,dfn[x],dfn[y]))%mod;
    }
    il void op3(int x,int z){ //将以x为根节点的子树内所有节点值都加上z
        return add(1,1,n,dfn[x],dfn[x]+sz[x]-1,z);
    }
    il int op4(int x){ //求以x为根节点的子树内所有节点值之和
        return ask(1,1,n,dfn[x],dfn[x]+sz[x]-1);		
    }
} using namespace operate;

int main(){
    cin>>n>>m>>root>>mod;
    for(ri int i=1;i<=n;++i) cin>>w[i],w[i]%=mod;
    for(ri int i=1,u,v;i<n;++i) cin>>u>>v,edge::add(u,v,i);
    init(root,root),getnum(root,root),build(1,1,n);
    for(ri int i=1,op,x,y,z;i<=m;++i){
        cin>>op>>x;
        if(op==1) cin>>y>>z,z%=mod,op1(x,y,z);
        if(op==2) cin>>y,cout<<op2(x,y)<<'\n';
        if(op==3) cin>>z,z%=mod,op3(x,z);
        if(op==4) cout<<op4(x)<<'\n';
    }
    return 0;
} 

长链剖分

  • 长链剖分本质上就是另外一种链剖分方式。
  • 长链剖分的剖分方法与轻重链剖分极其相似,只需要把以子树大小判断重儿子改成以节点深度判断即可
  • 长链剖分从一个节点到根的路径的轻边切换条数是 \(\sqrt{n}\) 级别的
  • 一般情况下可以使用长链剖分来优化的 DP 会有一维状态为深度维。
    to be continue~

edit

posted @ 2023-01-30 16:49  雨夜风月  阅读(38)  评论(0编辑  收藏  举报