【數據結構】線段樹初步認識


本篇文章,靈感來自於一步一步理解線段樹③,但是又與其的代碼講述實現有些不同。

目錄:
      
一、線段樹的定義

       二、線段樹的基本操作

       三、實戰演練

       四、代碼展示

一、線段樹的基本概念:

       1.定義

以下是百度百科的定義①

線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間划分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。對於線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間為[a,(a+b)/2],右兒子表示的區間為[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最后的子節點數目為N,即整個線段區間的長度。使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,時間復雜度為O(logN)。而未優化的空間復雜度為2N,因此有時需要離散化讓空間壓縮。線段樹至少支持下列操作:Insert(t,x):將包含在區間 int 的元素 x 插入到樹t中;Delete(t,x):從線段樹 t 中刪除元素 xSearch(t,x):返回一個指向樹 t 中元素 x 的指針。

              但是百科的定義往往讓人望而卻步,此處我用一種網上的較為“簡潔”的定義:

線段樹是一種針對於數據規模較大的區間查詢和操作的一種數據結構(完全二叉樹②),基於線段樹的二叉樹結構,對它的每次查詢或者改變的操作,復雜度可視為O(logn)。並且線段樹的每一個節點可以用來表示一個固定的區間。由於線段樹是完全二叉樹,所以線段樹的葉子結點所表示的區間長度是最短的,可以視為是線段樹的“底層”(按樹的定義來說應該是最高層)。

2.基本思想:遞歸、二分

       以二分的方式查詢、修改,以遞歸的方式建樹、查詢回溯、修改回溯。

3.數據結構類型:樹(完全二叉樹)

       完全二叉樹定義:

若設二叉樹的深度為h,除第 h 層外,其它各層 (1h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。完全二叉樹是由滿二叉樹而引出來的。對於深度為K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度為K的滿二叉樹中編號從1n的結點一一對應時稱之為完全二叉樹。一棵二叉樹至多只有最下面的一層上的結點的度數可以小於2,並且最下層上的結點都集中在該層最左邊的若干位置上,而在最后一層上,右邊的若干結點缺失的二叉樹,則此二叉樹成為完全二叉樹。

       4.性質:

              1’對於當前的節點root,其左孩子的下標為root*2,右孩子的下標root*2+1

              2’左孩子管轄的區間范圍為【LMID】,右孩子管轄的區間范圍為【MID+1R

              3’有着完全二叉樹的性質。

 

二、線段樹的基本操作

下面我們以一道線段樹的經典模板題引入【區間修改詢問求和問題】,來源:洛谷②;

題目大意:已知一個數列,你需要多次操作,每次操作有兩種,一個是將某個區間內的所有數加上X,一個是詢問某個區間內所有數的和;對於100%的數據:N<=100000M<=100000

操作1      格式:1 x y k 含義:將區間[x,y]內每個數加上k

操作2       格式:2 x y 含義:輸出區間[x,y]內每個數的和

輸入樣例#15 5          //n個數和m次操作

1 5 4 2 3 //已知數列

2 2 4

1 2 3 2

2 3 4

1 1 5 1

2 1 4

輸出樣例#111

8

20

解法1:每次模擬,顯然時間是會爆炸的,O(100000*100000)>1s

解法2:樹狀數組,但是由於是動態查詢修改的,導致對於樹狀數組的維護需要浪費掉非常多的時間

解法3:線段樹!

我們可以用線段樹來解決這個問題:預處理耗時O(n),查詢、更新操作O(logn),需要額外的空間O(n)

由於線段樹的父節點區間是平均分割到左右子樹,因此線段樹是完全二叉樹,對於包含n個葉子節點的完全二叉樹,它一定有n-1個非葉節點,總共2n-1個節點,因此存儲線段是需要的空間復雜度是O(n)。③

       葉子節點是原始組數a中的元素

非葉子節點代表它的所有子孫葉子節點所在區間之和


我們如圖創建好線段樹(這個是初始化版的),從而進行查詢!

       1.創建線段樹(萬事開頭難,這關過了后面相對也就簡單了)

對於線段樹我們可以選擇和普通二叉樹一樣的鏈式結構。

由於線段樹是完全二叉樹,我們也可以用結構體定義的數組來存儲,下面的討論及代碼都是運用結構體的數組來存儲線段樹,節點結構如下(注意到用數組存儲時,有效空間為2n-1,實際空間確不止這么多,比如上面的線段樹中我們有總共4個點的空間雖然是0的初始值,但是確實占據了一定的數組空間,實際上我們要的空間是滿二叉樹所需要的總空間,所以經常做線段樹的時候都會聽到人家說:線段樹開個4倍空間穩穩過就是這個意思 )。

定義當前線段樹代碼:

struct fkt{

       int left;     //記錄左區間的位置

       int right;    //記錄右區間的位置

       int tot;    //記錄當前區間的和

}tree[MAXN];      //定義一棵線段樹 

void build(int lef,int rig,int root){

//從lef到rig的距離以root為根建樹(前提是要求rig-lef+1能夠構成一個完全二叉樹)

  tree[root].left=lef; //記錄左區間的位置   tree[root].right=rig; //記錄右區間的位置   if(lef==rig){tree[root].tot=a[lef];} //到達底端,我們直接把數組中存下的數復制給線段樹   else{   //否則遞歸的去構造左右子樹     int mid=(lef+rig)>>1;     build(lef,mid,root<<1);     build(mid+1,rig,root<<1|1);   //並且根據左右子樹節點的值來更新我當前的根節點的值     tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;   } }

       2.查詢線段樹的各個區間

              構建好線段樹后,我們就得到了上圖的那種線段樹,但是要如何進行查詢呢?

              我們前面有提到,線段樹的基本思想是遞歸和二分,所以我們能不能用遞歸、二分的思想查詢呢?

              答案是可以的!

              假設我們要查找區間[3,7]的值我們怎么做呢?如圖:

我們先將所要查詢的區間與當前區間進行比較,顯然[3,7]是在[1,8]的里面的,所以我們就去根節點的兩個孩子節點里找自己所需要的求值


 

然后我們將分開后的區間繼續比較。
顯然,[3,4][1,4]內,所以直接繼續去根節點的兩個孩子節點里找自己所需要的求值

顯然,[5,7][5,8]內,所以直接繼續去根節點的兩個孩子節點里找自己所需要的求值

 


 

同理,不談。

 


 

最后找到根節點的時候因為這個時候是有值的,所以進行返回操作,將值傳回去給自己的父節點。

 


 

最后輸出9即為[3,7]查詢的正解

 


 

              那我們看懂圖解了,代碼實現就簡單多了!

              顯然,我們在做的時候是有一些多余的操作的,每一次都要訪問到底才返回。這樣線段樹的優勢又在哪里呢?

              我們發現:    對於[3,4]的這個區間,我們已經預處理過它的值了,所以可以不用繼續訪問,直接返回。

                                   對於[5,6]的區間,同樣是因為全部相等,所以直接返回。

對於[7,8]的區間,因為[7,7]並不能完全覆蓋[7,8],換個意思講就是[7,8]並不是[7,7]的子區間。這個時候我們只能繼續遞歸到正好滿足當前查詢區間為目標區間的子區間就可以返回了

實際上這樣的工作效率就會非常的高。

查詢的思想是選出一些區間,使他們相連后恰好涵蓋整個查詢區間,因此線段樹適合解決“相鄰的區間的信息可以被合並成兩個區間的並區間的信息”的問題。

線段樹查詢代碼實現:

/*

[nl,nr]表示此次所查詢的區間

root表示我當前所訪問到的節點編號

∵我前面曾經存儲過樹的左右孩子

所以我當前的區間就可以直接用[tree[root].left,tree[root].right]表示

*/

int query(int nl,int nr,int root){

  if(tree[root].left>nr||tree[root].right<nl)       return 0;  //如果二者沒有交集則返回一個極小的值

  if(tree[root].left>=nl&&tree[root].right<=nr)     return tree[root].tot;

  //如果這個當前區間本身就是屬於我要求的那個區間,那么我就不繼續遞歸了,直接回溯

  //實際上圖解中是仍舊遞歸到最后的,所以不免會多一些操作,耗費時間

  //像這樣就比較簡便。

  return query(nl,nr,root<<1)+query(nl,nr,root<<1|1);

}

       3.更新線段樹的節點

              顯然,如果更新單節點的話我們完全可以直接用樹狀數組④解決,

              而題目要求的是區間性的修改和查詢訪問,所以樹狀數組的二進制應用是行不通的。

              (我自認為)線段樹最高明的地方就在於懶惰標記(lazy_tag)

           因為有了懶惰標記(lazy_tag)才使得線段樹變得高效,方便。

       一般來說,我們對於區間內的所有元素的修改,顯然如果逐個遞歸回溯修改,把每一次的區間更新完,是極其耗費時間的(顯然不可能是O(logn)),線段樹引入懶惰標記就可以保證“只拿這次對我有用的,這次對我有用的一次性拿光,暫時沒用的先不管他,可以先存着”。

       在本文中,我們以add表示lazy_tag,同時我們需要一個維護線段樹的函數update、一個向下傳遞懶惰標記(lazy_tag)的函數pushdown、一個向上傳遞值的函數pushup來更新。

       懶惰標記的含義即對於我當前的[tree[root].left,tree[root].right],暫時有lazy_tag的數還沒加上去,我們不急着加,等到要用或者訪問查詢的時候才用它,這樣可以節省了很多不必要的操作,這就是線段樹能做到Onlogn)的一個重要原因!。

       代碼實現如下:

       

//從建樹開始重新說起

void build(int lef,int rig,int root){

  tree[root].left=lef;

  tree[root].right=rig;

  tree[root].add=0;    //懶惰標記lazy_tag

  if(lef==rig){tree[root].tot=a[lef];}

  else{

    int mid=(lef+rig)>>1;

    build(lef,mid,root<<1);

    build(mid+1,rig,root<<1|1);

    tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;

    //要記得把線段樹先初始化一下

  }

}     

 

 //向下傳遞懶惰標記(lazy_tag)(說明自己現在需要當前的根加上懶惰標記了,而我不想浪費時間地去遞歸把剩下的給一次性更新完,所以我們考慮直接傳遞懶惰標記,不更新左右孩子,除非我需要。) 

void pushdown(int root){

  if(tree[root].add){

    tree[root<<1].add+=tree[root].add;

    tree[root<<1|1].add+=tree[root].add;

    //當前區間加上了懶惰標記的值實際上加的值是lazy_tag*區間長度

    //畫一下圖就可以知道了

    tree[root<<1].tot+=tree[root].add*(tree[root<<1].right-tree[root<<1].left+1);

    tree[root<<1|1].tot+=tree[root].add*(tree[root<<1|1].right-tree[root<<1|1].left+1);

    tree[root].add=0;

  }
}

 

       

//向上推傳遞值,說白了就是刷新一下自身的值(保證下面的值改變的時候自己也會改變)

void pushup(int root){

  tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;

}

      

  

 /*區間更新,意思:

       當前的更新區間為[tree[root].left,tree[root].right]

       目標的更新區間為[nl,nr]

       顯然如果沒交集就不管他。

       如果二者區間有交集我們再詳細處理

       如果當前查找的區間是我們目標區間的其中一個子區間,自然直接返回即可。

       (當初我這里也是搞得亂亂的,推薦大家可以做圖一下,畫個圖一下就看出來了。)

*/

void update(int nl,int nr,int root,int c){

  if(tree[root].left>nr||tree[root].right<nl)       return;

  if(tree[root].left>=nl&&tree[root].right<=nr){

    tree[root].add+=c;

    tree[root].tot+=c*(tree[root].right-tree[root].left+1);

    return;

  }

  pushdown(root);

  int mid=(tree[root].left+tree[root].right)>>1;

  if(nl<=mid)           update(nl,nr,root<<1,c);

  if(mid+1<=nr)       update(nl,nr,root<<1|1,c);
  
  pushup(root); }

  

//查詢工作,在[nl,nr]的區間內查找,當前查找[tree[root].left,tree[root].right]區間      

int query(int nl,int nr,int root){

  if(tree[root].left>nr||tree[root].right<nl)       return 0;  //返回一個極小的值

  if(tree[root].left>=nl&&tree[root].right<=nr)             return tree[root].tot;

  pushdown(root);     //向下傳遞才能計算求和。

  return query(nl,nr,root<<1)+query(nl,nr,root<<1|1);   //返回所計算的求和
}

       實際上這題就完成了!而且我們也學會了線段樹的一些基本操作!

三、實戰演練

1.線段樹模板1【實現區間加法並且動態查詢】

       鏈接: https://www.luogu.org/problem/show?pid=3372

2.線段樹模板2【實現區間加法與乘法並且動態查詢】

       鏈接: https://www.luogu.org/problem/show?pid=3373

3. [JSOI2008]最大數

       鏈接: https://www.luogu.org/problem/show?pid=1198

本人代碼在底部,可借鑒,請勿抄襲!

 

 

 

 

 

 

四、代碼展示

1.CODE

 

 1 #include<bits/stdc++.h>
 2 #define LL long long
 3 using namespace std;
 4 LL op,n,m,x,y,k,a[300005],low_bit,tempt;
 5 struct fkt{
 6     LL left;
 7     LL right;
 8     LL tot;
 9     LL add;
10 }tree[300005];
11 namespace qaq{
12     LL change(LL test){LL ans=0;while(test){test>>=1;ans++;}test=1;while(ans--)test*=2;return test;} 
13     void build(LL lef,LL rig,LL root){        
14 tree[root].left=lef;
15         tree[root].right=rig;
16         tree[root].add=0;
17         if(lef==rig){tree[root].tot=a[lef];}
18         else{
19             LL mid=(lef+rig)>>1;
20             build(lef,mid,root<<1);
21             build(mid+1,rig,root<<1|1);
22             tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;
23         }
24     }
25     void pushdown(LL root){
26         if(tree[root].add){
27             tree[root<<1].add+=tree[root].add;
28             tree[root<<1|1].add+=tree[root].add;
29             tree[root<<1].tot+=tree[root].add*(tree[root<<1].right-tree[root<<1].left+1);
30             tree[root<<1|1].tot+=tree[root].add*(tree[root<<1|1].right-tree[root<<1|1].left+1);
31             tree[root].add=0;
32         }
33     } 
34     void pushup(LL root){
35         tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;
36     }     
37     void update(LL nl,LL nr,LL root,LL c){
38         if(tree[root].left>nr||tree[root].right<nl)    return;
39         if(tree[root].left>=nl&&tree[root].right<=nr){
40             tree[root].add+=c;
41             tree[root].tot+=c*(tree[root].right-tree[root].left+1);
42             return;
43         }
44         pushdown(root);
45         LL mid=(tree[root].left+tree[root].right)>>1;
46         if(nl<=mid)        update(nl,nr,root<<1,c);
47         if(mid+1<=nr)    update(nl,nr,root<<1|1,c);
48         pushup(root);
49     } 
50     LL query(LL nl,LL nr,LL root){
51         if(tree[root].left>nr||tree[root].right<nl)    return 0; 
52         if(tree[root].left>=nl&&tree[root].right<=nr)    return tree[root].tot;
53         pushdown(root); 
54         return query(nl,nr,root<<1)+query(nl,nr,root<<1|1);
55     }
56     
57     int main(){
58         scanf("%lld%lld",&n,&m);
59         for(LL i=1;i<=n;i++)    scanf("%lld",&a[i]);
60         tempt=change(n);
61         build(1,tempt,1);
62         while(m--){
63             scanf("%lld",&op);
64             if(op==1){
65                 scanf("%lld%lld%lld",&x,&y,&k);
66                 update(x,y,1,k);
67             }
68             else{
69                 scanf("%lld%lld",&x,&y);
70                 printf("%lld\n",query(x,y,1));
71             }
72         }
73         return 0;
74     }
75 }
76 int main(){
77     qaq::main();
78     return 0;
79 }

2.CODE

  1 #include<bits/stdc++.h>
  2 #define LL long long
  3 using namespace std;
  4 LL op,n,m,x,y,k,a[300005],low_bit,tempt,p;
  5 struct fkt{
  6     LL left;
  7     LL right;
  8     LL tot;
  9     LL multi;
 10     LL add;
 11 }tree[300005];
 12 namespace qaq{
 13     LL change(LL test){LL ans=0;while(test){test>>=1;ans++;}test=1;while(ans--)test*=2;return test;} 
 14     void build(LL lef,LL rig,LL root){ 
 15         tree[root].left=lef;
 16         tree[root].right=rig;
 17         if(lef==rig){tree[root].tot=a[lef]%p;}
 18         else{
 19             LL mid=(lef+rig)>>1;
 20             build(lef,mid,root<<1);
 21             build(mid+1,rig,root<<1|1);
 22             tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;
 23             tree[root].tot%=p;
 24         }
 25     }
 26     void pushdown(LL root){
 27         if(tree[root].multi!=1){
 28             tree[root<<1].add*=tree[root].multi;
 29             tree[root<<1].add%=p;
 30             tree[root<<1|1].add*=tree[root].multi;
 31             tree[root<<1|1].add%=p;
 32             tree[root<<1].multi*=tree[root].multi;
 33             tree[root<<1].multi%=p;
 34             tree[root<<1|1].multi*=tree[root].multi;
 35             tree[root<<1|1].multi%=p;
 36             tree[root<<1].tot*=tree[root].multi;
 37             tree[root<<1].tot%=p;
 38             tree[root<<1|1].tot*=tree[root].multi;
 39             tree[root<<1|1].tot%=p;
 40             tree[root].multi=1;
 41         }
 42         if(tree[root].add){
 43             tree[root<<1].add+=tree[root].add;
 44             tree[root<<1].add%=p;
 45             tree[root<<1|1].add+=tree[root].add;
 46             tree[root<<1|1].add%=p;
 47             tree[root<<1].tot+=tree[root].add*(tree[root<<1].right-tree[root<<1].left+1);
 48             tree[root<<1].tot%=p;
 49             tree[root<<1|1].tot+=tree[root].add*(tree[root<<1|1].right-tree[root<<1|1].left+1);
 50             tree[root<<1|1].tot%=p;
 51             tree[root].add=0;
 52         }
 53     } 
 54     void pushup(LL root){
 55         tree[root].tot=tree[root<<1].tot+tree[root<<1|1].tot;
 56     }     
 57     void update(LL nl,LL nr,LL root,LL c,bool cc){
 58         if(tree[root].left>nr||tree[root].right<nl)    return;
 59         if(tree[root].left>=nl&&tree[root].right<=nr){
 60             if(cc==0){
 61                 tree[root].add+=c;
 62                 tree[root].add%=p;
 63                 tree[root].tot+=c*(tree[root].right-tree[root].left+1);
 64                 tree[root].tot%=p;
 65             }
 66             else{
 67                 tree[root].multi*=c;
 68                 tree[root].multi%=p;
 69                 tree[root].add*=c;
 70                 tree[root].add%=p;
 71                 tree[root].tot*=c;
 72                 tree[root].tot%=p;
 73             }
 74             return;
 75         }
 76         LL mid=(tree[root].left+tree[root].right)>>1;
 77         pushdown(root);
 78         if(nl<=mid)        update(nl,nr,root<<1,c,cc);
 79         if(mid+1<=nr)    update(nl,nr,root<<1|1,c,cc);
 80         pushup(root);
 81     } 
 82     LL query(LL nl,LL nr,LL root){
 83         if(tree[root].left>nr||tree[root].right<nl)    return 0; 
 84         if(tree[root].left>=nl&&tree[root].right<=nr)    return tree[root].tot;
 85         pushdown(root); 
 86         LL x=query(nl,nr,root<<1)%p;
 87         LL y=query(nl,nr,root<<1|1)%p;
 88         pushup(root);
 89         return (x+y)%p;
 90     }
 91     int main(){
 92         scanf("%lld%lld%lld",&n,&m,&p);
 93         for(LL i=1;i<=300000;i++){
 94             tree[i].multi=1;
 95             tree[i].add=0;
 96         }
 97         for(LL i=1;i<=n;i++){
 98             scanf("%lld",&a[i]);
 99             a[i]%=p;
100         }    
101         tempt=change(n);
102         build(1,tempt,1);
103         while(m--){
104             scanf("%lld",&op);
105             if(op==1){//乘法 
106                 scanf("%lld%lld%lld",&x,&y,&k);
107                 update(x,y,1,k,1);
108             }
109             else if(op==2){//加法 
110                 scanf("%lld%lld%lld",&x,&y,&k);
111                 update(x,y,1,k,0);
112             }
113             else{
114                 scanf("%lld%lld",&x,&y);
115                 printf("%lld\n",query(x,y,1)%p);
116             }
117         }
118         return 0;
119     }
120 }
121 int main(){
122     qaq::main();
123     return 0;
124 }

 

 

 

3.CODE

 1 #include<bits/stdc++.h>
 2 #define LL long long
 3 using namespace std;
 4 LL m,p,t=0,len=0,n;
 5 char op;
 6 struct fkt{
 7     LL left;
 8     LL right;
 9     LL numb;
10 }tree[2400000];
11 namespace qaq{
12     LL query(LL l,LL r,LL root,LL ql,LL qr){
13         LL ans=-999999999;
14         if(ql<=l&&qr>=r)    return tree[root].numb;
15         LL mid=(l+r)>>1;
16         if(ql<=mid)        ans=max(ans,query(l,mid,root<<1,ql,qr));
17         if(mid+1<=qr)    ans=max(ans,query(mid+1,r,root<<1|1,ql,qr));
18         return ans;
19     }
20     
21     void update(LL l,LL r,LL root,LL place,LL x){
22         if(l==r){tree[root].numb=x;return;}
23         LL mid=(l+r)>>1;
24         if(place<=mid)    update(l,mid,root<<1,place,x);
25         else    update(mid+1,r,root<<1|1,place,x);    
26         tree[root].numb=max(tree[root<<1].numb,tree[root<<1|1].numb);        
27     }
28     
29     int main(){
30         cin>>m>>p;
31         for(LL i=1;i<=m;i++)tree[i].numb=-999999999;
32         while(m--){
33             cin>>op>>n;;
34             if(op=='A'){
35                 len++;
36                 n=(n+t)%p;
37                 update(1,262144,1,len,n);
38             }
39             else{
40                 t=query(1,262144,1,len-n+1,len);
41                 printf("%lld\n",t);
42             }
43         }
44         return 0;
45     }
46 }
47 int main(){    
48 qaq::main();
49     return 0;
50 }

 

 

 

 歡迎糾錯。等待補充......

 

 

 

參考文獻、資料或解釋:

①:【線段樹】

https://baike.baidu.com/item/%E7%BA%BF%E6%AE%B5%E6%A0%91/10983506?fr=aladdin#ref_[2]_670683

②:【線段樹模板:區間修改詢問求和問題】 https://www.luogu.org/problem/show?pid=3372

③:【一步一步理解線段樹】http://www.cnblogs.com/TenosDoIt/p/3453089.html

④:【樹狀數組】可參考該博客 http://www.cnblogs.com/hsd-/p/6139376.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM