树链剖分学习笔记

这篇文章是较于模板的知识,如果想要做题,左转例题篇

定义

树链剖分是什么?

树链剖分指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构来维护每一条链,包含重链剖分长链剖分和实链剖分,我们平常使用的都是重链剖分。

树链剖分解决了什么?

树上的单点(单边)修改,路径修改,子树修改和换根

树上的单点(单边)查询,路径查询,子树查询

是比DFS序和树上差分更多变,比LCT更简单的数据结构。

如何实现树剖

Luogu P3384 【模板】树链剖分

例题引入,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

操作1:将树从x到y结点最短路径上所有节点的值都加上z

操作2:求树从x到y结点最短路径上所有节点的值之和

操作3:将以x为根节点的子树内所有节点值都加上z

操作4:求以x为根节点的子树内所有节点值之和

子树的操作还好处理,路径上的便有些棘手。思考:为何不能将一条路径操作分段转到序列上,再用序列上的数据结构维护呢?


 

定义size[x]为以x为根的子树大小,son[x]为x的重儿子(定义为x的儿子中size最大的儿子),对于每个节点:连向它重儿子的边为重边,其余则为轻边。

重边组成的路径叫做重链。

 

 

图中黑色的边是重边,打上红点的节点是其父亲的轻儿子(除重儿子以外的儿子)

可以直观的感受到,轻边总是连接着两条重链,而每个点只归属一条重链。 我们萌生出了一个想法,就是存下每一条重链,并在路径操作时分成若干条重链进行操作,即实现了树链剖分。

 

完善定义

dep[x]:节点x的深度

prt[x]:节点x的父亲

son[x]:节点x的重儿子

size[x]:以x为根的子树大小

top[x]:节点x所在重链的链头

tid[x]:第二次DFS的DFS序

rk[x]:tid的反映射

前面四个数组都可以由第一次DFS求得,在知道重儿子之后,我们就可以第二次DFS求出后三个数组。 第二次DFS的规则是,先搜当前节点的重儿子,再搜轻儿子。

tid有什么用呢?我们把上图的tid写出来: 1 4 9 13 14 8 10 2 6 11 12 5 3 7

首先它是一个DFS序,所以自然满足对于任意一个x,区间[tid[x],tid[x]+size[x]-1]是x的子树,故可以迅速的实现子树操作。

其次,应为DFS的顺序是默认的重儿子优先,所以每一条重链在tid上都是连续的

说白了,就可以把一条路径拆为若干个在tid上的连续区间,再用数据结构维护。 怎么拆分呢?

类似于LCA的爬树法,选一个深度小的向上跳,对应的区间就是[tid[top[x]],tid[x]],然后通过top[x]的父亲爬到另一条重链上,直到跳到同一条重链上

e.g:拆分(5,14)这条路径:(5,5)+(2,2)+(1,14)

思路大概有了,看一看代码实现。

//预处理代码
void
DFS1(int x,int fa,int depth){ dep[x]=depth,prt[x]=fa,size[x]=1; for(int i=h[x];i;i=w[i].nxt){ int v=w[i].to; if(v==fa)continue; DFS1(v,x,depth+1); size[x]+=size[v]; if(size[v]>size[son[x]])son[x]=v;//更新重儿子 } } void DFS2(int x,int sp){ top[x]=sp,tid[x]=++tot,rk[tot]=x; if(!son[x])return; DFS2(son[x],sp);//先搜重儿子 for(int i=h[x];i;i=w[i].nxt){ int v=w[i].to; if(v==prt[x]||v==son[x])continue; DFS2(v,v);//自己为链头搜下去 } }
//对应4个操作的代码
inline void change(int x,int y,int d){ while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); update(1,tid[top[x]],tid[x],d); x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); update(1,tid[x],tid[y],d); } inline int ask(int x,int y){ int ans=0; while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]])swap(x,y); ans+=query(1,tid[top[x]],tid[x]); x=prt[top[x]]; } if(dep[x]>dep[y])swap(x,y); return ans+query(1,tid[x],tid[y]); } inline void changeson(int x,int d){ update(1,tid[x],tid[x]+size[x]-1,d); } inline int askson(int x){ return query(1,tid[x],tid[x]+size[x]-1); }

 加上线段树,我们愉快地解决了这道题

#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define int long long
#define INF 0x7fffffff
inline int read(){
    char ch;
    int bj=0;
    while(!isdigit(ch=getchar()))
      bj|=(ch=='-');
    int res=ch^(3<<4);
    while(isdigit(ch=getchar()))
      res=(res<<1)+(res<<3)+(ch^(3<<4));
    return bj?-res:res;
}
void printnum(int x){
    if(x>9)printnum(x/10);
    putchar(x%10+'0');
}
inline void print(int x,char ch){
    if(x<0){
        putchar('-');
        x=-x;
    }
    printnum(x);
    putchar(ch);
}
const int MAXN=100005;
int n,m,mod,root,cnt,tot;
int h[MAXN],size[MAXN],prt[MAXN],son[MAXN],dep[MAXN],tid[MAXN],top[MAXN],rk[MAXN],a[MAXN];
struct Edge {
    int to,nxt;
}w[MAXN<<1];
inline void AddEdge(int x,int y){
    w[++cnt].to=y;
    w[cnt].nxt=h[x];
    h[x]=cnt;
}
void DFS1(int x,int fa,int depth){
    dep[x]=depth,prt[x]=fa,size[x]=1;
    for(int i=h[x];i;i=w[i].nxt){
        int v=w[i].to;
        if(v==fa)continue;
        DFS1(v,x,depth+1);
        size[x]+=size[v];
        if(size[v]>size[son[x]])son[x]=v; 
    }
} 
void DFS2(int x,int sp){
    top[x]=sp,tid[x]=++tot,rk[tot]=x;
    if(!son[x])return;
    DFS2(son[x],sp);
    for(int i=h[x];i;i=w[i].nxt){
        int v=w[i].to;
        if(v==prt[x]||v==son[x])continue;
        DFS2(v,v);
    }
}
struct Segtree {
    int l,r,sum,bj;
}tree[MAXN<<2];
inline void pushup(int k){
    tree[k].sum=(tree[k<<1].sum+tree[k<<1|1].sum)%mod; 
}
inline void pushdown(int k){
    if(tree[k].bj){
        tree[k<<1].bj=(tree[k<<1].bj+tree[k].bj)%mod;
        tree[k<<1|1].bj=(tree[k<<1|1].bj+tree[k].bj)%mod;
        tree[k<<1].sum=(tree[k<<1].sum+(tree[k<<1].r-tree[k<<1].l+1)*tree[k].bj%mod)%mod;
        tree[k<<1|1].sum=(tree[k<<1|1].sum+(tree[k<<1|1].r-tree[k<<1|1].l+1)*tree[k].bj%mod)%mod;
        tree[k].bj=0;
    }
}
inline void build(int k,int l,int r){
    tree[k].l=l,tree[k].r=r;
    if(l==r){
        tree[k].sum=a[rk[l]];
        return;
    }
    int mid=(l+r)>>1;
    build(k<<1,l,mid);
    build(k<<1|1,mid+1,r);
    pushup(k);
}
inline void update(int k,int l,int r,int d){
    if(l>tree[k].r||r<tree[k].l)return;
    if(l<=tree[k].l&&r>=tree[k].r){
        tree[k].bj=(tree[k].bj+d)%mod;
        tree[k].sum=(tree[k].sum+(tree[k].r-tree[k].l+1)*d%mod)%mod;
        return;
    }
    pushdown(k);
    update(k<<1,l,r,d);
    update(k<<1|1,l,r,d);
    pushup(k);
}
inline int query(int k,int l,int r){
    if(l>tree[k].r||r<tree[k].l)return 0;
    if(l<=tree[k].l&&r>=tree[k].r)return tree[k].sum;
    pushdown(k);
    return (query(k<<1,l,r)+query(k<<1|1,l,r))%mod; 
}
inline void change(int x,int y,int d){
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        update(1,tid[top[x]],tid[x],d);
        x=prt[top[x]];
    }    
    if(dep[x]>dep[y])swap(x,y);
    update(1,tid[x],tid[y],d);
}
inline int ask(int x,int y){
    int ans=0;
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        ans=(ans+query(1,tid[top[x]],tid[x]))%mod;
        x=prt[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    return (ans+query(1,tid[x],tid[y]))%mod;
}
inline void changeson(int x,int d){
    update(1,tid[x],tid[x]+size[x]-1,d);
}
inline int askson(int x){
    return query(1,tid[x],tid[x]+size[x]-1); 
}
signed main(){
    n=read(),m=read(),root=read(),mod=read();
    for(int i=1;i<=n;i++)a[i]=read();
    int op,x,y,d;
    for(int i=1;i<n;i++){
        x=read(),y=read();
        AddEdge(x,y),AddEdge(y,x);
    } 
    DFS1(root,0,1);
    DFS2(root,root);
    build(1,1,n);
    while(m--){
        op=read();
        if(op==1){
            x=read(),y=read(),d=read();
            change(x,y,d);
        } 
        else if(op==2){
            x=read(),y=read();
            print(ask(x,y),'\n');
        }
        else if(op==3){
            x=read(),d=read();
            changeson(x,d);
        }
        else {
            x=read();
            print(askson(x),'\n');
        }
    } 
    return 0;
}

树链剖分时间复杂度

引理:若(u,v)是轻边,则size[v] < size[u]/2

证明:显然。

有此可得:每跳一次轻边,子树大小就除以2,故拆路径的时间复杂度是O(log n) 这就是树剖为什么按照轻重划分的原因。

树剖求LCA

既然树剖也是爬树,那为何不能求LCA呢?想一想,当x和y跳到同一重链时,深度小的那个不就是LCA吗? 时间复杂度:O(log n)。

inline int LCA(int x,int y){
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])swap(x,y);
        x=prt[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    return x; 
}

 

偷偷说一句:树剖实际仅次于Tarjan求LCA,有时把欧拉环游序吊起来打(逃。

因为极端数据卡不住树剖,在满二叉树时O(log n)才会跑满。 在空间卡得紧的情况下,树剖是求LCA的最佳选择

posted @ 2019-11-08 23:08  Soledad_S  阅读(145)  评论(0编辑  收藏  举报