樹鏈剖分


樹鏈剖分

1. 相關概念

  • 重兒子:父親節點的所有兒子中子樹結點數目最多(size最大)的結點;

  • 輕兒子:父親節點中除了重兒子以外的兒子;

  • 重邊:父親結點和重兒子連成的邊;

  • 輕邊:父親節點和輕兒子連成的邊;

  • 重鏈:由多條重邊連接而成的路徑;

  • 輕鏈:由多條輕邊連接而成的路徑

  • 如下圖所示

2. 樹鏈剖分的實現

    • 上圖,紅點為重鏈的起點加粗黑邊為重邊細邊輕邊,加粗黑圈為重子節點,其他為輕子節點,節點右邊的數字為第二遍bfs遍歷順序。
  1. 首先求出每個節點所在的子樹大小,找到它的重兒子(即預處理出size,son數組)

    • 比如:節點1的三個子節點,size[2]=5,size[3]=2,size[4]=6,節點最大的是4,所以,節點1的重兒子是節點4
    • 如果一個節點的多個子節點一樣大,且均為最大節點,那隨便找一個當做它的重兒子。
    • 葉節點沒有重兒子,非葉節點有且只有一個重兒子。
  2. dfs過程中順便記錄其父親以及深度,操作1,2可以通過一遍dfs完成。

    void dfs1(int u,int fa){ //預處理出當前節點、父節點、層次深度
        f[u]=fa;size[u]=1;    //這個點本身size=1
        for(int i=head[u];i;i=e[i].next){
            int v=e[i].to;
            if(v==fa)continue;
            deep[v]=deep[u]+1;
            dfs1(v,u);    //層次深度+1
            size[u]+=size[v];    //子節點的size已被處理,用它來更新父節點的size
            if(size[v]>size[son[u]])//選取size最大的作為重兒子
                son[u]=v; //son[u]表示u的重兒子   
        }
    }
    
  3. 第二遍dfs,連接重鏈,同時標記每一個節點的dfs序,並且為了用數據結構來維護重鏈,我們在dfs時保證一條重鏈上各個節點dfs序連續。

    void dfs2(int u,int t){    //當前節點、重鏈頂端
        top[u]=t;//保存當前節點所在鏈的頂端節點
        dfn[u]=++cnt;    //cnt標記dfs序
        rk[cnt]=u;    //序號cnt對應節點u
        if(son[u])dfs2(son[u],t);//先走重兒子
    /*我們選擇優先進入重兒子來保證一條重鏈上各個節點dfs序連續,
    一個點和它的重兒子處於同一條重鏈,所以重兒子所在重鏈的頂端還是t*/
        for(int i=head[u];i;i=e[i].next){//遍歷輕鏈
            int v=e[i].to;
            if(v!=son[u]&&v!=f[u])
                dfs2(v,v);    //一個點位於輕鏈底端,那么它的top必然是它本身
        }
    }
    

