線段樹是一種二叉搜索樹 ,與區間樹 相似,它將一個區間划分成一些單元區間,每個單元區間對應線段樹中的一個葉結點 ---- 百度百科
說真的,線段樹真的是個超級超級棒的數據結構(๑•̀ㅂ•́)و✧真的相當好用,理解難度低應用廣泛還代碼好寫,初期可能代碼上有點難度,但是熟練后就會發現她的美!
進入正題,本期重點:
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 }
以上就是本講的建樹,單點查詢,單點修改和區間查詢的全部內容了,代碼在這↑,很快我應該就會更新帶懶操作的線段樹查改了,敬請期待!
如果您感覺本文對您有幫助,千萬不要吝嗇手上的贊和關注,最后,感謝您的訪問,再會!