線段樹學習筆記
對線段樹的介紹
- 線段樹是一種樹形資料結構,屬於二叉樹
- 一棵線段樹的每個結點都可以用區間\([l,r]\)來表示。線段樹的葉結點表示的區間為\([l,l]\)或\([r,r]\)。
- 線段樹支援單點修改,區間修改,區間和/區間最值等的查詢。
- 對於線段樹的區間修改一般採用延遲修改方式
- 線段樹去掉最後一層後是一棵滿二叉樹
線段樹的建樹
線段樹的表示
因為線段樹是一棵二叉樹,所以可以採用與二叉堆一致的表示法:
編號為p的結點的
- 左結點的編號為\(p\times2\)
- 右結點的編號為\(p\times2+1\)
類似二叉堆,線段樹是一個結構體陣列。
C++ - 6 行
struct SegmentTree
{
int l,r;// 表示該結點代表的區間
int dat;//區間[l,r]的最大值
int add;//延遲修改標記
}
線段樹的結點
為了避免最最讓人難受的RE,所以考慮一棵線段樹的最大結點數。
設現在需要表示區間\([1,n]\),使得構造的線段樹為一棵滿二叉樹。
由線段樹的性質得知,該二叉樹有\(n\)個葉結點,則總結點數為\(n+n\div2+n\div4+···+2+1\),即\(4n-1\)。
因為我的個人習慣,我一般會不處理一個陣列的第一個元素,即下標為\(0\)的元素。
C++ - 27 行
#include<bits/stdc++.h>
using namespace std;
struct SegmentTree
{
int l,r;// 表示該結點代表的區間
int dat;//區間[l,r]的最大值
int add;//延遲修改標記
} t[4*1000001];
int va[114514];//區間是陣列下標區間,這是線段樹葉結點實際代表的值
void bulid(int l,int r,int p)// 結點的陣列下標為p,表示區間[l,r]
{
t[p].l = l;//代表區間初始化
t[p].r = r;
if(l == r)//葉結點
{
t[p].dat = va[l];
return;// 如果你想RE,建議刪去這行
}
int mid = (t[p].l + t[p].r) / 2;
bulid(l,mid,p*2);// 左子結點
bulid(mid+1,r,p*2+1);//右子結點
t[p].dat = max(t[p*2].dat,t[p*2+1].dat);//自下往上更新資料
}
int main()
{
//略
}
線段樹的區間修改&延遲修改
區間修改
對於訪問到的每一個結點,如果修改範圍完全包括了它,那麼直接修改+打延遲修改標記。否則:
-
若修改範圍與該結點的左結點有重合,則遞迴修改左結點
-
若修改範圍與該結點的右結點有重合,則遞迴修改右結點
C++ - 20 行
#include<bits/stdc++.h>
using namespace std;
void change(int l,int r,int add,int p)//add 為區間修改所要加上的值
{
if(l <= t[p].l && t[p].r <= r)
{
t[p].add += add;
t[p].dat += add;
return ;
}
spread(p);
int mid = (t[p].l + t[p].r) / 2;
if(l <= mid)
{
change(l,r,add,p*2);
}
if(mid < r)
{
change(l,r,add,p*2+1);
}
}
延遲修改
延遲修改的含義並不是當訪問到該結點時在進行修改,而是在訪問到它時,它已經被修改,若其不是葉結點,則給它的兩個子結點,進行對應修改並打上標記,代表該結點已被修改,但其子結點沒有被修改。
C++ - 16 行
#include<bits/stdc++.h>
using namespace std;
void spread(int p)// p為父結點下標
{
if(t[p].add)
{
t[p*2].add = t[p*2+1].add = t[p].add;//給子結點打標記
t[p*2].dat += t[p].add;
t[p*2+1].dat += t[p].add;//因為是最大值,所以直接加上就行
t[p].add = 0;//刪除標記
}
}
int main()
{
//略
}
線段樹的區間查詢
對於訪問到的每一個結點,如果查詢範圍完全包括了它,那麼直接返回。否則:
-
若查詢範圍與該結點的左結點有重合,則遞迴查詢左結點
-
若查詢範圍與該結點的右結點有重合,則遞迴查詢右結點
C++ - 18 行
#include<bits/stdc++.h>
using namespace std;
long long ask(int l,int r,int p)
{
if(l <= t[p].l && t[p].r <= r)
{
return t[p].dat;
}
spread(p);
int mid = (t[p].l + t[p].r) / 2;
long long dat=0x8000000000000000;
if(l <= mid)
{
dat=max(dat,ask(l,r,p*2));
}
if(mid < r)
{
dat=max(dat,ask(l,r,p*2+1));
}
return dat;
}
The end
完整程式碼
#include<bits/stdc++.h>
using namespace std;
struct SegmentTree
{
int l,r;// 表示該結點代表的區間
long long dat;//區間[l,r]的最大值
long long add;//延遲修改標記
} t[4*1000001];
long long va[114514];//區間是陣列下標區間,這是線段樹葉結點實際代表的值
void bulid(int l,int r,int p)// 結點的陣列下標為p,表示區間[l,r]
{
t[p].l = l;//代表區間初始化
t[p].r = r;
if(l == r)//葉結點
{
t[p].dat = va[l];
return;// 如果你想RE,建議刪去這行
}
int mid = (l + r) / 2;
bulid(l,mid,p*2);// 左子結點
bulid(mid+1,r,p*2+1);//右子結點
t[p].dat=max(t[p*2].dat , t[p*2+1].dat );//自下往上更新資料
}
void spread(int p)// p為父結點下標
{
if(t[p].add )
{
t[p*2].add += t[p].add;
t[p*2+1].add += t[p].add;//給子結點打標記
t[p*2].dat += t[p].add;
t[p*2+1].dat += t[p].add;//因為是最大值,所以直接加上就行
//刪除標記
}
t[p].add = 0;
}
void change(int l,int r,int add,int p)//add 為區間修改所要加上的值
{
if(l <= t[p].l && t[p].r <= r)
{
t[p].add += add;
t[p].dat += add;
return ;
}
spread(p);
int mid = (t[p].l + t[p].r) / 2;
if(l <= mid)
{
change(l,r,add,p*2);
}
if(mid < r)
{
change(l,r,add,p*2+1);
}
t[p].dat=max(t[p*2].dat , t[p*2+1].dat );
}
long long ask(int l,int r,int p)
{
if(l <= t[p].l && t[p].r <= r)
{
return t[p].dat;
}
spread(p);
int mid = (t[p].l + t[p].r) / 2;
long long dat=0x8000000000000000;
if(l <= mid)
{
dat=max(dat,ask(l,r,p*2));
}
if(mid < r)
{
dat=max(dat,ask(l,r,p*2+1));
}
return dat;
}
int main()
{
int n,m;
cin>>n>>m;
bulid(1,n,1);//建樹
for(int i=1,op,l,r,add;i<=m;i++)//op為操作型別,[l,r]表示區間,add為修改加上的值
{
cin>>op>>l>>r;
if(op==1)
change(l,r,add,1);
if(op==2)
{
cin>>add;
cout<<ask(l,r,1)<<endl;
}
}
}