3. 樹鏈刨分LCA

  • 算法實現:求樹上節點u,vLCA

    1. 如果u,v在同一個重鏈上,即,top[u]==top[v],則深度小的為LCA
    2. 節點u,v不在同一個重鏈,讓深度大的鏈頂節點u往上跳,跳到其鏈頂的父親節點上,即u=f[top[u]].
    3. 重復步驟2直到節點u,v在同一個重鏈,此時深度小的為LCA
  • 例題:luogu P3379 樹鏈剖分求LCA

  • Code

    #include <bits/stdc++.h>
    const int maxn=5e5+5;
    struct edge{
        int to,next;
    }e[2*maxn];
    int n,m,root,len,head[maxn],deep[maxn],siz[maxn],son[maxn],top[maxn],f[maxn];
    void Insert(int x,int y){
        e[++len].to=y;e[len].next=head[x];head[x]=len;
    }
    void dfs1(int u){
        siz[u]=1;deep[u]=deep[f[u]]+1;
        for(int i=head[u];i;i=e[i].next){
            int v=e[i].to;
            if(v==f[u])continue;
            f[v]=u;
            dfs1(v);
            siz[u]+=siz[v];
            if(!son[u]||siz[son[u]]<siz[v])//求重兒子
                son[u]=v;
        }
    }
    void dfs2(int u,int tp){
        top[u]=tp;
        if(son[u])dfs2(son[u],tp);
        for(int i=head[u];i;i=e[i].next){
            int v=e[i].to;
            if(v!=f[u] && v!=son[u])//v是輕兒子
                dfs2(v,v);
        }
    }
    int LCA(int u,int v){
        while(top[u]!=top[v]){
            if(deep[top[u]]>=deep[top[v]])
                u=f[top[u]];
            else 
                v=f[top[v]];
        }
        return deep[u] < deep[v] ? u : v;
    }
    void Solve(){
        scanf("%d%d%d",&n,&m,&root);
        for(int i=1;i<n;++i){
            int x,y;scanf("%d%d",&x,&y);
            Insert(x,y);Insert(y,x);
        }
        dfs1(root);
        dfs2(root,root);
        for(int i=1;i<=m;++i){
            int u,v;scanf("%d%d",&u,&v);
            printf("%d\n",LCA(u,v));
        }
    }
    int main(){
        Solve();
        return 0;
    }
    
  • 時間效率:dfsO(n),查詢一次為:O(log(n)),總的時間效率:O(m*log(n))

  • u,v均在輕鏈的底端是,每次往上跳時只能從父親節點一個個往上跳,但輕鏈到根節點距離小於log(n),一般情況下樹鏈刨分的常數非常小,不到1/2

