1. 程式人生 > >樹鏈剖分新手正確的入門姿勢 附帶dfs序介紹 —— 詳細證明一下一些結論

樹鏈剖分新手正確的入門姿勢 附帶dfs序介紹 —— 詳細證明一下一些結論

part one、dfs序/時間戳

dfs序就是按照樹的先序遍歷的順序,為每個點記錄下進入/最後一次出去這個點的時間。

dfs序是維護一個樹基本套路之一,有一些基本的用處(蒟蒻我知道的):

1.樹結構線性化,主要用於確定子樹的範圍。比如例題:

2.樹鏈的劃分,樹鏈剖分中用於將重節連續標號轉化為重鏈。(下面會有講)

3.別的蒟蒻就不會了。

就不貼程式碼了,參考上面給的題解連結裡的AC程式碼。

part two、樹鏈剖分

本篇重點是樹鏈剖分。 百科上對樹鏈剖分的描述:指一種對樹進行劃分的演算法,它先通過輕重邊剖分將樹分為多條鏈,保證每個點屬於且只屬於一條鏈,然後再通過資料結構(樹狀陣列、SBT、SPLAY、線段樹等)來維護每一條鏈。

最經典的例子就是動態修改節點值,查詢從某個節點到另一節點路徑上點權和。

網上有大量的講解,但對我這種蒟蒻來說非常新手不友好。那麼我來總結一下原理。

題意:給一棵樹,並給定各個點權的值,然後有3種操作:

I  C1 C2 K: 把C1與C2的路徑上的所有點權值加上K

D C1 C2 K:把C1與C2的路徑上的所有點權值減去K

Q C:查詢節點編號為C的權值

資料量為5e4

(程式碼參考kuangbin的模板)

大家思考一下這個題怎麼處理,然後下面進行詳細討論。

前驅知識點:dfs序、dfs、線段樹/樹狀陣列、前向星(大家都是用的前向星存圖我也不知道為什麼,我只好跟風了)

----------------------回到正題------------------

天生我才必有用?

線段樹、主席樹都是利用二叉樹的性質。區間和樹都是比較完美的。

可是,如果給出一棵樹不是二叉樹(可能是任意的樹),而常規的樹(二叉樹)都是利用深度是節點個數/葉子個數的高階無窮小(logn)來簡化計算量的。而任意的樹可能度非常大,也可能深度特別大,樹鏈不完美,線段樹家族處理某條子鏈的能力不強,則事情就麻煩了。

輕、重節點:兄弟節點中,子樹結點數目最多的結點,其他的兄弟節點都為輕節點

重鏈:將連續的重節點連線在一起就是一條重鏈,最上面的重鏈的父親也算在重鏈內。

輕鏈:除了重節點以外的輕節點就是輕鏈,長度必為1且為葉子

,我們將會在下圖後面詳細分析。

樹鏈剖分的核心思想是:重鏈是引起樹複雜(深度過大)的原因,以重鏈為單位建立線段樹維護(或者其他資料結構),其他輕節點暴力向重鏈轉移。這樣就相當於在樹上建立了重鏈組成的高鐵,輕節點轉移到重鏈上之後就可以搭高鐵了(搭高鐵的意思是,轉移到重鏈之後,重鏈只需要查詢(修改)一次就可以查詢(修改)到結果)。

請允許我畫一個醜陋的

上圖中紅色為重節點,黑色為重鏈(也就是所謂的高鐵),注意我們邏輯上重鏈要包含最上面的父親。

有結論:

重鏈的個數不超過logn。

(網上說的是輕、重鏈的個數不超過logn,這至少在我們的定義中是錯誤的,舉個反例就是深度為2,有100個葉子的樹)

順帶把輕鏈必長度為1且為葉子一塊解釋了:

