線段樹初步:建樹、單點查改、區間查詢


  線段樹是一種二叉搜索樹 ,與區間樹 相似,它將一個區間划分成一些單元區間,每個單元區間對應線段樹中的一個葉結點 ---- 百度百科

  說真的,線段樹真的是個超級超級棒的數據結構(๑•̀ㅂ•́)و✧真的相當好用,理解難度低應用廣泛還代碼好寫,初期可能代碼上有點難度,但是熟練后就會發現她的美!

  進入正題,本期重點:

    1、線段樹建樹

    2、單點查詢

    3、單點修改

    4、區間查詢

一、線段樹建樹

  先來說說什么是線段樹,線段樹就是一個二叉樹,它的每一個子節點的范圍都是半個父節點所占的范圍,而且左右兒子的范圍之和就是父節點所占的范圍

  舉個例子吧

  1,2,3,4,5,6,7,8,9,10是給定的一串數字,以它為根節點建線段樹

  首先以給定數字作為根節點,如果把這是個數存在數組a里,在這里就是a[n]=n(我存數組喜歡從1開始),那么根節點的范圍就是1<=n<=10

 

 

  然后我們把這十個數分成兩份----1,2,3,4,5和6,7,8,9,10,分別作為根節點的左兒子和右兒子,即根節點包含1~10,那么它的左兒子就是1~5,右兒子是6~10,即可得到如下圖

  以此類推我們繼續往下分

 

 

  最后就可以完成整個二叉樹的建樹了

  

 

  用左邊界和右邊界來表示范圍,如圖

  

  這樣看就可以更直觀的發現一個節點的區間為m,n;那么他的左兒子節點區間就是[m,(m+n)/2];右兒子節點區間就是[(m+n)/2+1,n];

  來個例子看看吧?

  我輸入一行數,以他們的和建線段樹

  這是一個線段樹建樹的模板

  來看看

  先來看看節點怎么存

1 const int mm=100005;
2 struct tree{ 3 int l,r;  //左邊界和右邊界(left,right) 4 int sum;  //存的是l,r這個區間的和 5 }a[mm*4];

  每個節點三個元素,左邊界(l),右邊界(r),和在這個區域內的和(sum),節點a[n]的左右兒子就為a[n*2]和a[n*2+1]  

  接下來來看建樹

  我們先將一堆數字錄入到in[mm]中

  我們建樹也是要一個節點一個節點建的,從根節點開始,

1 void build(int l,int r,int num) //l是左區間,r是右區間,num是節點編號

  如果有不熟悉遞歸框架的朋友可以看這里:0基礎算法基礎學算法 第六彈 遞歸 - 球君 - 博客園 (cnblogs.com)

  因為num的左右節點就是l和r了,所以。。。

void build(int l,int r,int num)
{    
    a[num].l=l; a[num].r=r; }

  直接把l和r裝上

  當l和r相等之時,就不能在往下分了,而滿足這個條件的num的sum就是in[l];

  於是有了如下代碼

void build(int l,int r,int num)
{    
    a[num].l=l; a[num].r=r; if(l==r) { a[num].sum=in[l]; return ; } }

  那如果這一節點還可以再往下分,那就再分兩半,一個區間為[l,(l+r)/2]另一個區間為[(l+r)/2+1,r],往下遞歸

 1 void build(int l,int r,int num)
 2 { 3 a[num].l=l; 4 a[num].r=r; 5 if(l==r) 6  { 7 a[num].sum=in[l]; 8 return ; 9  } 10 int mid=(l+r)/2; 11 build(l,mid,num*2); 12 build(mid+1,r,num*2+1); 13 }

  通過這個圖,我們可以看出,一個父親節點的sum就是兩個兒子節點的sum之和

 

 

  即如下代碼

 1 void build(int l,int r,int num)
 2 { 3 a[num].l=l; 4 a[num].r=r; 5 if(l==r) 6  { 7 a[num].sum=in[l]; 8 return ; 9  } 10 int mid=(l+r)/2; 11 build(l,mid,num*2); 12 build(mid+1,r,num*2+1); 13 a[num].sum=a[num*2].sum+a[num*2+1].sum; 14 }

  以上就是建樹部分的代碼了,這時候我們再加上主函數進行錄入,和存線段樹用的結構體

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int mm=100005; 4 int in[mm]; 5 struct tree{ 6 int l,r; 7 int sum; 8 }a[mm*4]; 9 void build(int l,int r,int num) 10 { 11 a[num].l=l; 12 a[num].r=r; 13 if(l==r) 14  { 15 a[num].sum=in[l]; 16 return ; 17  } 18 int mid=(l+r)/2; 19 build(l,mid,num*2); 20 build(mid+1,r,num*2+1); 21 a[num].sum=a[num*2].sum+a[num*2+1].sum; 22 } 23 int main(){ 24 //freopen("in.txt","r",stdin); 25 //freopen("out.txt","w",stdout); 26 for(int i=1;i<=10;i++) 27  { 28 cin>>in[i]; 29  } 30 build(1,m,1); 31 return 0; 32 }

  就是完整的線段樹建樹部分的代碼了

  再加一行代碼調試一下

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int mm=100005; 4 int in[mm]; 5 struct tree{ 6 int l,r; 7 int sum; 8 }a[mm*4]; 9 void build(int l,int r,int num) 10 { 11 a[num].l=l; 12 a[num].r=r; 13 if(l==r) 14  { 15 a[num].sum=in[l]; 16 return ; 17  } 18 int mid=(l+r)/2; 19 build(l,mid,num*2); 20 build(mid+1,r,num*2+1); 21 a[num].sum=a[num*2].sum+a[num*2+1].sum; 22 } 23 int main(){ 24 //freopen("in.txt","r",stdin); 25 //freopen("out.txt","w",stdout); 26 for(int i=1;i<=10;i++) 27  { 28 cin>>in[i]; 29  } 30 build(1,10,1); 31 cout<<a[1].sum; 32 return 0; 33 }

  結果如下

  

 

  正常輸出

  來看下面一道題目,先只用完成建樹的部分

  P1816 忠誠 - 洛谷 | 計算機科學教育新生態 (luogu.com.cn)

  這里的建樹和標准的建樹幾乎一模一樣,只是每次建樹時的sum改成存這個節點區間內的最小值minn就好

  前面這段除了改變了sum變成minn以外沒有區別

 1 void build(int l,int r,int num)
 2 { 3 a[num].l=l; 4 a[num].r=r; 5 if(l==r) 6  { 7 a[num].minn=in[l]; 8 return ; 9  } 10 int mid=(l+r)/2; 11 build(l,mid,num*2); 12 build(mid+1,r,num*2+1); 13 }

  只是最后父親節點不再是將兩個兒子節點的sum累加了,而是選取兩個minn中的最小值

  即完整建樹

void build(int l,int r,int num)
{    
    a[num].l=l; a[num].r=r; if(l==r) { a[num].minn=in[l]; return ; } int mid=(l+r)/2; build(l,mid,num*2); build(mid+1,r,num*2+1); a[num].minn=min(a[num*2].minn,a[num*2+1].minn) ; }

  暫時就是這些了,接下來的部分等一會再細講

二、線段樹單點查詢

  線段樹是遞歸建樹的,單點查詢也是由遞歸完成的

  大致思想就是從根節點開始,如果兒子節點的區間包含要查詢的元素,就往下遞歸

  在剛剛建樹的基礎上輸入一個k,查詢輸入的第k個數是啥

  來分析一下,在線段樹中查詢一個元素,就需要從根節點開始,哪邊兒子包含這個元素就往他那里走,當這個節點就是原來要查詢的節點時,輸出值,返回

  先來看看當這個節點完全等於要查詢的元素下標時候

1 void search(int l,int r,int num,int res)//目前區間,目前編號,目標位置 
2 {
3     if(r==res&&l==res) 4  { 5 cout<<a[num].sum; 6 return ; 7  } 8 }

  直接輸出,然后立刻返回

  為了方便后邊調用,我們設置兩個變量

int mid=(l+r)/2;
tree an1=a[num*2];

  然后就是十分重要的判斷----判斷左右兒子哪一個的區間包含查詢的目的位置

  判斷條件很容易寫,即兒子的左區間小於等於這個下標,右區間大於等於這個下標,如果不滿足則就往另一兒子處走

  即

1 if(an1.l<=res&&an1.r>=res)
2 { 3 search(l,mid,num*2,res); 4 } 5 else 6 { 7 search(mid+1,r,num*2+1,res); 8 }

  這里可以看到我們只判斷一邊的兒子是否包含下標是因為,開始的時候,根節點包含所有錄入的數,可以保證能包含此下標,然后后面的每一步都是在包含這個下標的區域去遞歸,所以就形成了一個非黑即白的場面

  看看完整代碼

#include<bits/stdc++.h>
using namespace std;
const int mm=100005; int in[mm]; int n,m; struct tree{ int l,r; int sum; }a[mm*4]; void build(int l,int r,int num) { a[num].l=l; a[num].r=r; if(l==r) { a[num].sum=in[l]; return ; } int mid=(l+r)/2; build(l,mid,num*2); build(mid+1,r,num*2+1); a[num].sum=a[num*2].sum+a[num*2+1].sum; } void search(int l,int r,int num,int res)//目前區間,目前編號,目標位置 { if(r==res&&l==res) { cout<<a[num].sum; return ; } int mid=(l+r)/2; tree an1=a[num*2]; if(an1.l<=res&&an1.r>=res) { search(l,mid,num*2,res); } else { search(mid+1,r,num*2+1,res); } return ; } int main(){ //freopen("in.txt","r",stdin); //freopen("out.txt","w",stdout); cin>>n; for(int i=1;i<=n;i++) { cin>>in[i]; } build(1,n,1); cout<<a[1].sum<<endl; cin>>m; search(1,n,1,m); return 0; }

  n是你要輸入的元素個數,in是輸入的元素,m是要查詢的下標,輸出的第一行是你輸入的元素之和,第二行是你所查詢的下標的元素

  很多人可能看到這里會像,這不是多此一舉嗎?直接cout<<in[n];不好嗎?那么,接下來的單點查詢就要用到這個思路了

三,單點修改

  設想一下,對線段樹底部的一個元素進行修改,是不是他的爸爸,爺爺,祖先都要發生更改?

  因此,所有區間內包含了這個元素的節點都要進行改變。

  如果從根節點開始遞歸,那就按照“單點查詢”路線將走過的所有節點都改一遍。

  比如我們此時在輸入一個數s,表示在輸入數組in中的下標,把他改成d,那么沿途所有節點的sum都得增加(d-in[s])

 1 void reload(int l,int r,int num,int res,int exc)//新增的一個元素是要改成的數值 
 2 { 
 3     if(r==res&&l==res) 4  { 5 return ; 6  } 7 int mid=(l+r)/2; 8 tree an1=a[num*2]; 9 if(an1.l<=res&&an1.r>=res) 10  { 11 reload(l,mid,num*2,res,exc); 12  } 13 else 14  { 15 reload(mid+1,r,num*2+1,res,exc); 16  } 17 return ; 18 }

  首先大體上與查詢一樣,新增了一個變量exc,然后去掉了查詢時的輸出

  接下來再加上一句靈性的

a[num].sum+=exc-in[res];

  看看完整效果

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int mm=100005; 4 int in[mm]; 5 int n,m,s,d; 6 struct tree{ 7 int l,r; 8 int sum; 9 }a[mm*4]; 10 void build(int l,int r,int num) 11 { 12 a[num].l=l; 13 a[num].r=r; 14 if(l==r) 15  { 16 a[num].sum=in[l]; 17 return ; 18  } 19 int mid=(l+r)/2; 20 build(l,mid,num*2); 21 build(mid+1,r,num*2+1); 22 a[num].sum=a[num*2].sum+a[num*2+1].sum; 23 } 24 void search(int l,int r,int num,int res)//目前區間,目前編號,目標位置 25 { 26 if(r==res&&l==res) 27  { 28 cout<<a[num].sum<<endl; 29 return ; 30  } 31 int mid=(l+r)/2; 32 tree an1=a[num*2]; 33 if(an1.l<=res&&an1.r>=res) 34  { 35 search(l,mid,num*2,res); 36  } 37 else 38  { 39 search(mid+1,r,num*2+1,res); 40  } 41 return ; 42 } 43 void reload(int l,int r,int num,int res,int exc)//新增的一個元素是要改成的數值 44 { 45 a[num].sum+=exc-in[res]; 46 if(r==res&&l==res) 47  { 48 return ; 49  } 50 int mid=(l+r)/2; 51 tree an1=a[num*2]; 52 if(an1.l<=res&&an1.r>=res) 53  { 54 reload(l,mid,num*2,res,exc); 55  } 56 else 57  { 58 reload(mid+1,r,num*2+1,res,exc); 59  } 60 return ; 61 } 62 int main(){ 63 freopen("in.txt","r",stdin); 64 freopen("out.txt","w",stdout); 65 cin>>n; 66 for(int i=1;i<=n;i++) 67  { 68 cin>>in[i]; 69  } 70 build(1,n,1); 71 cout<<a[1].sum<<endl; 72 cin>>m; 73 search(1,n,1,m); 74 cin>>s>>d; 75 reload(1,n,1,s,d); 76 cout<<a[1].sum<<endl; 77 return 0; 78 }

  以上代碼就是關於建樹,單點查改的全部內容了😵

四,區間查詢

  區間查詢和單點查詢是有點相似的,只不過這里並不需要一查到底,倘若某節點的區間在查詢區間以內,就將該節點的區間拿出來累加,如果只有一部分在,那就繼續向下走

  區間查詢要求:輸入兩個數,求出in中下標為兩個數間的和;

  我們來重新搬出這幅圖

  

 

  比如,我們要查詢區間為[3,7]內的數字之和

  [1,10]的范圍大了,完全包含了[3,7],而且要查詢的區間的左邊界比[1,10]區間的中點小,因此向左兒子遞歸,而要查詢的區間的右邊界比[1,10]區間的中點大,所以還可以向右兒子遞歸

  

 

  再看到左兒子那里,查詢區間的左邊界比它的中點大因此不用往左兒子遞歸了,往右兒子遞歸;至於右兒子那邊,左邊界比mid小,右邊界也比mid小所以往左兒子繼續遞歸

  如圖

  

 

 

   因為[3,5],[6,7]節點都完全被查詢區間蓋滿了,所以就不必再往下遞歸,最后的結果便是這兩的sum之和

  來看看代碼怎么寫

  這次查找我們帶入5個變量,分別是該節點編號,查詢區間,目前節點區間

void found(int num,int l,int r,int ll,int rr)

  當該節點區間被查詢區間完全覆蓋時,就說明這是所需要查詢的一部分,用ans累加

  當該節點左右的區間不足觸碰到查詢區間邊界時,說明走過頭了,得回去

  回溯部分

 1 void found(int num,int l,int r,int ll,int rr)
 2 { 3 if(a[num].l>=l&&a[num].r<=r) 4  { 5 ans+=a[num].sum; 6 return ; 7  } 8 if(a[num].r<l||a[num].l>r) 9  { 10 return ; 11  } 12 }

  就像我之前一樣,為了方便我們再定義一個mid

int mid=(a[num].l+a[num].r)/2;

  按照我們前文討論的思路,當l<=mid時就往左兒子遞歸,當r>mid時就往右兒子遞歸

  看看整體效果

 1 void found(int num,int l,int r,int ll,int rr)
 2 { 3 if(a[num].l>=l&&a[num].r<=r) 4  { 5 ans+=a[num].sum; 6 return ; 7  } 8 if(a[num].r<l||a[num].l>r) 9  { 10 return ; 11  } 12 int mid=(a[num].l+a[num].r)/2; 13 if(l<=mid) 14  { 15 found(2*num,l,r,ll,mid); 16  } 17 if(r>mid) 18  { 19 found(2*num+1,l,r,mid+1,rr); 20  } 21 return ; 22 } 

  這樣就可以實現區間查詢了,最后再帶上主函數以及前面講的內容

  1 #include<bits/stdc++.h>
  2 using namespace std;
  3 const int mm=100005; 4 int in[mm]; 5 int n,m,s,d,a1,b1; 6 int ans; 7 struct tree{ 8 int l,r; 9 int sum; 10 }a[mm*4]; 11 void build(int l,int r,int num) 12 { 13 a[num].l=l; 14 a[num].r=r; 15 if(l==r) 16  { 17 a[num].sum=in[l]; 18 return ; 19  } 20 int mid=(l+r)/2; 21 build(l,mid,num*2); 22 build(mid+1,r,num*2+1); 23 a[num].sum=a[num*2].sum+a[num*2+1].sum; 24 } 25 void search(int l,int r,int num,int res)//目前區間,目前編號,目標位置 26 { 27 if(r==res&&l==res) 28  { 29 cout<<a[num].sum<<endl; 30 return ; 31  } 32 int mid=(l+r)/2; 33 tree an1=a[num*2]; 34 if(an1.l<=res&&an1.r>=res) 35  { 36 search(l,mid,num*2,res); 37  } 38 else 39  { 40 search(mid+1,r,num*2+1,res); 41  } 42 return ; 43 } 44 void reload(int l,int r,int num,int res,int exc)//新增的一個元素是要改成的數值 45 { 46 a[num].sum+=exc-in[res]; 47 if(r==res&&l==res) 48  { 49 return ; 50  } 51 int mid=(l+r)/2; 52 tree an1=a[num*2]; 53 if(an1.l<=res&&an1.r>=res) 54  { 55 reload(l,mid,num*2,res,exc); 56  } 57 else 58  { 59 reload(mid+1,r,num*2+1,res,exc); 60  } 61 return ; 62 } 63 void found(int num,int l,int r,int ll,int rr) 64 { 65 if(a[num].l>=l&&a[num].r<=r) 66  { 67 ans+=a[num].sum; 68 return ; 69  } 70 if(a[num].r<l||a[num].l>r) 71  { 72 return ; 73  } 74 int mid=(a[num].l+a[num].r)/2; 75 if(l<=mid) 76  { 77 found(2*num,l,r,ll,mid); 78  } 79 if(r>mid) 80  { 81 found(2*num+1,l,r,mid+1,rr); 82  } 83 return ; 84 } 85 int main(){ 86 //freopen("in.txt","r",stdin); 87 //freopen("out.txt","w",stdout); 88 cin>>n; 89 for(int i=1;i<=n;i++) 90  { 91 cin>>in[i]; 92  } 93 build(1,n,1); 94 cout<<a[1].sum<<endl; 95 cin>>m; 96 search(1,n,1,m); 97 cin>>s>>d; 98 reload(1,n,1,s,d); 99 cout<<a[1].sum<<endl; 100 /*reload(1,n,1,s,d); 101 cout<<a[1].sum<<endl;*///可以用作把改過的改回來 102 cin>>a1>>b1; 103 found(1,a1,b1,1,n); 104 cout<<ans<<endl; 105 return 0; 106 }

   以上就是本講的建樹,單點查詢,單點修改和區間查詢的全部內容了,代碼在這↑,很快我應該就會更新帶懶操作的線段樹查改了,敬請期待!

  如果您感覺本文對您有幫助,千萬不要吝嗇手上的贊和關注,最后,感謝您的訪問,再會! 


免責聲明!

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



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