树链剖分详细讲解,蒟蒻也能看懂

本文内容部分参考这篇博客,但图片,代码均为原创

树链剖分

定义

树链剖分是一种将树按轻重边 划分成若干条链,使任意两点路径可在 \(O(log\,n)\) 条链内分解,并可用线段树等结构维护路径或子树的算法。 它通过两次 DFS 求出重儿子、链顶、DFS 序等信息,从而把树上操作转化为序列区间操作,实现路径查询、路径修改、子树查询等高效处理。

核心思想

树链剖分(通常指重链剖分)通过以下方式将树结构线性化:

  • 重儿子:子树大小最大的儿子。
  • 重边:连接父节点和重儿子的边。
  • 重链:由连续重边构成的链。
  • 轻边/轻链:非重边构成的链。

关键性质:

  1. 每个节点最多只有一个重儿子。
  2. 从根到任意节点所经过的轻链 数量不超过 \(log\,n\),详见 oi-wiki。
  3. 重链在 DFS序 中是连续区间,可以用线段树维护。

实现方法

上面讲了一些废话,现在我们进入正题,如何实现树链剖分。

1.变量声明

为了实现树链剖分,我们需要声明下列变量(这里以树链剖分模板题为例)

cpp 复制代码
int n,m,r,p,a[N];
//n:节点数,m:问题数,r:根节点,a数组:初始的点权
vector<int>e[N];
//vector模拟邻接表存图
int d[N],f[N],siz[N],son[N];
//d数组:节点的深度,f数组:节点的父亲,siz数组:以某节点为根的子树的大小,son数组:某节点的重儿子
int id[N],w[N],top[N],times;
//id数组:类似dfs序,w数组:dfs序所对应的节点的权值,top数组:某条链的开头,times:时间戳
struct tree{
    int l,r;
    ll sum,lazy;
}t[N<<2];
//线段树基操

2.函数:dfs1

dfs1要处理 \(4\) 件事情

  1. 初始化每个点的深度 \(di\)
  2. 初始化每个点的父亲 \(fi\)
  3. 处理每个非叶子节点的子树大小
  4. 处理每个非叶子结点的重儿子编号
cpp 复制代码
inline void dfs1(int u,int fa){
    d[u]=d[fa]+1;
    f[u]=fa;
    siz[u]=1;
    int maxson=-1;
    for(auto v:e[u]){
        if (v==fa)continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if (siz[v]>maxson){
            son[u]=v;
            maxson=siz[v];
        }
    }
}

3.函数 dfs2

dfs2要处理 \(4\) 件事情

  1. 标记每个节点的新编号 id
  2. 初始化每个新编号的权值 w
  3. 处理每个点所在链的顶端 top
  4. 处理每一条链
cpp 复制代码
inline void dfs2(int u,int topf){
    id[u]=++times;
    w[times]=a[u];
    top[u]=topf;
    if (!son[u])return;
    dfs2(son[u],topf);
    for(auto v:e[u]){
        if (v==f[u] or v==son[u])continue;
        dfs2(v,v);//每一个轻儿子都有一条从他自己开始的链
    }
}
//处理顺序,先重再轻

解释 :处理顺序为什么是先重再轻呢?

我们所需的新编号应该是树上的一段连续的区间,处理顺序先重再轻可以确保每一条重链的新编号是连续的,又因为是 dfs 深度优先搜索,所以每一个子树的新编号也是连续的。

图片中蓝色数字即为该节点的新编号。

4.建线段树

最基础的"区间修改,区间查询的线段树",就不详细解释了,不会的参考这里

5.查询与修改

cpp 复制代码
//两点之间的路径权值修改
inline void range_add1(int x,int y,int val){
    val%=p;
    while(top[x]!=top[y]){
        if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
        update(1,id[top[x]],id[x],val);//让当前位置x,至x所属的链的top这个区间加上val
        x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
    }
    //此时x,y在同一条链上,在x至y的区间这个区间加上val
    if (d[x]>d[y])swap(x,y);//x深度更小,y深度更大
    update(1,id[x],id[y],val);
}
cpp 复制代码
//以某点为根的子树的权值修改
inline void range_add2(int x,int val){
    val%=p;
    //id 编号表示树上的一段区间,在x的子树的这段区间进行区间修改
    update(1,id[x],id[x]+siz[x]-1,val);
}
cpp 复制代码
//两点之间路径权值查询
inline ll range_ask1(int x,int y){
    ll ans=0;
    while(top[x]!=top[y]){
        if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
        ans+=query(1,id[top[x]],id[x]);//加上从当前位置x,至x所属的链的top这个区间的权值
        ans%=p;
        x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
    }
    //此时x,y在同一条链上,加上x至y的区间的权值
    if (d[x]>d[y])swap(x,y);
    ans+=query(1,id[x],id[y]);
    ans%=p;
    return ans;
}
cpp 复制代码
//以某点为根的子树的权值查询
inline ll range_ask2(int x){
    ll ans=0;
    //id 编号表示树上的一段区间,在x的子树的这段区间进行区间查询
    ans+=query(1,id[x],id[x]+siz[x]-1);
    ans%=p;
    return ans;
}

例题

题目传送门

完整代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define ll long long 
int n,m,r,p,a[N];
vector<int>e[N];
int d[N],f[N],siz[N],son[N];
int id[N],w[N],top[N],times;
struct tree{
    int l,r;
    ll sum,lazy;
}t[N<<2];
inline void dfs1(int u,int fa){
    d[u]=d[fa]+1;
    f[u]=fa;
    siz[u]=1;
    int maxson=-1;
    for(auto v:e[u]){
        if (v==fa)continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if (siz[v]>maxson){
            son[u]=v;
            maxson=siz[v];
        }
    }
}
inline void dfs2(int u,int topf){
    id[u]=++times;
    w[times]=a[u];
    top[u]=topf;
    if (!son[u])return;
    dfs2(son[u],topf);
    for(auto v:e[u]){
        if (v==f[u] or v==son[u])continue;
        dfs2(v,v);
    }
}
inline void pushup(int rt){
    t[rt].sum=t[ls].sum+t[rs].sum;
}
inline void pushdown(int rt){
    if (t[rt].lazy){
        ll lz=t[rt].lazy;
        t[rt].lazy=0;
        t[ls].lazy+=lz;
        t[rs].lazy+=lz;
        t[ls].sum+=lz*(t[ls].r-t[ls].l+1);
        t[rs].sum+=lz*(t[rs].r-t[rs].l+1);
        t[ls].lazy%=p,t[rs].lazy%=p;
        t[ls].sum%=p,t[rs].sum%=p;
    }
}
inline void build(int rt,int l,int r){
    t[rt].l=l,t[rt].r=r;
    if (l==r){
        t[rt].sum=w[l];
        return;
    }
    int mid=(l+r)>>1;
    build(ls,l,mid);
    build(rs,mid+1,r);
    pushup(rt);
}
inline void update(int rt,int l,int r,int val){
    if (l<=t[rt].l and t[rt].r<=r){
        t[rt].lazy+=val;
        t[rt].sum+=val*(t[rt].r-t[rt].l+1);
        t[rt].lazy%=p;
        t[rt].sum%=p;
        return;
    }
    pushdown(rt);
    int mid=(t[rt].l+t[rt].r)>>1;
    if (l<=mid)update(ls,l,r,val);
    if (mid<r)update(rs,l,r,val);
    pushup(rt);
}
inline ll query(int rt,int l,int r){
    if (l<=t[rt].l and t[rt].r<=r){
        return t[rt].sum;
    }
    pushdown(rt);
    int mid=(t[rt].l+t[rt].r)>>1;
    ll ans=0;
    if (l<=mid)ans+=query(ls,l,r);
    if (mid<r)ans+=query(rs,l,r);
    return ans;
}
inline void range_add1(int x,int y,int val){
    val%=p;
    while(top[x]!=top[y]){
        if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
        update(1,id[top[x]],id[x],val);//让当前位置x,至x所属的链的top这个区间加上val
        x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
    }
    //此时x,y在同一条链上,在x至y的区间这个区间加上val
    if (d[x]>d[y])swap(x,y);//x深度更小,y深度更大
    update(1,id[x],id[y],val);
}
inline void range_add2(int x,int val){
    val%=p;
    update(1,id[x],id[x]+siz[x]-1,val);//id 编号表示树上的一段区间,在x的子树的这段区间进行区间修改
}
inline ll range_ask1(int x,int y){
    ll ans=0;
    while(top[x]!=top[y]){
        if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
        ans+=query(1,id[top[x]],id[x]);//加上从当前位置x,至x所属的链的top这个区间的权值
        ans%=p;
        x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
    }
    //此时x,y在同一条链上,加上x至y的区间的权值
    if (d[x]>d[y])swap(x,y);
    ans+=query(1,id[x],id[y]);
    ans%=p;
    return ans;
}
inline ll range_ask2(int x){
    ll ans=0;
    ans+=query(1,id[x],id[x]+siz[x]-1);//id 编号表示树上的一段区间,在x的子树的这段区间进行区间查询
    ans%=p;
    return ans;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m>>r>>p;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        a[i]%=p;
    }
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    dfs1(r,0);
    dfs2(r,r);
    build(1,1,n);
    while(m--){
        int op;
        cin>>op;
        if (op==1){//x到y的路径上的点加z
            int x,y,z;
            cin>>x>>y>>z;
            range_add1(x,y,z);
        }
        if (op==2){
            int x,y;
            cin>>x>>y;
            cout<<range_ask1(x,y)<<"\n";
        }
        if (op==3){
            int x,y;
            cin>>x>>y;
            range_add2(x,y);
        }
        if (op==4){
            int x;
            cin>>x;
            cout<<range_ask2(x)<<"\n";
        }
    }
    return 0;
}

编辑不易,点个推荐再走吧~~