樹鏈剖分學習小記

前言

很早以前就聽說過這個算法,但是一直沒怎麼學習過,這一次好不容易有機會學習到了這個算法。
其實這個算法還是很簡單的,只要好好學,十分鐘不到就能學明白。

簡介

樹鏈剖分,又叫輕重鏈剖分,是一種對樹進行劃分的方法。
被劃分後的每個點只會屬於一條鏈上。
這有什麼作用呢?
這就相當於把一個樹上問題映射到了區間上,變成了一個在一個序列上的問題。
樹鏈剖分往往和樹狀數組、線段樹一起使用。
下面通過引入一道例題來講解這個算法。

例題

【ZJOI2008】樹的統計

Description

  一棵樹上有n個節點,編號分別爲1到n,每個節點都有一個權值w。
  我們將以下面的形式來要求你對這棵樹完成一些操作:
  I. CHANGE u t : 把結點u的權值改爲t
  II. QMAX u v: 詢問從點u到點v的路徑上的節點的最大權值
  III. QSUM u v: 詢問從點u到點v的路徑上的節點的權值和
  注意:從點u到點v的路徑上的節點包括u和v本身

Input

  輸入文件的第一行爲一個整數n,表示節點的個數。
  接下來n – 1行,每行2個整數a和b,表示節點a和節點b之間有一條邊相連。
  接下來n行,每行一個整數,第i行的整數wi表示節點i的權值。
  接下來1行,爲一個整數q,表示操作的總數。
  接下來q行,每行一個操作,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式給出。

Output

  對於每個“QMAX”或者“QSUM”的操作,每行輸出一個整數表示要求輸出的結果。

Sample Input

4
1 2
2 3
4 1
4 2 1 3
12
QMAX 3 4
QMAX 3 3
QMAX 3 2
QMAX 2 3
QSUM 3 4
QSUM 2 1
CHANGE 1 5
QMAX 3 4
CHANGE 3 6
QMAX 3 4
QMAX 2 4
QSUM 3 4

Sample Output

4
1
2
2
10
6
5
6
5
16

Data Constraint

【數據說明】
  對於100%的數據,保證1<=n<=30000,0<=q<=200000;中途操作中保證每個節點的權值w在-30000到30000之間。

分析

如果這是一個區間問題的話,那就是個大水題了,直接用線段樹亂搞一下就可以了。
然而這是在樹上的,單單使用線段樹顯得有些乏力,那麼我們就使用樹鏈剖分把他轉化爲區間問題。
下面開始講解樹鏈剖分。

講解

先定義一個概念。
重兒子——就是一個結點子結點最多的兒子,連向兒子的邊稱爲重邊
輕兒子——即除了重兒子以外的兒子,連向輕兒子的邊稱爲輕邊
因此,樹剖又叫輕重鏈剖分
該算法的核心即是:把重兒子與父親劃分到一條鏈上,輕兒子則自己單獨開闢一條新的鏈。
下面來講講該算法的實現。
談一談數組的設置。
top[x]表示x結點所在的鏈的開始結點
s[x],即size[x]的縮寫,表示x結點的子節點的個數
h[x],即x結點的深度
fa[x],即x的父親結點
tr[x]即重兒子優先得到的dfs序
pr[x]即tr[x]的反函數
son[x]即x的重兒子
我們可以通過兩遍dfs來算出這些數組

void dfs1(int x){
    s[x]=1;
    int i,j;
    for(i=he[x];i;i=nx[i]){
        j=b[i];
        if (j==fa[x]) continue;
        d[j]=d[x]+1;
        fa[j]=x;
        dfs1(j);
        s[x]+=s[j];
        if (!son[x]||s[son[x]]<s[j]) son[x]=j;}
}
void dfs2(int x,int y){
    int i;
    top[pre[tr[x]=++num]=x]=y;
    if (!son[x]) return;
    dfs2(son[x],y);
    for(i=he[x];i;i=nx[i])
      if (b[i]!=fa[x]&&b[i]!=son[x]) dfs2(b[i],b[i]);
}

這個代碼應該非常好理解
有了這個之後,上面的例題在套用線段樹應該就可以得到解決了吧

有些毒瘤題比較噁心,用dfs很可能會爆棧,不想打人工棧的同學可以打bfs
下面提供一個bfs版本的樹剖
變量名和dfs版本略有出入

void bfs(){
    int l,r,i,j,t,res;
    l=0,r=1,q[r]=1;
    while (l<r){
        x=q[++l];
        for(i=he[x];i;i=nx[i]){
            j=b[i];
            if (j==fa[x]) continue;
            fa[j]=x; h[j]=h[x]+1;
            q[++r]=j;}
    }
    fo(i,1,n) top[i]=i,s[i]=1;
    fa[1]=0;
    fd(i,n,1) s[fa[q[i]]]+=s[q[i]];
    dfn[1]=1; s[0]=0;
    fo(i,1,n) {
        x=q[i],t=0,res=dfn[x];
        for(j=he[x];j;j=nx[j]) if (s[b[j]]>s[t]&&b[j]!=fa[x]) t=b[j]; 
        if (t) son[x]=t,top[t]=top[x],dfn[t]=res+1,res+=s[t];
        for(j=he[x];j;j=nx[j]) 
          if (b[j]!=t&&b[j]!=fa[x]) dfn[b[j]]=res+1,res+=s[b[j]];}
    fo(i,1,n) seq[dfn[i]]=i;
}

dfn數組相當於dfs版本的tr數組
seq數組相當於dfs版本的pre數組

最近樹剖的題目做了不少,有時間的話會把博客寫出來和大家一起分享
Enjoy coding life!

下面還有一道板子題

jzoj4604 樹

題意就是給定一棵樹,給出兩種操作
一種是給樹中的某個點打標記
另一種是詢問
詢問離某個點最近的打了標記的點(一開始只有一號點打了標記)

Solution

很裸的樹剖
可以用來練練熟練度和碼力

Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
#define L rt<<1
#define R (rt<<1)+1 
using namespace std;
const int N=100025;
int n,i,x,y,tot,num,seq[N],tr[N<<2],qq; char ss[20];
int nx[N<<1],b[N<<1],he[N],s[N],dfn[N],top[N],h[N],fa[N],son[N],q[N];
int read(){
    int sum=0;
    char c=getchar();
    while (c<'0'||c>'9') c=getchar();
    while (c>='0'&&c<='9'){
        sum=sum*10+c-'0';
        c=getchar();}
    return sum;
}
inline void add(int x,int y){
    nx[++tot]=he[x];he[x]=tot;b[tot]=y;
}
void bfs(){
    int l,r,i,j,t,res;
    l=0,r=1,q[r]=1;
    while (l<r){
        x=q[++l];
        for(i=he[x];i;i=nx[i]){
            j=b[i];
            if (j==fa[x]) continue;
            fa[j]=x; h[j]=h[x]+1;
            q[++r]=j;}
    }
    fo(i,1,n) top[i]=i,s[i]=1;
    fa[1]=0;
    fd(i,n,1) s[fa[q[i]]]+=s[q[i]];
    dfn[1]=1; s[0]=0;
    fo(i,1,n) {
        x=q[i],t=0,res=dfn[x];
        for(j=he[x];j;j=nx[j]) if (s[b[j]]>s[t]&&b[j]!=fa[x]) t=b[j]; 
        if (t) son[x]=t,top[t]=top[x],dfn[t]=res+1,res+=s[t];
        for(j=he[x];j;j=nx[j]) 
          if (b[j]!=t&&b[j]!=fa[x]) dfn[b[j]]=res+1,res+=s[b[j]];}
    fo(i,1,n) seq[dfn[i]]=i;
}
inline void update(int rt){ tr[rt]=max(tr[L],tr[R]);}
inline void change(int rt,int l,int r,int x){
    if (l==r) {
        tr[rt]=l;
        return;}
    int mid=(l+r)>>1;
    if (x<=mid) change(L,l,mid,x);
      else change(R,mid+1,r,x);
    update(rt);
}
inline int query(int rt,int l,int r,int x,int y){
    if (l==x&&r==y) return tr[rt];
    int mid=(l+r)>>1;
    if (y<=mid) return query(L,l,mid,x,y);
      else if (x>mid) return query(R,mid+1,r,x,y);
        else return max(query(L,l,mid,x,mid),query(R,mid+1,r,mid+1,y)); 
}
inline void lca(int x){
    int ans=0;
    while (x>=1&&ans==0) {
        int f1=top[x];
        ans=query(1,1,n,dfn[f1],dfn[x]);
        x=fa[f1];}
    printf("%d\n",seq[ans]);
}
int main(){
//  freopen("1.in","r",stdin);
//  freopen("1.out","w",stdout);
    n=read(); qq=read();
    fo(i,2,n) add(x=read(),y=read()),add(y,x);
    s[1]=0,fa[1]=1;
    bfs();
    fa[1]=1;
    change(1,1,n,1);
    while (qq){
        qq--;
        scanf("%s",ss+1); x=read();
        if (ss[1]=='C') change(1,1,n,dfn[x]);
          else lca(x);  
    }
    return 0;
}
發佈了58 篇原創文章 · 獲贊 1 · 訪問量 6155
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章