樹鏈剖分詳解

轉載請註明出處,部份內容引自banananana大神的博客html



別說你不知道什麼是樹╮(─▽─)╭(幫你百度一下node

前置知識:  dfs序  線段樹ios



先來回顧兩個問題:
1,將樹從x到y結點最短路徑上全部節點的值都加上z數組

這也是個模板題了吧數據結構

咱們很容易想到,樹上差分能夠以O(n+m)的優秀複雜度解決這個問題ide

2,求樹從x到y結點最短路徑上全部節點的值之和優化

lca大水題,咱們又很容易地想到,dfs O(n)預處理每一個節點的dis(即到根節點的最短路徑長度)ui

而後對於每一個詢問,求出x,y兩點的lca,利用lca的性質distance ( x , y ) = dis ( x ) + dis ( y ) - 2 * dis ( lca )求出結果spa

時間複雜度O(mlogn+n)3d

如今來思考一個bug:
若是剛纔的兩個問題結合起來,成爲一道題的兩種操做呢?

剛纔的方法顯然就不夠優秀了(每次詢問以前要跑dfs更新dis)



樹鏈剖分華麗登場
樹剖是經過輕重邊剖分將樹分割成多條鏈,而後利用數據結構來維護這些鏈(本質上是一種優化暴力)

 

首先明確概念:

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

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

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

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

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

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

 

好比上面這幅圖中,用黑線鏈接的結點都是重結點,其他均是輕結點,

2-11就是重鏈,2-5就是輕鏈,用紅點標記的就是該結點所在重鏈的起點,也就是下文提到的top結點,

還有每條邊的值實際上是進行dfs時的執行序號。


變量聲明:

const int maxn=1e5+10; struct edge{ int next,to; }e[2*maxn]; struct Node{ int sum,lazy,l,r,ls,rs; }node[2*maxn]; int rt,n,m,r,a[maxn],cnt,head[maxn],f[maxn],d[maxn],size[maxn],son[maxn],rk[maxn],top[maxn],id[maxn];
名稱 解釋
f[u] 保存結點u的父親節點
d[u] 保存結點u的深度值
size[u] 保存以u爲根的子樹節點個數
son[u] 保存重兒子
rk[u] 保存當前dfs標號在樹中所對應的節點
top[u] 保存當前節點所在鏈的頂端節點
id[u] 保存樹中每一個節點剖分之後的新編號(DFS的執行順序)

 


咱們要作的就是(樹鏈剖分的實現):


1,對於一個點咱們首先求出它所在的子樹大小,找到它的重兒子(即處理出size,son數組),
解釋:好比說點1,它有三個兒子2,3,4

2所在子樹的大小是5

3所在子樹的大小是2

4所在子樹的大小是6

那麼1的重兒子是4

ps:若是一個點的多個兒子所在子樹大小相等且最大

那隨便找一個當作它的重兒子就行了

葉節點沒有重兒子,非葉節點有且只有一個重兒子


2,在dfs過程當中順便記錄其父親以及深度(即處理出f,d數組),操做1,2能夠經過一遍dfs完成

void dfs1(int u,int fa,int depth)    //當前節點、父節點、層次深度
{ f[u]=fa; d[u]=depth; size[u]=1;    //這個點自己size=1
    for(int i=head[u];i;i=e[i].next) { int v=e[i].to; if(v==fa) continue; dfs1(v,u,depth+1);    //層次深度+1
        size[u]+=size[v];    //子節點的size已被處理,用它來更新父節點的size
        if(size[v]>size[son[u]]) son[u]=v;    //選取size最大的做爲重兒子
 } } //進入
dfs1(root,0,1);

dfs跑完大概是這樣的,你們能夠手動模擬一下


3,第二遍dfs,而後鏈接重鏈,同時標記每個節點的dfs序,而且爲了用數據結構來維護重鏈,咱們在dfs時保證一條重鏈上各個節點dfs序連續(即處理出數組top,id,rk)

void dfs2(int u,int t)    //當前節點、重鏈頂端
{ top[u]=t; id[u]=++cnt;    //標記dfs序
    rk[cnt]=u;    //序號cnt對應節點u
    if(!son[u]) return; 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必然是它自己
 } }