樹鏈剖分(線段樹)

  • 例題:luogu P3384 【模板】輕重鏈剖分

  • 分析:

    • 區間修改、區間查詢是線段樹的基本操作,但我們熟悉的是線性數據結果上的操作。

    • 樹鏈剖分正好能把樹上的路徑划分成一條條重鏈,一條重鏈正好是一個區間。

    • Code

      #include<bits/stdc++.h>
      using namespace std;
      const int maxn=1e5+1000; 
      struct edge{int next,to;}e[maxn<<2];
      struct  node{int l,r,w,siz,lazy;}tr[maxn<<2];
      int len,root,MOD,cnt=0,n,m;
      int a[maxn],head[maxn];;
      int deep[maxn],f[maxn],son[maxn],size[maxn],top[maxn],dnf[maxn],rk[maxn];
      void Insert(int u,int v){
          e[++len].to=v;e[len].next=head[u];head[u]=len;
       } 
      void dfs1(int u,int fa){//處理深度,父親節點,u為根的節點個數 
          size[u]=1;
          for(int i=head[u];~i;i=e[i].next){
              int v=e[i].to;
              if(v==fa) continue;
              deep[v]=deep[u]+1;f[v]=u;
              dfs1(v,u);
              size[u]+=size[v];
              if(!son[u] || size[v]>size[son[u]])//重鏈的兒子
                  son[u]=v;
           }
       }
       void dfs2(int u,int tp){
          top[u]=tp;//標記重鏈的頂點
          dnf[u]= ++cnt;//節點對應編號
          rk[cnt]=a[u];//編號對應節點建樹的關鍵一員 
          if(son[u])
              dfs2(son[u],tp);
          for(int i=head[u];~i;i=e[i].next){
              int v=e[i].to;
              if(v!=son[u] && v!=f[u]) dfs2(v,v);//非重鏈的頂點就是自己 
           }     
       }
       void push_up(int u){
          tr[u].w=(tr[u<<1].w+tr[u<<1|1].w+MOD)%MOD;
       }
       void build(int u,int l,int r){
          tr[u].l=l;tr[u].r=r;tr[u].siz=r-l+1;
          if(l==r){
              tr[u].w=rk[l];
              return ;        
           }
           int mid=(l+r)>>1;
           build(u<<1,l,mid);build(u<<1|1,mid+1,r);
           push_up(u);
       }
       void push_down(int u){
          if(tr[u].lazy){
              tr[u<<1].w=(tr[u<<1].w+tr[u<<1].siz*tr[u].lazy)%MOD;
              tr[u<<1|1].w=(tr[u<<1|1].w+tr[u<<1|1].siz*tr[u].lazy)%MOD;
              tr[u<<1].lazy=(tr[u<<1].lazy+tr[u].lazy)%MOD;
              tr[u<<1|1].lazy=(tr[u<<1|1].lazy+tr[u].lazy)%MOD;
              tr[u].lazy=0;
           }
       }
       void update(int u,int l,int r,int val){
          if(l<=tr[u].l && tr[u].r<=r){
              tr[u].w+=tr[u].siz*val;
              tr[u].lazy+=val;
              return ;
           }
           push_down(u);
           int mid=(tr[u].l+tr[u].r)>>1;
           if(l<=mid) update(u<<1,l,r,val);
           if(r>mid) update(u<<1|1,l,r,val);
           push_up(u);
        } 
       void treeadd(int u,int v,int val){
          while(top[u]!=top[v]){//將兩個節點跳到同一重鏈上     
              if(deep[top[u]]<deep[top[v]]) swap(u,v);
              update(1,dnf[top[u]],dnf[u],val);
              u=f[top[u]];
           }
           if(deep[u]>deep[v]) swap(u,v);//節點編號的順序依照深度大小,深度越大節點編號越大 
           update(1,dnf[u],dnf[v],val);//因為遍歷線段樹區間[l,r]必須從小到大 
       }
       int query(int u,int l,int r){
          int ans=0;
          if(l<=tr[u].l && tr[u].r<=r) return tr[u].w;
          push_down(u);
          int mid=(tr[u].l+tr[u].r)>>1;
          if(l<=mid) ans=(ans+query(u<<1,l,r))%MOD;
          if(r>mid) ans=( ans+query(u<<1|1,l,r))%MOD;
          return ans;
       }
       void querysum(int u,int v){//樹剖求區間和
          int ans=0;
          while(top[u]!=top[v]){//不在一條重鏈上時,把深度低的鏈頂到節點連續區間求和
              if(deep[top[u]]<deep[top[v]]) swap(u,v);
              ans=(ans+query(1,dnf[top[u]],dnf[u]))%MOD;
              u=f[top[u]];
           }//跳出循環后,u,v在同一條鏈
           if(deep[u]>deep[v]) swap(u,v);
           ans=(ans+query(1,dnf[u],dnf[v]))%MOD;
           printf("%d\n",ans);
       }
      int main(){
          memset(head,-1,sizeof(head));
          scanf("%d%d%d%d",&n,&m,&root,&MOD);
          for(int i=1;i<=n;i++) scanf("%d",&a[i]);
          for(int i=1;i<=n-1;i++){
              int x,y;
              scanf("%d %d",&x,&y);
              Insert(x,y);Insert(y,x);
          }
          dfs1(root,0);
          dfs2(root,root); 
          build(1,1,n); 
          while(m--){
              int op,x,y,z;
              scanf("%d",&op);
              if(op==1){
                  scanf("%d%d%d",&x,&y,&z); z=z%MOD;
                  treeadd(x,y,z);
              }
              else if(op==2){
                  scanf("%d %d",&x,&y);
                  querysum(x,y);
              }
              else if(op==3){
                  scanf("%d %d",&x,&y);
                  update(1,dnf[x],size[x]+dnf[x]-1,y%MOD);
              }
              else if(op==4){
                  scanf("%d",&x);
                  printf("%d\n",query(1,dnf[x],dnf[x]+size[x]-1));
              }
          }
       } 
      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM