閒了好久的wym復活拉~更個辣雞的線段樹
如果你不知道什麼是線段樹這個就不用看
由於我們平時可能會遇到一些噁心的題叫做給你 \(10^5\) 個數的陣列,然後 \(10^5\) 次修改或查詢,這樣顯然暴力是可以做的而且ST表我們無視修改。這個時候可以用線段樹、樹狀陣列或者其他大佬們的神仙演算法,由於我不知道何謂lowbit所以用的線段樹。
線段樹的空間是樹狀陣列的4倍,或者某些例外,8倍,即 \(4N\) 或 \(8N\)。
線段樹的好處在於它的功能比樹狀陣列多,最重要的在於,樹狀陣列維護的是字首和,所以不能維護最大最小值。而線段樹維護的是實實在在的區間和,所以樹狀陣列能做到的,線段樹都能做到。但是線段樹的時空複雜度都比樹狀陣列高,而且程式碼更復雜。有些 \(N=5 \times 10^6\) 的題就不能用線段樹做了。
好了瞎扯完了,現在來看線段樹的思想是什麼。
看圖(圖中線段樹維護的是區間和):
由圖可以得出,首先,線段樹是一個二叉堆,或者叫作完全二叉樹,而且樹中每個點對應陣列中的一個區間。這樣方便的地方在於,我查詢一個區間的時候,如果恰好遇到一個查詢區間包含的區間時,就能直接取值了。
題目給出一個陣列,首先我們要建出這個線段樹。
const int N=1e5+10;
int n,a[N],t[N*4]; //a陣列是輸入的,t陣列用來儲存線段樹,根為1,一個節點 i 的左子節點為 2i,右子節點為 2i+1
void build(int now,int tl,int tr){ //now表示線段樹的節點,tl和tr表示原陣列的區間
if(tl==tr){ //遇到一個原陣列的數(區間長度為1),即線段樹中的葉子節點了
t[now]=a[tl]; //或a[tr]
return ;
}
int mid=(tl+tr)/2; //區分左子樹和右子樹
build(now*2,tl,mid); //遞迴建左子樹
build(now*2+1,mid+1,tr); //遞迴建右子樹
t[now]=t[now*2]+t[now*2+1]; //區間和,或者可以定義其他操作,注意這裡一定不要忘寫
}
可以輸出測試一下,以上圖為例:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],t[N*4];
int flag[N];
void build(int now,int tl,int tr){
if(tl==tr){
flag[now]=1;
t[now]=a[tl];
return ;
}
int mid=(tl+tr)/2;
build(now*2,tl,mid);
build(now*2+1,mid+1,tr);
t[now]=t[now*2]+t[now*2+1];
flag[now]=1;
}
int main(){
n=5;
a[1]=1,a[2]=2,a[3]=3,a[4]=4,a[5]=5;
build(1,1,n);
for(int i=1;i<=n*4;i++){
if(flag[i]){
cout<<t[i]<<" ";
}
}
return 0;
}
輸出結果:
15 6 9 3 3 4 5 1 2
相當於原圖片中樹的層次遍歷。你也可以寫一個dfs求前序/中序/後序遍歷。或者也可以用dfs/bfs求樹中每個點代表的區間或數。甚至,你可以用bfs寫build。
很明顯,build函式時間複雜度 \(O(n)\)。
現在樹建好了,我們先講查詢,再講修改。
單點查詢:跟build類似,但略有不同。自己思考一下為什麼tl==tr
不需要再判斷tl==pos
或tr==pos
。
int query(int now,int tl,int tr,int pos){ //pos代表查詢的陣列下標
if(tl>pos||tr<pos){ //不在範圍內
return 0;
}
if(tl==tr){ //找到了
return t[now];
}
int mid=(tl+tr)/2;
return query(now*2,tl,mid,pos)+query(now*2+1,mid+1,tr,pos);
}
區間查詢:這個時候,t陣列中非葉子的節點,即代表原陣列中區間的部分就派上用場了。查詢時,如果發現了一個查詢範圍包含的區間,就可以直接取走了。否則,把目前的區間分成兩半,然後遞迴去找。
比如我們要查詢區間 \([2,5]\) 的和。
\([1,5]\) 不完全屬於 \([2,5]\),分成 \([1,3]\) 和 \([4,5]\)。
\([1,3]\) 不完全屬於 \([2,5]\)。分成 \([1,2]\) 和 \([3,3]\)。
\([1,2]\) 不完全屬於 \([2,5]\)。分成 \([1,1]\) 和 \([2,2]\)。
\([1,1]\) 不屬於 \([2,5]\)。返回 \(0\)。
\([2,2]\) 屬於 \([2,5]\)。返回 \(t\) 陣列中 \([2,2]\) 對應的 \(2\)。
\([1,2]\) 收到返回值 \(0\) 和 \(2\)。相加得到 \(2\)。\([1,2]\) 返回 \(2\)。
\([3,3]\) 屬於 \([2,5]\)。返回 \(t\) 陣列中 \([3,3]\) 對應的 \(3\)。
\([1,3]\) 收到返回值 \(2\) 和 \(3\)。相加得到 \(5\)。\([1,3]\) 返回 \(5\)。
\([4,5]\) 屬於 \([4,5]\)。返回 \(t\) 陣列中 \([4,5]\) 對應的 \(9\)。
\([1,5]\) 收到返回值 \(5\) 和 \(9\)。相加得到 \(14\)。查詢函式結束,函式返回值 \(14\)。
看似複雜,只要畫個圖,就明白了。自己嘗試一下吧。可以結合程式碼。
int query(int now,int tl,int tr,int l,int r){ //l和r表示查詢的區間
if(tl>=l&&tr<=r){ //[tl,tr]完全屬於[l,r]
return t[now];
}
if(tl>r||tr<l){ //[tl,tr]不再[l,r]範圍內
return 0;
}
int mid=(tl+tr)/2; //不完全屬於
return query(now*2,tl,mid,l,r)+query(now*2+1,mid+1,tr,l,r);
}
自己執行試一下,也可以自己修改程式碼:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],t[N*4];
void build(int now,int tl,int tr){
if(tl==tr){
t[now]=a[tl];
return ;
}
int mid=(tl+tr)/2;
build(now*2,tl,mid);
build(now*2+1,mid+1,tr);
t[now]=t[now*2]+t[now*2+1];
}
int query(int now,int tl,int tr,int l,int r){
if(tl>=l&&tr<=r){
return t[now];
}
if(tl>r||tr<l){
return 0;
}
int mid=(tl+tr)/2;
return query(now*2,tl,mid,l,r)+query(now*2+1,mid+1,tr,l,r);
}
int main(){
n=5;
a[1]=1,a[2]=2,a[3]=3,a[4]=4,a[5]=5;
build(1,1,n);
cout<<query(1,1,n,2,5);
return 0;
}
單點修改:其實和build還是區別不大。你可以嘗試自己理解。通常情況下會是區間增加。
void modify(int now,int tl,int tr,int pos,int x){ //修改陣列中下標為pos的數為x
if(tl>pos||tr<pos){ //不在範圍內
return ;
}
if(tl==tr){ //這個點就是要修改的點
t[now]=x;
return ;
}
int mid=(tl+tr)/2;
modify(now*2,tl,mid,pos,x);
modify(now*2+1,mid+1,tr,pos,x);
t[now]=t[now*2]+t[now*2+1]; //這個地方不能忘,修改之後要更新所有祖先的值
}
結合區間查詢的程式碼(可以自行修改):
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],t[N*4];
void build(int now,int tl,int tr){
if(tl==tr){
t[now]=a[tl];
return ;
}
int mid=(tl+tr)/2;
build(now*2,tl,mid);
build(now*2+1,mid+1,tr);
t[now]=t[now*2]+t[now*2+1];
}
int query(int now,int tl,int tr,int l,int r){
if(tl>=l&&tr<=r){
return t[now];
}
if(tl>r||tr<l){
return 0;
}
int mid=(tl+tr)/2;
return query(now*2,tl,mid,l,r)+query(now*2+1,mid+1,tr,l,r);
}
void modify(int now,int tl,int tr,int pos,int x){
if(tl>pos||tr<pos){
return ;
}
if(tl==tr){
t[now]=x;
return ;
}
int mid=(tl+tr)/2;
modify(now*2,tl,mid,pos,x);
modify(now*2+1,mid+1,tr,pos,x);
t[now]=t[now*2]+t[now*2+1];
}
int main(){
n=5;
a[1]=1,a[2]=2,a[3]=3,a[4]=4,a[5]=5;
build(1,1,n);
modify(1,1,n,4,5);
cout<<query(1,1,n,2,5);
return 0;
}
以上所有操作的時間複雜度都是 \(O(\log n)\) 的。對於單點查詢,最多會查到數中最深的點,而一棵完全二叉樹的深度大概時 \(\log n\) 左右。對於區間查詢和單點修改,同理。進行區間操作時,會及時停止遞迴(當某子樹不在查詢範圍內時),實際上遞迴的次數是低於 \(\log n\) 的,可以自己舉幾個例子試試看。
區間增加,可以把區間每個數都單獨單點修改一次,但這樣會變成 \(O(n \log n)\),\(n\) 次區間修改就是 \(O(n^2 \log n)\)。這塊內容下次再講,我們先看例題,練習一下基礎。
習題
第一題
模板題。記得開long long
。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+10;
ll n,m,t[N*4];
ll query(ll now,ll tl,ll tr,ll l,ll r){
if(tl>=l&&tr<=r){
return t[now];
}
if(tl>r||tr<l){
return 0;
}
ll mid=(tl+tr)/2;
return query(now*2,tl,mid,l,r)+query(now*2+1,mid+1,tr,l,r);
}
void add(ll now,ll tl,ll tr,ll pos,ll x){
if(tl>pos||tr<pos){
return ;
}
if(tl==tr){
t[now]+=x;
return ;
}
ll mid=(tl+tr)/2;
add(now*2,tl,mid,pos,x);
add(now*2+1,mid+1,tr,pos,x);
t[now]=t[now*2]+t[now*2+1];
}
int main(){
//freopen("xx.in","r",stdin);
//freopen("xx.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(ll i=1;i<=m;i++){
ll k,a,b;
cin>>k>>a>>b;
if(k){
cout<<query(1,1,n,a,b)<<"\n";
}else{
add(1,1,n,a,b);
}
}
return 0;
}
第二題
這道題有點挑戰。看似沒法透過時間限制,但是這道題的操作是平方根,\(10^{12}\) 開 \(6\) 次平方根就是 \(1\) 了。所以在修改的時候,如果發現一個子樹都是 \(1\),就不用修改了,因為 \(1\) 的平方根還是 \(1\)。否則,只能一直遞迴,直到葉子節點,再把它取平方根。記得t[now]=t[now*2]+t[now*2+1]
。判定子樹都為 \(1\) 的方法很多,你可以看看子樹的和是否等於子樹大大小,或者專門再寫一個線段樹維護是否都是 \(1\)。這道題思路懂了,就好寫了。有坑,注意 \(l\) 可能大於 \(r\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+10;
ll n,m,a[N],t[N*8];
void build(ll now,ll tl,ll tr){
if(tl==tr){
t[now]=a[tl];
return ;
}
int mid=(tl+tr)/2;
build(now*2,tl,mid);
build(now*2+1,mid+1,tr);
t[now]=t[now*2]+t[now*2+1];
}
ll query(ll now,ll tl,ll tr,ll l,ll r){
if(tl>=l&&tr<=r){
return t[now];
}
if(tl>r||tr<l){
return 0;
}
ll mid=(tl+tr)/2;
return query(now*2,tl,mid,l,r)+query(now*2+1,mid+1,tr,l,r);
}
void modify(ll now,ll tl,ll tr,ll l,ll r){
if(tl>r||tr<l){
return ;
}
if(tl==tr){
t[now]=sqrt(t[now]);
return ;
}
ll mid=(tl+tr)/2;
if(t[now*2]!=mid-tl+1){
modify(now*2,tl,mid,l,r);
}
if(t[now*2+1]!=tr-mid){
modify(now*2+1,mid+1,tr,l,r);
}
t[now]=t[now*2]+t[now*2+1];
}
int main(){
//freopen("xx.in","r",stdin);
//freopen("xx.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(ll i=1;i<=n;i++){
cin>>a[i];
}
build(1,1,n);
cin>>m;
for(ll i=1;i<=m;i++){
ll k,l,r;
cin>>k>>l>>r;
if(l>r){
swap(l,r);
}
if(k){
cout<<query(1,1,n,l,r)<<"\n";
}else{
modify(1,1,n,l,r);
}
}
return 0;
}
如果這道題你是不看題解程式碼AC的,證明你對線段樹的最基礎的部分已經足夠熟悉了。但是線段樹的的基本操作比這個難,加油吧~