dfs跑完大概是這樣的,你們能夠手動模擬一下


4,兩遍dfs就是樹鏈剖分的主要處理,經過dfs咱們已經保證一條重鏈上各個節點dfs序連續,那麼能夠想到,咱們能夠經過數據結構(以線段樹爲例)來維護一條重鏈的信息
回顧上文的那個題目,修改和查詢操做原理是相似的,以查詢操做爲例,其實就是個LCA,不過這裏使用了top來進行加速,由於top能夠直接跳轉到該重鏈的起始結點,輕鏈沒有起始結點之說,他們的top就是本身。須要注意的是,每次循環只能跳一次,而且讓結點深的那個來跳到top的位置,避免兩個一塊兒跳從而擦肩而過。

int sum(int x,int y) { int ans=0,fx=top[x],fy=top[y]; while(fx!=fy)    //兩點不在同一條重鏈
 { if(d[fx]>=d[fy]) { ans+=query(id[fx],id[x],rt);    //線段樹區間求和,處理這條重鏈的貢獻
            x=f[fx],fx=top[x];    //將x設置成原鏈頭的父親結點,走輕邊,繼續循環
 } else { ans+=query(id[fy],id[y],rt); y=f[fy],fy=top[y]; } } //循環結束,兩點位於同一重鏈上,但兩點不必定爲同一點,因此咱們還要統計這兩點之間的貢獻
    if(id[x]<=id[y]) ans+=query(id[x],id[y],rt); else ans+=query(id[y],id[x],rt); return ans; }

你們若是明白了樹鏈剖分,也應該有觸類旁通的能力(反正我沒有),修改和LCA就留給你們本身完成了


5,樹鏈剖分的時間複雜度
樹鏈剖分的兩個性質:

1,若是(u, v)是一條輕邊,那麼size(v) < size(u)/2;

2,從根結點到任意結點的路所通過的輕重鏈的個數一定都小於logn;

能夠證實,樹鏈剖分的時間複雜度爲O(nlog^2n)


幾道例題:
1,樹鏈剖分模板
就是剛纔講的
上代碼:

 