我們根據重鏈的定義可知:每個節點的孩子中必有一個重節點,這很好理解。而我們把重節點上面的父親節點算作重鏈的開頭第一個節點,所以:只要節點有子孫,則一定劃歸於重鏈,換句話說就是:不是葉子的節點,就是重鏈的一部分(要麼是重節點,要麼是重節點的父親)。顯然,不可能有兩個葉子相連,則輕鏈長度只能為1.

重要的話再用紅字寫一遍,輕鏈長度為1。

證明重鏈的個數不超過logn:

(類似反證法)

    我們經過分析發現對於一個非葉子節點:要麼它是重鏈,要麼在重鏈最上端,也就是說對每個非輕鏈的節點都存在一條經過它或者從它開始,直達一個葉子的重鏈。那麼要使重鏈儘可能的多,必須要使得樹不斷分叉(如果不的話就只有一條直達葉子的重鏈),分叉越多層數就越少,而我們知道每組兄弟中,只有一個是重節點,則分叉不能太多,那麼就是二叉,而對於平衡二叉樹最多logn級別的,這是重鏈最多的情況,普通情況重鏈自然就小於logn了。

為什麼可以這麼做?(複雜度分析):

我們樹鏈剖分核心思想就是重鏈用資料結構維護,輕鏈儘可能搭重鏈便車來簡化運算,而輕鏈長度最多為1。這樣,輕鏈只需一次就可以到達重鏈,而重鏈個數不超過logn,所以重鏈之間的輾轉也就不超過logn次(如下圖黃色為輕鏈向根節點的移動軌跡),所以總體時間複雜度為logn。

重鏈如何成為快速的“高鐵”?——dfs序+資料結構維護

再重複一遍之前得到的結論:輕鏈只需一次就能移動到重鏈,重鏈之間輾轉不超過logn次

我們接下來分析重鏈為什麼能、如何加速整個資料結構。也就是重鏈內部是如何傳遞的。

所以要回答這個問題,就是要回答如何用資料結構維護重鏈。比如例題是對鏈進行加減查詢操作,所以我們選擇樹狀陣列(或者線段樹)來維護重鏈。(再把例題貼一遍:)

題意:給一棵樹,並給定各個點權的值,然後有3種操作:

I  C1 C2 K: 把C1與C2的路徑上的所有點權值加上K

D C1 C2 K:把C1與C2的路徑上的所有點權值減去K

Q C:查詢節點編號為C的權值

資料量為5e4

(程式碼參考kuangbin的模板)

上面我們已經說了,輕鏈向重鏈靠攏,重鏈使用資料結構維護。要用樹狀陣列維護重鏈,就要先將重鏈連續標號。也就是線性化。

這裡使用dfs序將樹上節點重新標號。從根節點出發,按照優先遍歷重節點的順序,先序遍歷整棵樹,記錄下每個節點的時間戳。這樣我們發現,由於是優先遍歷重節點,則同一個重鏈內部一定是連續序號的。只要重鏈內部序號連續,就可以用樹狀陣列維護區間特性了。

具體操作

當然不同題目要求,我們採取的資料結構維護也不一樣,但有一些基本操作。以上述例題為例。

定義資料和初始化:

#include<bits/stdc++.h> #include<cstring> using namespace std;  int const maxn=5e4+10;//資料量 /*              樹的記憶體和結構               */  struct Edge{    //前向星資料結構(我按照鄰接表理解的,下面我都按照鄰接表來講)      int to;           //鄰接表條目      int next;       //指向下一個節點的指標。  }edge[maxn*2];    //鄰接表記憶體池 ,要開足夠大。  int head[maxn],tot;    //head是鄰接表頭指標。tot是記憶體池分配指標,初始為0;

/*            下面是節點、鏈的資訊            */ int top[maxn];    //top[v] 記錄節點v所在重鏈的頂端節點。頂端節點應為輕節點(重節點的父親) int fa[maxn];      //記錄節點的父親節點(前驅) int deep[maxn];    //記錄節點深度 int num[maxn];    //num[v]表示以v為根的子樹節點數。  int p[maxn];//p[v]表示v對應的位置(節點對應的dfs序) int fp[maxn];//與p陣列相反。(dfs序對應的節點號)  int son[maxn];  //重兒子。

