樹鏈剖分小節

來源:互聯網
上載者:User

前段時間學習了下樹鏈剖分,好久沒看了,今天又複習一遍,趕緊寫下來,別又忘了。

我們在資訊學競賽中,有時會碰到這麼一類題型,在一棵樹中,修改兩點之間路徑上的所有邊(或點)上的某個變數(如邊的長度,點的權值等等),然後詢問單個點(或邊)或者兩點之間路徑上的所有點(或邊)的某些性質(如邊權之和,最大邊最小邊等等)。對於這樣的題,往往容易往線段樹上去靠,但是,單單是用線段樹是無法維護每一條鏈的性質的,所以我們需要一種演算法將樹鏈分開來,使得每條鏈可以和線段樹中的一個區間一一對應上。(當然樹鏈剖分遠遠不止這些簡單的應用,也不一定要和線段樹有什麼關係,總之就是將樹鏈剖分開來吧)。

樹鏈剖分有很多種剖分方法,最常用的應該就是輕重邊剖分了吧(在網上大部分介紹的都是這種剖分方法),什麼是輕重邊剖分呢?

我們首先將樹中的邊分為兩部分,輕邊和重邊,記size(U)為以U為根的子樹的節點的個數,令V為U的兒子中size最大的一個(如有多個最大,只取一個),則我們說邊(U,V)為重邊,其餘的邊為輕邊(如所示紅色為重邊,藍色為輕邊)。


我們將一棵樹的所有邊按上述方法分成輕邊和重邊後,我們可以得到以下幾個性質:

1:若(U,V)為輕邊,則size(V)<=size(U)/2。

這是顯然的。

2:從根到某一點的路徑上輕邊的個數不會超過O(logN),(N為節點總數)。

這也是很簡單,因為假設從跟root到v的路徑有k條輕邊,它們是 root->...->v1->...->v2->......->vk->...->v,我們設size(v)=num,顯然num>=1,則由性質1,我們有size(Vk)>=2,size(Vk-1)>=4......size(v1)>=2^k,顯然有2^k<=N,所以k<=log2(N)。


如果我們把一條鏈中的連續重邊連起來,成為重鏈,則一條鏈就變成了輕邊與重鏈交替分段的鏈,且段數是log(N)層級的,則我們可以講重鏈放線上段樹中維護,輕邊可放可不放,為了方便我一般還是放,但是速度就會打一點折扣了。思路就是這麼多,接下來就是具體實現了。

我們需要維護一下值:

siz[v]表示以v為根的子樹的節點總數。

dep[v]表示v的深度。

son[v]表示與v在同一重鏈上的v的兒子節點。

fa[v]表示v的父親節點。

top[v]表示v所在鏈的頂端節點。

w[v]表示節點v線上段樹中的位置。

siz[],son[],fa[],dep[]可以在第一遍dfs中求出來,top[],w[]可在第二遍dfs中求出來。具體過程看代碼吧。

struct edge{    int to;    int next;}e[maxn<<1];int box[maxn],cnt,tot;int siz[maxn],top[maxn],son[maxn],dep[maxn],fa[maxn];void init(){    tot=0;    son[0]=dep[0]=0;    memset(box,-1,sizeof(box));    cnt=0;}void add(int from,int to){    e[cnt].to=to;    e[cnt].next=box[from];    box[from]=cnt++;}void dfs(int now,int pre){    siz[now]=1;    fa[now]=pre;    son[now]=0;    dep[now]=dep[pre]+1;    int t,v;    for(t=box[now];t+1;t=e[t].next)    {        v=e[t].to;        if(v!=pre)        {            dfs(v,now);            siz[now]+=siz[v];            if(siz[son[now]]<siz[v])            {                son[now]=v;            }        }    }}void dfs2(int now,int tp){    top[now]=tp;    if(son[now])    dfs2(son[now],top[now]);    int t,v;    for(t=box[now];t+1;t=e[t].next)    {        v=e[t].to;        if(v!=fa[now]&&v!=son[now])        dfs2(v,v);    }}

以上是剖分過程,關於如何在樹鏈剖分後維護兩點間路徑的資訊,請看這裡LCA的樹鏈剖分實現

這裡需要注意的是,對於有些題要修改的權值或詢問的權值在點上,有的在邊上,這在剖分時雖然過程沒有變,但在處理的時候是有區別的,具體不同我想在下面兩道題裡體現。

權值在邊上的情況。

http://codeforces.com/problemset/problem/165/D

codeforces 165D Beard Graph

題意:給一棵樹,樹的每條邊有一種顏色,黑色或白色,一開始所有邊均為黑色,有兩個操作:

操作1:將第i條邊變成白色或將第i條邊變成黑色。

操作2 :詢問u,v兩點之間僅經過黑色變的最短距離。

思路:其實這道題可以不用樹鏈剖分,存在更高效的方法,但是一時又想不到更好的例子。

因為是一棵樹,所以兩點之間的路徑是確定的,所以只需要判斷路徑中是否所有的邊均為黑色邊即可,全是黑邊意味著沒有白邊,所以我們可以這麼做,我們將每條邊剖分放入線段樹中後,初始時將所有邊權設為0,對操作1,如果要將一條邊改為黑色,則將線段樹賦值為零,否則分值為1,然後對於操作2,我們只要看兩點間路徑是否權之和為0即可,若為0,返回兩點間距離,否則返回0。

上代碼:

#include <iostream>#include <string.h>#include <stdio.h>#include <algorithm>#define maxn 100010using namespace std;#define mid ((t[p].l+t[p].r)>>1)#define ls (p<<1)#define rs (ls|1)struct tree{    int l,r;    int sum;}t[maxn<<2];void pushup(int p){    t[p].sum=t[ls].sum+t[rs].sum;}void build(int p,int l,int r){    t[p].l=l,t[p].r=r,t[p].sum=0;    if(l==r)    return;    build(ls,l,mid);    build(rs,mid+1,r);}void add(int p,int x,int val){    if(t[p].l==t[p].r)    {        t[p].sum+=val;        return;    }    if(x<=mid)    add(ls,x,val);    else    add(rs,x,val);    pushup(p);}int query(int p,int l,int r){    if(t[p].l==l&&t[p].r==r)    {        return t[p].sum;    }    if(l>mid)    return query(rs,l,r);    else if(r<=mid)    return query(ls,l,r);    else    return query(ls,l,mid)+query(rs,mid+1,r);}int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn];struct edge{    int to;    int next;}e[maxn<<1];int box[maxn],cnt,tot;void init(){    tot=0;    son[0]=dep[0]=0;    memset(box,-1,sizeof(box));    cnt=0;}void add(int from,int to){    e[cnt].to=to;    e[cnt].next=box[from];    box[from]=cnt++;}void dfs(int now,int pre){    siz[now]=1;    fa[now]=pre;    son[now]=0;    dep[now]=dep[pre]+1;    int t,v;    for(t=box[now];t+1;t=e[t].next)    {        v=e[t].to;        if(v!=pre)        {            dfs(v,now);            siz[now]+=siz[v];            if(siz[son[now]]<siz[v])            {                son[now]=v;            }        }    }}void dfs2(int now,int tp){    w[now]=++tot;    top[now]=tp;    if(son[now])    dfs2(son[now],top[now]);    int t,v;    for(t=box[now];t+1;t=e[t].next)    {        v=e[t].to;        if(v!=fa[now]&&v!=son[now])        dfs2(v,v);    }}int solve(int a,int b){    int f1=top[a],f2=top[b],dist=0;    while(f1!=f2)    {        if(dep[f1]<dep[f2])        {            swap(f1,f2);            swap(a,b);        }        dist+=w[a]-w[f1]+1;        int tmp=query(1,w[f1],w[a]);        if(tmp)        return -1;        a=fa[f1];        f1=top[a];    }    if(a==b)    return dist;//注意這裡    else    {        if(dep[a]>dep[b])        swap(a,b);        int tmp=query(1,w[son[a]],w[b]);//注意這裡        if(tmp)        return -1;        return dist+w[b]-w[a];    }}int Edge[maxn][2];int main(){    int n,q,i,a,b;    scanf("%d",&n);    init();    for(i=1;i<n;i++)    {        scanf("%d%d",&Edge[i][0],&Edge[i][1]);        add(Edge[i][0],Edge[i][1]);        add(Edge[i][1],Edge[i][0]);    }    build(1,1,n);    dfs(1,0);    dfs2(1,1);    scanf("%d",&q);    while(q--)    {        int k;        scanf("%d",&k);        if(k==3)        {            scanf("%d%d",&a,&b);            printf("%d\n",solve(a,b));        }        else        {            scanf("%d",&i);            int tmp;            if(dep[Edge[i][0]]>dep[Edge[i][1]])            tmp=Edge[i][0];            else            tmp=Edge[i][1];            if(k==1)            add(1,w[tmp],-1);            else            add(1,w[tmp],1);        }    }    return 0;}

權值在點上的情況:

http://acm.hdu.edu.cn/showproblem.php?pid=3966

HDU:3966 Aragorn's Story

題意:題意很明白,給一棵樹,將兩點之間的路徑中的所有點的權值增加或減少一個數,詢問特定點當前的權值大小。

思路:思路應該很清晰了,將樹剖分後放進線段樹中維護。

代碼如下:

#pragma comment(linker,"/STACK:100000000,100000000")#include <iostream>#include <string.h>#include <stdio.h>#include <algorithm>#define maxn 50010using namespace std;#define mid ((t[p].l+t[p].r)>>1)#define ls (p<<1)#define rs (ls|1)struct tree{    int l,r;    int lazy;}t[maxn<<2];int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn],num[maxn],tt[maxn];void pushdown(int p){    if(t[p].lazy)    {        t[ls].lazy+=t[p].lazy;        t[rs].lazy+=t[p].lazy;        t[p].lazy=0;    }}void build(int p,int l,int r){    t[p].l=l,t[p].r=r,t[p].lazy=0;    if(l==r)    {        t[p].lazy=num[tt[l]];        return;    }    build(ls,l,mid);    build(rs,mid+1,r);}void add(int p,int l,int r,int val){    if(t[p].l==l&&t[p].r==r)    {        t[p].lazy+=val;        return;    }    pushdown(p);    if(r<=mid)    add(ls,l,r,val);    else if(l>mid)    add(rs,l,r,val);    else    {        add(ls,l,mid,val);        add(rs,mid+1,r,val);    }}int query(int p,int x){    if(t[p].l==t[p].r)    {        return t[p].lazy;    }    pushdown(p);    if(x>mid)    return query(rs,x);    else    return query(ls,x);}struct edge{    int to;    int next;}e[maxn<<1];int box[maxn],cnt,tot;void init(){    tot=0;    son[0]=dep[0]=0;    memset(box,-1,sizeof(box));    cnt=0;}void add(int from,int to){    e[cnt].to=to;    e[cnt].next=box[from];    box[from]=cnt++;}void dfs(int now,int pre){    siz[now]=1;    fa[now]=pre;    son[now]=0;    dep[now]=dep[pre]+1;    int t,v;    for(t=box[now];t+1;t=e[t].next)    {        v=e[t].to;        if(v!=pre)        {            dfs(v,now);            siz[now]+=siz[v];            if(siz[son[now]]<siz[v])            {                son[now]=v;            }        }    }}void dfs2(int now,int tp){    w[now]=++tot;    tt[tot]=now;    top[now]=tp;    if(son[now])    dfs2(son[now],top[now]);    int t,v;    for(t=box[now];t+1;t=e[t].next)    {        v=e[t].to;        if(v!=fa[now]&&v!=son[now])        dfs2(v,v);    }}void solve(int a,int b,int val){    int f1=top[a],f2=top[b];    while(f1!=f2)    {        if(dep[f1]<dep[f2])        {            swap(f1,f2);            swap(a,b);        }        add(1,w[f1],w[a],val);        a=fa[f1];        f1=top[a];    }    if(a==b)    {        add(1,w[a],w[a],val);//注意這裡    }    else    {        if(dep[a]>dep[b])        swap(a,b);        add(1,w[a],w[b],val);//注意這裡    }}int main(){    freopen("dd.txt","r",stdin);    int n,m,q,a,b,c;    char str[2];    while(scanf("%d%d%d",&n,&m,&q)!=EOF)    {        init();        int i;        for(i=1;i<=n;i++)        {            scanf("%d",&num[i]);        }        for(i=1;i<=m;i++)        {            scanf("%d%d",&a,&b);            add(a,b);            add(b,a);        }        dfs(1,0);        dfs2(1,1);        build(1,1,n);        while(q--)        {            int node;            scanf("%s",str);            if(str[0]=='Q')            {                scanf("%d",&node);                printf("%d\n",query(1,w[node]));            }            else            {                scanf("%d%d%d", &a,&b,&c);                if(str[0]=='I')                solve(a,b,c);                else                solve(a,b,-c);            }        }    }    return 0;}

我已將需要注意的地方在代碼中標記下來了,

區別就是在修改最後一條鏈時,也就是a,b在同一條重鏈中時,我們不妨設dep[a]<=dep[b],這時我們知道a是原來我們要求的v,w兩點的LCA。因為我們樹鏈剖分時,將重鏈放入線段樹中時,事實上將點與邊一一對應了,每個點對應於其父節點與其串連的邊,對於根節點,可設定一個虛擬節點,把它看成根節點的父節點。這樣在放入線段樹中的操作就可以不變(其實還是為了實現方便)。如果權值在邊上,那麼我要求v,w兩點間的路徑時,其LCA所對應的邊並不在這條路徑裡,所以我們要少更新一條邊。如果權值在點上,則LCA顯然也在v與w之間的路徑中,則需要更新LCA。這就是兩種題的不同點。

PS:其實樹鏈剖分還有好多應用還有拓展,不過本弱菜還沒有學得到,這裡只是將最基本的應用總結出來,希望各位神牛不要BS。

PS2:DFS寫法容易爆棧,所以還有非遞迴寫法,如BFS寫法和類比棧等等,不過我還沒研究出來。。。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.