1. 程式人生 > >樹鏈剖分演算法詳解

樹鏈剖分演算法詳解

學OI也有一段時間了,感覺該搞點東西了。

於是學習了樹()鏈()剖(pou)分(糞)

當然,學習這個演算法是需要先學習線段樹的。不懂的還是再過一段時間吧。


如果碰到一道題,要對一顆樹的兩個點中的最短路徑、以u為根的子樹之類的東西進行修改或者查詢,那麼大概就是樹鏈剖分的題了。

樹鏈剖分就是把一顆樹的節點按照新的順序扔到一顆線段樹裡面,然後保證一條樹鏈上的點線上段樹中儘可能連續。

 

為什麼是儘可能?因為在一棵樹中,怎麼搞也無法保證對於每一個節點,他的父親編號都是它的-1,所以是儘可能。那麼怎麼儘可能呢?

有很多演算法,今天提到的就是樹鏈剖分。我們把一顆樹上的所有鏈分成輕鏈

重鏈,然後就可以對於每一段連續的重鏈進行線段樹上的修改了。

 

而劃分輕鏈和重鏈的依據是:對於每一個節點u,v是它的兒子,v有一個大小,就是size,代表以v為根的子樹的大小。我們選取u最大的兒子為重(zhong)兒子,其餘兒子為輕兒子。以連向重兒子的邊為重邊,剩下的邊為輕邊。

然後所有重邊連成的鏈叫做重鏈,(並不存在輕鏈)比如下圖,紅色的鏈是重鏈(注意,對於一個葉子節點,如果連向它的是一條輕鏈,那麼他自己就是一條重鏈)

這樣,我們把一棵樹劃分成了重鏈和輕鏈,我們能保證所有重鏈都不重不漏的包含了所有的點。

那麼這些重鏈有什麼用?在劃分重鏈的過程中用到的DFS,這個DFS能保證,對於每一條重鏈,他們的DFS序是連續的!

這樣,我們就可以用線段樹(或者其他資料結構)維護了!

 現在,我們把熟練剖分化成兩個部分:

1、把樹上的所有點劃分重鏈,然後求出它們的DFS序,以這個順序扔到線段樹裡面。

2、線上段樹上進行維護。

所以,如何實現劃分重鏈?我們需要用兩個DFS,第一個DFS找到所有點的重兒子,第二個DFS將所有重兒子連成重鏈。

第一個DFS:size是以當前點為根的子樹的大小,f是當前點的父親,son是當前點的重兒子。

inline void getson(int u,int fa){//獲取每個節點的重兒子 
    size[u]=1;
    for(int e=head[u];e;e=nxt[e])
        
if(to[e]!=fa){ depth[to[e]]=depth[u]+1; f[to[e]]=u; getson(to[e],u); size[u]+=size[to[e]];//記錄以每個節點為根的樹的大小 if(!son[u] || size[son[u]]<size[to[e]]) son[u]=to[e];//判斷後將這個點變為重兒子 } return ; }

第二個DFS:

inline void getdfn(int u,int t){//連成重鏈,其中我們可以保證,對於每一條重鏈,它們的dfn值是連續的。t記錄的是當前鏈的鏈首 
    top[u]=t;//top記錄當前鏈鏈首 
    dfn[u]=++cnt;//記錄dfn值,也是線上段樹中的位置 
    link[cnt]=u;//dfn的逆運算,用於建樹時的初始賦值 
    if(!son[u])  return ;//如果當前點沒有重兒子,說明是這條重鏈的結束。 
    getdfn(son[u],t);//繼續走這條重鏈 
    for(int e=head[u];e;e=nxt[e])//這個相當於走每一條輕鏈 
        if(to[e]!=son[u] && to[e]!=f[u])
            getdfn(to[e],to[e]);//重新開始走每一條重鏈 
    return ;
}

然後,對於線段樹的建樹,是獨立的,我們不用考慮鏈的關係。(input是輸入檔案)

inline void build(int i,int l,int r){//平凡的建樹 
    tree[i].l=l,tree[i].r=r;
    if(l==r){
        tree[i].sum=input[link[l]]%mod;//link的作用 
        return ;
    }
    int mid=(l+r)>>1;
    build(i<<1,l,mid);
    build(i<<1|1,mid+1,r);
    tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%mod;
    return ;
}

最後是修改,查詢和修改很像,一起說了。

我們要把u到v路徑上所有的點都+k,那麼我們就把u,v中深的那個,它到它所在重邊的頂端+k。

然後跳過一條輕邊,重複上面的步驟,知道u,v到一條重邊上。

最後把u到v,+k就可以了。

inline void treeadd(int x,int y,int z){//將題中對樹的修改轉化成對線段樹的修改 
    int tx=top[x],ty=top[y]; 
    while(tx!=ty){//如果兩個點不在一條重鏈上 
        if(depth[tx]<depth[ty])  swap(x,y),swap(tx,ty);//保證x的重鏈首元素在下方 
        add(1,dfn[tx],dfn[x],z);//從x一直修改到x所在重鏈的收元素,因為他們在一條重鏈中,所以線上段樹中的位置是連續的。 
        x=f[tx];//走過一條輕鏈,到上面一個重鏈的末尾 
        tx=top[x],ty=top[y];//分別更新x、y的重鏈頂端,準備下一次更新 
    }
    if(depth[x]<depth[y])  swap(x,y);//現在x、y都到了一條重鏈上了,然後要保證x在下面。 
    add(1,dfn[y],dfn[x],z);//再只用更新他們所在的鏈就可以了。 
    return ;
}
inline int treesum(int x,int y){//將題中對樹查詢得指令改為對線段樹的查詢。 
    int ans=0;
    int tx=top[x],ty=top[y];
    while(tx!=ty){//這一段和修改幾乎一樣,就是把原本對每一個區間的修改,變為了查詢,其實都一樣。 
        if(depth[tx]<depth[ty])  swap(tx,ty),swap(x,y);
        ans=(ans+query(1,dfn[tx],dfn[x]))%mod;
        x=f[tx];
        tx=top[x],ty=top[ty];
    }
    if(depth[x]<depth[y])  swap(x,y);
    return (ans+query(1,dfn[y],dfn[x]))%mod;
}

 

對於線段樹上的維護,和樸素的線段樹一樣,就不多說了。

 

如果題目中說要將以i為根的子樹+k,那就直接線上段樹上從dfn[i]到dfn[i]+size[i],+k就可以了。

具體看AC程式碼:(洛谷模板題)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#define in(a) a=read()
#define REP(i,k,n)  for(int i=k;i<=n;i++)
#define MAXN 100010
using namespace std;
inline int read(){
    int x=0,f=1;
    char ch=getchar();
    for(;!isdigit(ch);ch=getchar())
        if(ch=='-')
            f=-1;
    for(;isdigit(ch);ch=getchar())
        x=x*10+ch-'0';
    return x*f;
}
int n,m,r,mod,input[MAXN];
int total,head[MAXN],to[MAXN<<1],nxt[MAXN<<1];
int size[MAXN],depth[MAXN],f[MAXN],son[MAXN];
int cnt,dfn[MAXN],link[MAXN],top[MAXN];
struct node{
    int l,r,sum,lt;
}tree[MAXN<<2];
inline void adl(int a,int b){
    total++;
    to[total]=b;
    nxt[total]=head[a];
    head[a]=total;
    return ;
}
inline void getson(int u,int fa){//獲取每個節點的重兒子 
    size[u]=1;
    for(int e=head[u];e;e=nxt[e])
        if(to[e]!=fa){
            depth[to[e]]=depth[u]+1;
            f[to[e]]=u; 
            getson(to[e],u);
            size[u]+=size[to[e]];//記錄以每個節點為根的樹的大小 
            if(!son[u] || size[son[u]]<size[to[e]])  son[u]=to[e];//判斷後將這個點變為重兒子 
        }
    return ;
}
inline void getdfn(int u,int t){//連成重鏈,其中我們可以保證,對於每一條重鏈,它們的dfn值是連續的。t記錄的是當前鏈的鏈首 
    top[u]=t;//top記錄當前鏈鏈首 
    dfn[u]=++cnt;//記錄dfn值,也是線上段樹中的位置 
    link[cnt]=u;//dfn的逆運算,用於建樹時的初始賦值 
    if(!son[u])  return ;//如果當前點沒有重兒子,說明是這條重鏈的結束。 
    getdfn(son[u],t);//繼續走這條重鏈 
    for(int e=head[u];e;e=nxt[e])//這個相當於走每一條輕鏈 
        if(to[e]!=son[u] && to[e]!=f[u])
            getdfn(to[e],to[e]);//重新開始走每一條重鏈 
    return ;
}
inline void build(int i,int l,int r){//平凡的建樹 
    tree[i].l=l,tree[i].r=r;
    if(l==r){
        tree[i].sum=input[link[l]]%mod;//link的作用 
        return ;
    }
    int mid=(l+r)>>1;
    build(i<<1,l,mid);
    build(i<<1|1,mid+1,r);
    tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%mod;
    return ;
}
inline void pushdown(int i){//平凡的pushdown 
    if(!tree[i].lt)  return ;
    tree[i<<1].lt+=tree[i].lt;
    tree[i<<1|1].lt+=tree[i].lt;
    int mid=(tree[i].l+tree[i].r)>>1;
    tree[i<<1].sum=(tree[i<<1].sum+(mid-tree[i].l+1)*tree[i].lt)%mod;
    tree[i<<1|1].sum=(tree[i<<1|1].sum+(tree[i].r-mid)*tree[i].lt)%mod;
    tree[i].lt=0;
    return ;
}
inline void add(int i,int l,int r,int k){//平凡的區間修改 
    if(tree[i].l>=l && tree[i].r<=r){
        tree[i].sum=(tree[i].sum+(tree[i].r-tree[i].l+1)*k)%mod;
        tree[i].lt+=k;
        return ;
    }
    pushdown(i);
    if(tree[i<<1].r>=l)  add(i<<1,l,r,k);
    if(tree[i<<1|1].l<=r) add(i<<1|1,l,r,k);
    tree[i].sum=(tree[i<<1].sum+tree[i<<1|1].sum)%mod;
    return ;
}
inline int query(int i,int l,int r){//平凡的區間查詢 
    if(tree[i].l>=l && tree[i].r<=r)  return tree[i].sum;
    int sum=0;
    pushdown(i);
    if(tree[i<<1].r>=l)  sum=(sum+query(i<<1,l,r))%mod;
    if(tree[i<<1|1].l<=r)  sum=(sum+query(i<<1|1,l,r))%mod;
    return sum;
}
inline void treeadd(int x,int y,int z){//將題中對樹的修改轉化成對線段樹的修改 
    int tx=top[x],ty=top[y]; 
    while(tx!=ty){//如果兩個點不在一條重鏈上 
        if(depth[tx]<depth[ty])  swap(x,y),swap(tx,ty);//保證x的重鏈首元素在下方 
        add(1,dfn[tx],dfn[x],z);//從x一直修改到x所在重鏈的收元素,因為他們在一條重鏈中,所以線上段樹中的位置是連續的。 
        x=f[tx];//走過一條輕鏈,到上面一個重鏈的末尾 
        tx=top[x],ty=top[y];//分別更新x、y的重鏈頂端,準備下一次更新 
    }
    if(depth[x]<depth[y])  swap(x,y);//現在x、y都到了一條重鏈上了,然後要保證x在下面。 
    add(1,dfn[y],dfn[x],z);//再只用更新他們所在的鏈就可以了。 
    return ;
}
inline int treesum(int x,int y){//將題中對樹查詢得指令改為對線段樹的查詢。 
    int ans=0;
    int tx=top[x],ty=top[y];
    while(tx!=ty){//這一段和修改幾乎一樣,就是把原本對每一個區間的修改,變為了查詢,其實都一樣。 
        if(depth[tx]<depth[ty])  swap(tx,ty),swap(x,y);
        ans=(ans+query(1,dfn[tx],dfn[x]))%mod;
        x=f[tx];
        tx=top[x],ty=top[ty];
    }
    if(depth[x]<depth[y])  swap(x,y);
    return (ans+query(1,dfn[y],dfn[x]))%mod;
}
int main(){
    in(n),in(m),in(r),in(mod);
    REP(i,1,n)  in(input[i]);
    int a,b;
    REP(i,1,n-1)  in(a),in(b),adl(a,b),adl(b,a);
    depth[r];
    getson(r,0);
    getdfn(r,r);
    build(1,1,n);
    int p,x,y,z;
    REP(i,1,m){
        in(p);
        if(p==1)  in(x),in(y),in(z),treeadd(x,y,z);
        if(p==2)  in(x),in(y),printf("%d\n",treesum(x,y));
        if(p==3)  in(x),in(z),add(1,dfn[x],dfn[x]+size[x]-1,z);//我們會發現,在樹鏈剖分中,i這顆子樹裡面所有的節點的dfn都是連續的,我們修改u的子樹就是將u到u+size-1修改就可以了。 
        if(p==4)  in(x),printf("%d\n",query(1,dfn[x],dfn[x]+size[x]-1));//查詢同上。 
    }
    return 0;
}