#include<iostream>
#include<cstdio>
#define int long long
using namespace std;
const int maxn=1e5+10;
struct edge{
    int next,to;
}e[maxn*2];
struct node{
    int l,r,ls,rs,sum,lazy;
}a[maxn*2];
int n,m,r,rt,mod,v[maxn],head[maxn],cnt,f[maxn],d[maxn],son[maxn],size[maxn],top[maxn],id[maxn],rk[maxn];
void add(int x,int y)
{
    e[++cnt].next=head[x];
    e[cnt].to=y;
    head[x]=cnt;
}
void dfs1(int x)
{
    size[x]=1,d[x]=d[f[x]]+1;
    for(int v,i=head[x];i;i=e[i].next)
        if((v=e[i].to)!=f[x])
        {
            f[v]=x,dfs1(v),size[x]+=size[v];
            if(size[son[x]]<size[v])
                son[x]=v;
        }
}
void dfs2(int x,int tp)
{
    top[x]=tp,id[x]=++cnt,rk[cnt]=x;
    if(son[x])
        dfs2(son[x],tp);
    for(int v,i=head[x];i;i=e[i].next)
        if((v=e[i].to)!=f[x]&&v!=son[x])
            dfs2(v,v);
}
inline void pushup(int x)
{
    a[x].sum=(a[a[x].ls].sum+a[a[x].rs].sum)%mod;
}
void build(int l,int r,int x)
{
    if(l==r)
    {
        a[x].sum=v[rk[l]],a[x].l=a[x].r=l;
        return;
    }
    int mid=l+r>>1;
    a[x].ls=cnt++,a[x].rs=cnt++;
    build(l,mid,a[x].ls),build(mid+1,r,a[x].rs);
    a[x].l=a[a[x].ls].l,a[x].r=a[a[x].rs].r;
    pushup(x);
}
inline int len(int x)
{
    return a[x].r-a[x].l+1;
}
inline void pushdown(int x)
{
    if(a[x].lazy)
    {
        int ls=a[x].ls,rs=a[x].rs,lz=a[x].lazy;
        (a[ls].lazy+=lz)%=mod,(a[rs].lazy+=lz)%=mod;
        (a[ls].sum+=lz*len(ls))%=mod,(a[rs].sum+=lz*len(rs))%=mod;
        a[x].lazy=0;
    }
}
void update(int l,int r,int c,int x)
{
    if(a[x].l>=l&&a[x].r<=r)
    {
        (a[x].lazy+=c)%=mod,(a[x].sum+=len(x)*c)%=mod;
        return;
    }
    pushdown(x);
    int mid=a[x].l+a[x].r>>1;
    if(mid>=l)
        update(l,r,c,a[x].ls);
    if(mid<r)
        update(l,r,c,a[x].rs);
    pushup(x);
}
int query(int l,int r,int x)
{
    if(a[x].l>=l&&a[x].r<=r)
        return a[x].sum;
    pushdown(x);
    int mid=a[x].l+a[x].r>>1,tot=0;
    if(mid>=l)
        tot+=query(l,r,a[x].ls);
    if(mid<r)
        tot+=query(l,r,a[x].rs);
    return tot%mod;
}
inline int sum(int x,int y)
{
    int ret=0;
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]])
            swap(x,y);
        (ret+=query(id[top[x]],id[x],rt))%=mod;
        x=f[top[x]];
    }
    if(id[x]>id[y])
        swap(x,y);
    return (ret+query(id[x],id[y],rt))%mod;
}
inline void updates(int x,int y,int c)
{
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]])
            swap(x,y);
        update(id[top[x]],id[x],c,rt);
        x=f[top[x]];
    }
    if(id[x]>id[y])
        swap(x,y);
    update(id[x],id[y],c,rt);
}
signed main()
{
    scanf("%lld%lld%lld%lld",&n,&m,&r,&mod);
    for(int i=1;i<=n;i++)
        scanf("%lld",&v[i]);
    for(int x,y,i=1;i<n;i++)
    {
        scanf("%lld%lld",&x,&y);
        add(x,y),add(y,x);
    }
    cnt=0,dfs1(r),dfs2(r,r);
    cnt=0,build(1,n,rt=cnt++);
    for(int op,x,y,k,i=1;i<=m;i++)
    {
        scanf("%lld",&op);
        if(op==1)
        {
            scanf("%lld%lld%lld",&x,&y,&k);
            updates(x,y,k);
        }
        else if(op==2)
        {
            scanf("%lld%lld",&x,&y);
            printf("%lld\n",sum(x,y));
        }
        else if(op==3)
        {
            scanf("%lld%lld",&x,&y);
            update(id[x],id[x]+size[x]-1,y,rt);
        }
        else
        {
            scanf("%lld",&x);
            printf("%lld\n",query(id[x],id[x]+size[x]-1,rt));
        }
    }
    return 0;
}
View Code

 

 

2,[NOI2015]軟件包管理器
觀察到題目要求支持兩種操做

1,install x:表示安裝軟件包x

2,uninstall x:表示卸載軟件包x

對於操做一,咱們能夠統計x到根節點未安裝的軟件包的個數,而後區間修改成已安裝

對於操做二,咱們能夠統計x所在子樹已安裝軟件包的個數,而後將子樹修改成未安裝
上代碼:

#include<iostream> #include<cstdio>
#define int long long
using namespace std; const int maxn=1e5+10; struct edge{ int next,to; }e[2*maxn]; struct Node{ int l,r,ls,rs,sum,lazy; }node[2*maxn]; int rt,n,m,cnt,head[maxn]; int f[maxn],d[maxn],size[maxn],son[maxn],rk[maxn],top[maxn],tid[maxn]; int readn() { int x=0; char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+ch-'0'; ch=getchar(); } return x; } void add_edge(int x,int y) { e[++cnt].next=head[x]; e[cnt].to=y; head[x]=cnt; } void dfs1(int u,int fa,int depth) { f[u]=fa; d[u]=depth; size[u]=1; for(int i=head[u];i;i=e[i].next) { int v=e[i].to; if(v==fa) continue; dfs1(v,u,depth+1); size[u]+=size[v]; if(size[v]>size[son[u]]||!son[u]) son[u]=v; } } void dfs2(int u,int t) { top[u]=t; tid[u]=++cnt; rk[cnt]=u; if(!son[u]) return; dfs2(son[u],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); } } void pushup(int x) { int lson=node[x].ls,rson=node[x].rs; node[x].sum=node[lson].sum+node[rson].sum; node[x].l=node[lson].l; node[x].r=node[rson].r; } void build(int li,int ri,int cur) { if(li==ri) { node[cur].ls=node[cur].rs=node[cur].lazy=-1; node[cur].l=node[cur].r=li; return; } int mid=(li+ri)>>1; node[cur].ls=cnt++; node[cur].rs=cnt++; build(li,mid,node[cur].ls); build(mid+1,ri,node[cur].rs); pushup(cur); } void pushdown(int x) { int lson=node[x].ls,rson=node[x].rs; node[lson].sum=node[x].lazy*(node[lson].r-node[lson].l+1); node[rson].sum=node[x].lazy*(node[rson].r-node[rson].l+1); node[lson].lazy=node[x].lazy; node[rson].lazy=node[x].lazy; node[x].lazy=-1; } void update(int li,int ri,int c,int cur) { if(li<=node[cur].l&&node[cur].r<=ri) { node[cur].sum=c*(node[cur].r-node[cur].l+1); node[cur].lazy=c; return; } if(node[cur].lazy!=-1) pushdown(cur); int mid=(node[cur].l+node[cur].r)>>1; if(li<=mid) update(li,ri,c,node[cur].ls); if(mid<ri) update(li,ri,c,node[cur].rs); pushup(cur); } int query(int li,int ri,int cur) { if(li<=node[cur].l&&node[cur].r<=ri) return node[cur].sum; if(node[cur].lazy!=-1) pushdown(cur); int tot=0; int mid=(node[cur].l+node[cur].r)>>1; if(li<=mid) tot+=query(li,ri,node[cur].ls); if(mid<ri) tot+=query(li,ri,node[cur].rs); return tot; } int sum(int x) { int ans=0; int fx=top[x]; while(fx) { ans+=tid[x]-tid[fx]-query(tid[fx],tid[x],rt)+1; update(tid[fx],tid[x],1,rt); x=f[fx]; fx=top[x]; } ans+=tid[x]-tid[0]-query(tid[0],tid[x],rt)+1; update(tid[0],tid[x],1,rt); return ans; } signed main() { n=readn(); for(int i=1;i<n;i++) { int x=readn(); add_edge(x,i); add_edge(i,x); } cnt=0; dfs1(0,-1,1); dfs2(0,0); cnt=0; rt=cnt++; build(1,n,rt); m=readn(); for(int i=1;i<=m;i++) { int x; string op; cin>>op; x=readn(); if(op=="install") printf("%lld\n",sum(x)); else if(op=="uninstall") { printf("%lld\n",query(tid[x],tid[x]+size[x]-1,rt)); update(tid[x],tid[x]+size[x]-1,0,rt); } } return 0; }
View Code

 

3,[SDOI2011]染色
有一些思惟含量的題

統計顏色段數量時不能簡單地區間加法

線段樹還應維護區間最左顏色和區間最右顏色

合併時

若是S(l,k)的右端與S(k+1,r)的左端顏色相同,那麼S(l,r)=S(l,k)+S(k+1,r)-1(減去重複的那一個)

不然S(l,r)=S(l,k)+S(k+1,r)正常合併

相關文章
相關標籤/搜索