• 树链剖分学习笔记


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

    定义

    树链剖分是什么?

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

    树链剖分解决了什么?

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

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

    是比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),'
    ');
            }
            else if(op==3){
                x=read(),d=read();
                changeson(x,d);
            }
            else {
                x=read();
                print(askson(x),'
    ');
            }
        } 
        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的最佳选择

  • 相关阅读:
    (整理)SQLServer_DBA 工具
    (转)winform Form 淡入淡出效果
    (转)SQLServer_T-SQL 语句执行时间的查询
    (整理)IIS 7 503 "service unavailable" errors
    (转载)C#中使用GUID
    (转载)SQL Server 2005 如何启用xp_cmdshell组件
    设计模式之适配器
    jaxb 组装及解析xml
    springMvc 简单搭建
    设计模式之工厂模式
  • 原文地址:https://www.cnblogs.com/soledadstar/p/11823805.html
Copyright © 2020-2023  润新知