int pos; 

void init(){    //初始化     tot=0;     memset(head,-1,sizeof(head));     pos=1;//使用樹狀陣列,編號從1開始     memset(son,-1,sizeof(son));  } 

建立樹:

首先我們要把樹建立起來,這裡選擇用鏈式前向星(也就是記憶體池+鄰接表)的方式存圖。

/*不斷新增邊來建樹*/ void addedge(int u,int v){     //頭插法向u的鄰接表裡插入v,若無向圖則要正反都新增      edge[tot].to=v;     //從內測池edge中取一個節點空間,      edge[tot].next=head[u];    //將節點插入鄰接表,頭插法      head[u]=tot++;    //將頭節點重新指向連結串列頭部,記憶體池計數變數加一  }

初始化資訊:

第一次dfs得到的資訊有:每個點的(深度、父親、子樹點的個數、重兒子)

void dfs1(int u,int pre,int d){ //當前節點,前驅節點,深度      deep[u]=d;        //初始化深度     fa[u]=pre;        //記錄前驅     num[u]=1;         //子樹點個數統計,算上自己的1     for(int i=head[u]; i!=-1 ;i=edge[i].next){    //遍歷u的所有兒子         int v=edge[i].to; //兒子         if(v!=pre){             dfs1(v,u,d+1);  //遞迴              num[u]+=num[v]; //加上兒子的子樹節點個數             if(son[u]==-1 || num[v]>num[son[u]]){ //尋找重兒子                 son[u] = v;              }         }     } }

設定dfs序並將重節點串成重鏈:

/*第二次dfs優先遍歷重節點設定dfs序,連線重鏈,並尋找每個節點(如果在重鏈上)所在重鏈的頭部*/ void getpos(int u,int sp){    //當前節點,所在重鏈頭部。      top[u]=sp;    //統計所在重鏈頭部     p[u]=pos++;    //記錄節點號對應的dfs序      fp[p[u]]=u;    //記錄dfs序對應的節點號      if(son[u] == -1)    //因為只有葉子沒有重兒子,所以用來判斷是否為葉子。          return;     getpos(son[u],sp);//優先遞迴遍歷重兒子,重兒子重鏈頭部跟自己一樣,所以直接填sp      for(int i=head[u] ; i!=-1;i=edge[i].next){    //遍歷輕兒子          int v=edge[i].to; //輕兒子          if(v!=son[u] && v!=fa[u]){    //確保是輕兒子             getpos(v,v); //輕兒子要麼是輕鏈(輕鏈就不用管啦),要麼是重鏈開頭,所以sp填輕兒子本身。          }     }  }

至此樹鏈剖分部分就完成了,接下來是用資料結構(樹狀陣列)維護重鏈。

樹狀陣列維護重鏈

(直接套一個裸的樹狀陣列) 

注意:柱狀陣列是建立在dfs序上的。

/*-------------------樹狀陣列-------------------*/ #define lowbit(x) (x&-x) int c[maxn];//樹 int n; int sum(int i){//求字首和      int s=0;     while(i>0){         s+=c[i];         i-=lowbit(i);     }     return s; } void add(int i,int val){     while(i<=n){         c[i]+=val;         i+=lowbit(i);     } }

題目解決部分

改變路徑上點權

對於輕鏈直接暴力改變,對於重鏈使用樹狀陣列區間更新。

/*                    解決題目                */ /*            u-->v的路徑上點的值改變val      */ void change(int u,int v,int val){     int f1=top[u],f2=top[v];//top是所在鏈起始端點(對於輕鏈就是本身嘍)      int tmp=0;     while(f1 !=f2){    //直到u和v輾轉到同一個重鏈後停止。 一段一段change          if(deep[f1]<deep[f2]){    //為了方便,使f1深度大 也就是u移動次數多              swap(f1,f2);             swap(u,v);         }         add(p[f1],val);    //由於u深度大,所以先讓u往 lca靠          add(p[u]+1,-val);//這裡的add是字尾區間加值,所以這一句把多加的字尾區間減掉,就變成了重鏈上一個區間。          u=fa[f1];    //重鏈之間輾轉          f1=top[u];    //重鏈之間輾轉      }      if(deep[u]>deep[v])        //while結束之後u和v在同一重鏈上了,然後 把最後一段change掉          swap(u,v);     add(p[u],val);        //兩個點已經在同一個重鏈上了,直接區間改變即可。      add(p[v]+1,-val); }

主函式

int a[maxn]; int main(){     #ifndef ONLINE_JUDGE     freopen("r.txt","r",stdin);     #endif     int m,q;     while(scanf("%d%d%d",&n,&m,&q)!=EOF){         int u,v;         int c1,c2,k;         char op[10];         init();         for(int i=1;i<=n;i++)             scanf("%d",&a[i]);         while(m--){             scanf("%d%d",&u,&v);             addedge(u,v);             addedge(v,u);//無向圖雙向都要加          }         dfs1(1,0,0);//根節點,根節點的father,根節點的深度         getpos(1,1);//根節點,根節點所在重鏈起始節點。         memset(c,0,sizeof(c));//樹狀陣列清零         for(int i=1;i<=n;i++){             add(p[i],a[i]);             add(p[i]+1,-a[i]);         }          while(q--){             scanf("%s",op);             if(op[0]=='Q'){                 scanf("%d",&u);                 printf("%d\n",sum(p[u]));             }             else{                 scanf("%d%d%d",&c1,&c2,&k);                 if(op[0]=='D')                     k=-k;                 change(c1,c2,k);             }         }      } }      

全部AC程式碼

#include<bits/stdc++.h> #include<cstring> using namespace std;  int const maxn=5e4+10;//資料量 /*              樹的記憶體和結構               */  struct Edge{    //前向星資料結構(我按照鄰接表理解的,下面我都按照鄰接表來講)      int to;        //鄰接表條目      int next;//指向下一個節點的指標。  }edge[maxn*2];//鄰接表記憶體池 ,要開足夠大。  int head[maxn],tot;//head是鄰接表頭指標。tot是記憶體池分配計數指標,初始為0;

/*            下面是節點、鏈的資訊            */ int top[maxn];//top[v] 記錄節點v所在重鏈的頂端節點。頂端節點應為輕節點(重節點的父親) int fa[maxn];//記錄節點的父親節點(前驅) int deep[maxn];//記錄節點深度 int num[maxn];//num[v]表示以v為根的子樹節點數。  int p[maxn];//p[v]表示v對應的位置(節點對應的dfs序) int fp[maxn];//與p陣列相反。(dfs序對應的節點號)  int son[maxn];//重兒子。

int pos;  void init(){     tot=0;     memset(head,-1,sizeof(head));     pos=1;//使用樹狀陣列,編號從1開始     memset(son,-1,sizeof(son));  } 

/*新增邊*/ void addedge(int u,int v){     //頭插法向u的鄰接表裡插入v,若無向圖則要正反都新增      edge[tot].to=v;     //從內測池edge中取一個節點空間,      edge[tot].next=head[u];    //將節點插入鄰接表,頭插法      head[u]=tot++;    //將頭節點重新指向連結串列頭部,記憶體池計數變數加一  } void dfs1(int u,int pre,int d){ //當前節點,前驅節點,深度      deep[u]=d;        //初始化深度     fa[u]=pre;        //記錄前驅     num[u]=1;         //子樹點個數統計     for(int i=head[u]; i!=-1 ;i=edge[i].next){    //遍歷u的所有兒子         int v=edge[i].to; //兒子         if(v!=pre){             dfs1(v,u,d+1);  //遞迴              num[u]+=num[v]; //加上兒子的子樹節點個數             if(son[u]==-1 || num[v]>num[son[u]]){ //尋找重兒子                 son[u] = v;              }         }     } } /*第二次dfs優先遍歷重節點設定dfs序,連線重鏈,並尋找每個節點(如果在重鏈上)所在重鏈的頭部*/ void getpos(int u,int sp){    //當前節點,所在重鏈頭部。      top[u]=sp;    //統計所在重鏈頭部     p[u]=pos++;    //記錄節點號對應的dfs序      fp[p[u]]=u;    //記錄dfs序對應的節點號      if(son[u] == -1)    //因為只有葉子沒有重兒子,所以用來判斷是否為葉子。          return;     getpos(son[u],sp);//優先遞迴遍歷重兒子,重兒子重鏈頭部跟自己一樣,所以直接填sp      for(int i=head[u] ; i!=-1;i=edge[i].next){    //遍歷輕兒子          int v=edge[i].to; //輕兒子          if(v!=son[u] && v!=fa[u]){    //確保是輕兒子             getpos(v,v); //輕兒子要麼是輕鏈(輕鏈的起始就是本身嘍),要麼是重鏈開頭,所以sp填輕兒子本身。          }     }  }

/*-------------------樹狀陣列-------------------*/ #define lowbit(x) (x&-x) int c[maxn];//樹 int n; int sum(int i){//求字首和      int s=0;     while(i>0){         s+=c[i];         i-=lowbit(i);     }     return s; } void add(int i,int val){//字尾區間加一個值      while(i<=n){         c[i]+=val;         i+=lowbit(i);     } }

/*                    解決題目                */ /*            u-->v的路徑上點的值改變val      */ void change(int u,int v,int val){     int f1=top[u],f2=top[v];//top是所在鏈起始端點(對於輕鏈就是本身嘍)      int tmp=0;     while(f1 !=f2){    //直到u和v輾轉到同一個重鏈後停止。 一段一段change          if(deep[f1]<deep[f2]){    //為了方便,使f1深度大 也就是u移動次數多              swap(f1,f2);             swap(u,v);         }         add(p[f1],val);    //由於u深度大,所以先讓u往 lca靠          add(p[u]+1,-val);//這裡的add是字尾區間加值,所以這一句把多加的字尾區間減掉,就變成了重鏈上一個區間。          u=fa[f1];    //重鏈之間輾轉          f1=top[u];    //重鏈之間輾轉      }      if(deep[u]>deep[v])        //while結束之後u和v在同一重鏈上了,然後 把最後一段change掉          swap(u,v);     add(p[u],val);        //兩個點已經在同一個重鏈上了,直接區間改變即可。      add(p[v]+1,-val); }

int a[maxn]; int main(){     #ifndef ONLINE_JUDGE     freopen("r.txt","r",stdin);     #endif     int m,q;     while(scanf("%d%d%d",&n,&m,&q)!=EOF){         int u,v;         int c1,c2,k;         char op[10];         init();         for(int i=1;i<=n;i++)             scanf("%d",&a[i]);         while(m--){             scanf("%d%d",&u,&v);             addedge(u,v);             addedge(v,u);//無向圖雙向都要加          }         dfs1(1,0,0);//根節點,根節點的father,根節點的深度         getpos(1,1);//根節點,根節點所在重鏈起始節點。         memset(c,0,sizeof(c));//樹狀陣列清零         for(int i=1;i<=n;i++){             add(p[i],a[i]);             add(p[i]+1,-a[i]);         }          while(q--){             scanf("%s",op);             if(op[0]=='Q'){                 scanf("%d",&u);                 printf("%d\n",sum(p[u]));             }             else{                 scanf("%d%d%d",&c1,&c2,&k);                 if(op[0]=='D')                     k=-k;                 change(c1,c2,k);             }         }      } }