數據結構的半夜——線段樹學習筆記1
說過以後寫blog要嚴肅點,我現在就嚴肅地修改一下,刪冗余,精簡語言
線段樹,英文Segment Tree
這種樹形數據結構十分容易形象地繪成圖形
如圖所示,線段樹有以下性質:
- 線段樹本質是棵二叉樹.
- 線段樹每個節點是代表一個區間[l,r],而每個節點的兩個子樹分別[l,mid],[mid+1,r]。
- 葉子節點l等於r且長度為1
- 線段樹深度不超過logn
綜上所述,我們可以按二叉樹的建樹方法,若節點編號為th,令節點的左兒子編號為th x 2,而右兒子記為th x 2+1。
kuanbin帶你飛的前三道例題,是非常好的線段樹入門例題,可以借此來初步理解線段樹以及線段樹的基本應用
A-敵兵布陣 HDU-1166
C國的死對頭A國這段時間正在進行軍事演習,所以C國間諜頭子Derek和他手下Tidy又開始忙乎了。A國在海岸線沿直線布置了N個工兵營地,Derek和Tidy的任務就是要監視這些工兵營地的活動情況。由於采取了某種先進的監測手段,所以每個工兵營地的人數C國都掌握的一清二楚,每個工兵營地的人數都有可能發生變動,可能增加或減少若幹人手,但這些都逃不過C國的監視。
中央情報局要研究敵人究竟演習什麽戰術,所以Tidy要隨時向Derek匯報某一段連續的工兵營地一共有多少人,例如Derek問:“Tidy,馬上匯報第3個營地到第10個營地共有多少人!”Tidy就要馬上開始計算這一段的總人數並匯報。但敵兵營地的人數經常變動,而Derek每次詢問的段都不一樣,所以Tidy不得不每次都一個一個營地的去數,很快就精疲力盡了,Derek對Tidy的計算速度越來越不滿:"你個死肥仔,算得這麽慢,我炒你魷魚!”Tidy想:“你自己來算算看,這可真是一項累人的工作!我恨不得你炒我魷魚呢!”無奈之下,Tidy只好打電話向計算機專家Windbreaker求救,Windbreaker說:“死肥仔,叫你平時做多點acm題和看多點算法書,現在嘗到苦果了吧!”Tidy說:"我知錯了。。。"但Windbreaker已經掛掉電話了。Tidy很苦惱,這麽算他真的會崩潰的,聰明的讀者,你能寫個程序幫他完成這項工作嗎?不過如果你的程序效率不夠高的話,Tidy還是會受到Derek的責罵的.
Input
第一行一個整數T,表示有T組數據。
每組數據第一行一個正整數N(N<=50000),表示敵人有N個工兵營地,接下來有N個正整數,第i個正整數ai代表第i個工兵營地裏開始時有ai個人(1<=ai<=50)。
接下來每行有一條命令,命令有4種形式:
(1) Add i j,i和j為正整數,表示第i個營地增加j個人(j不超過30)
(2)Sub i j ,i和j為正整數,表示第i個營地減少j個人(j不超過30);
(3)Query i j ,i和j為正整數,i<=j,表示詢問第i到第j個營地的總人數;
(4)End 表示結束,這條命令在每組數據最後出現;
每組數據最多有40000條命令
Output
對第i組數據,首先輸出“Case i:”和回車,
對於每個Query詢問,輸出一個整數並回車,表示詢問的段中的總人數,這個數保持在int以內。
Sample Input
1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End
Sample Out
Case 1:
6
33
59
由題意可知,這是一個單點修改與區間詢問的問題
個人習慣
#define lson th<<1
#define rson th<<1|1
由於綜上線段樹的性質(2),可以用這種方式來表示他們左右子節點。
分步解構
遞歸建樹:
void pushup(int th)
{
sum[th]=sum[lson]+sum[rson];//簡單的結合律
}
void build(int l,int r,int th)
{
if(l==r)//當為葉子節點時
{
tree[th]=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,lson);//構建左節點
build(mid+1,r,rson);//構建右節點
pushup(th);//從兩個子節點更新
}
單點修改,運用二分思想
void update(int l,int r/*當前區間的左右端點*/,
int pos/*所要修改的點*/,
int th/*子樹的編號*/,
int k/*所要修改的值*/)
{
if(l==pos && r==pos)//當找到這個節點時修改
{
sum[th]+=k;
return;
}
int mid=(l+r)>>1;//一分為二
if(pos<=mid) update(l,mid,pos,lson,k);//當pos在右區間時,遞歸右子樹
else update(mid+1,r,pos,rson,k);//當pos在左區間時,遞歸左子樹
pushup(th);//動過了當然要自下而上更新啦
}
時間復雜度網上大有分析,這裏便不贅述
區間詢問
int query(int l,int r,int x,int y,int th)
{
if(x<=l&&r<=y){
return sum[th];//當被詢問區間完全包含當前區間時,直接返回
}
int res=0;
int mid=(l+r)>>1;
if(x<=mid) res+=query(l,mid,x,y,lson);
if(mid<y) res+=query(mid+1,r,x,y,rson);
return res;
}
標程
#include<cstdio>
#include<iostream>
#include<cstring>
#define maxn 50005
#define lson th<<1
#define rson th<<1|1
int sum[maxn*4];
int a[maxn];
void pushup(int th)
{
sum[th]=sum[lson]+sum[rson];
}
void build(int l,int r,int th)
{
if(l==r)
{
sum[th]=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,lson);
build(mid+1,r,rson);
pushup(th);
}
void update(int l,int r,int pos,int th,int k)
{
if(l==pos && r==pos)
{
sum[th]+=k;
return;
}
int mid=(l+r)>>1;
if(pos<=mid) update(l,mid,pos,lson,k);
else update(mid+1,r,pos,rson,k);
pushup(th);
}
int query(int l,int r,int x,int y,int th)
{
if(x<=l&&r<=y){
return sum[th];
}
int res=0;
int mid=(l+r)>>1;
if(x<=mid) res+=query(l,mid,x,y,lson);
if(mid<y) res+=query(mid+1,r,x,y,rson);
return res;
}
int main()
{
int T;
scanf("%d",&T);
for(int i=1;i<=T;i++)
{
printf("Case %d:\n",i);
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",a+i);
build(1,n,1);
char str[7];
while(scanf("%s",str)){
if(str[0]=='E') break;
if(str[0]=='A')
{
int pos,k;
scanf("%d%d",&pos,&k);
update(1,n,pos,1,k);
}
else if(str[0]=='S')
{
int pos,k;
scanf("%d%d",&pos,&k);
update(1,n,pos,1,-1*k);
}
else{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",query(1,n,x,y,1));
}
}
}
}
B-I Hate It HDU-1754
Time Limit: 9000/3000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 107180 Accepted Submission(s): 40245
Problem Description
很多學校流行一種比較的習慣。老師們很喜歡詢問,從某某到某某當中,分數最高的是多少。
這讓很多學生很反感。
不管你喜不喜歡,現在需要你做的是,就是按照老師的要求,寫一個程序,模擬老師的詢問。當然,老師有時候需要更新某位同學的成績。
Input
本題目包含多組測試,請處理到文件結束。
在每個測試的第一行,有兩個正整數 N 和 M ( 0<N<=200000,0<M<5000 ),分別代表學生的數目和操作的數目。
學生ID編號分別從1編到N。
第二行包含N個整數,代表這N個學生的初始成績,其中第i個數代表ID為i的學生的成績。
接下來有M行。每一行有一個字符 C (只取‘Q‘或‘U‘) ,和兩個正整數A,B。
當C為‘Q‘的時候,表示這是一條詢問操作,它詢問ID從A到B(包括A,B)的學生當中,成績最高的是多少。
當C為‘U‘的時候,表示這是一條更新操作,要求把ID為A的學生的成績更改為B。
Output
對於每一次詢問操作,在一行裏面輸出最高成績。
Sample Input
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
Sample Output
5
6
5
9
HintHuge input,the C function scanf() will work better than cin
由題意可知,此題為區間最大值與單點修改,較上面A題,編碼難度較為簡單
這裏單點修改與建樹就不再贅述,因為這裏只要修改pushup()函數
void pushup(int th)
{
maxx[th]=max(maxx[lson],maxx[rson]);
}
簡單的詢問
int query(int l,int r,int x,int y,int th)
{
if(x>r || y<l) return 0; //完全沒有交集的情況
if(x<=l && r<=y) return maxx[th]; //詢問區間包含當前區間
int mid=(l+r) >> 1;
return max(query(l,mid,x,y,lson),query(mid+1,r,x,y,rson)); //遞歸詢問
}
由這道題,大家應該能舉一反三,寫出區間最小值
標程
#include<cstdio>
#include<iostream>
#define maxn 200005
#define lson th<<1
#define rson th<<1|1
using namespace std;
int maxx[maxn*4];
int a[maxn];
void pushup(int th)
{
maxx[th]=max(maxx[lson],maxx[rson]);
}
void build(int l,int r,int th)
{
if(l==r)
{
maxx[th]=a[l];
return;
}
int mid=(l+r) >> 1;
build(l,mid,lson);
build(mid+1,r,rson);
pushup(th);
}
void update(int l,int r,int pos, int th ,int k)
{
if(l==pos&&r==pos)
{
maxx[th]=k;
return;
}
int mid=(l+r) >> 1;
if(pos<=mid) update(l,mid,pos,lson,k);
else update(mid+1,r,pos,rson,k);
pushup(th);
}
int query(int l,int r,int x,int y,int th)
{
if(x>r || y<l) return 0;
if(x<=l && r<=y) return maxx[th];
int mid=(l+r) >> 1;
return max(query(l,mid,x,y,lson),query(mid+1,r,x,y,rson));
}
int n,m;
int main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
build(1,n,1);
for(int i=1;i<=m;i++)
{
char str[5];
scanf("%s",str);
if(str[0]=='Q')
{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",query(1,n,x,y,1));
}
else{
int pos,k;
scanf("%d%d",&pos,&k);
update(1,n,pos,1,k);
}
}
}
return 0;
}
C-A Simple Problem with Integers
Time Limit: 5000MS Memory Limit: 131072K
Total Submissions: 147182 Accepted: 45731
Case Time Limit: 2000MS
Description
You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.
Input
The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1, A2, ... , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
"C a b c" means adding c to each of Aa, Aa+1, ... , Ab. -10000 ≤ c ≤ 10000.
"Q a b" means querying the sum of Aa, Aa+1, ... , Ab.
Output
You need to answer all Q commands in order. One answer in a line.
Sample Input
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
Sample Output
4
55
9
15
Hint
The sums may exceed the range of 32-bit integers.
Source
POJ Monthly--2007.11.25, Yang Yi
我為大家大致翻譯一下題幹,意為需維護一個長度為N的int數組A,相對應的Q組詢問,有兩種操作,C l r k 意為將[l,r]區間每個數加上k,Q l r,詢問[l,r]的區間和。
這便是區間修改問題。
這裏要使用一種標記數組,lazytag
lazytag存儲在父節點上,為該區間總共需要加上的數值,所以為了維護線段樹,每次向下層遞歸前,都要下傳lazytag
我們先來看一下區間修改函數
inline void update(ll l,/*當前區間左端點*/
ll r,/*當前區間右端點*/
ll x,/*所詢問區間左端點*/
ll y,/*所詢問區間右端點*/
ll th,
ll k)
{
if(x<=l && r<=y)//若完全包含,便把值記錄在tag上
{
add(l,r,th,k);
return;
}
pushdown(l,r,th);//遞歸前下傳標記
ll mid=l+r >>1;
if(x<=mid) update(l,mid,x,y,lson,k);
if(y>mid) update(mid+1,r,x,y,rson,k);
pushup(th);
}
add函數
inline void add(ll l,ll r,ll th,ll k)
{
tag[th]+=k;//tag更新
sum[th]+=(r-l+1)*k;//自身維護
return;
}
這裏有個pushdown函數,是pushup函數的反向操作
inline void pushdown(ll l,ll r,ll th)
{
if(tag[th]==0) return;//若tag為0則返回
ll mid=l+r >>1;
add(l,mid,lson,tag[th]);//下傳標記
add(mid+1,r,rson,tag[th]);
tag[th]=0;//別忘了清零
}
這裏為什麽只下傳一層呢:因為正如之前所說,每次向下遞歸前都要下傳標記,所以到下層標記自然會下傳
所以這裏的詢問有了些小小的變化
ll query(ll l,ll r,ll x,ll y,ll th)
{
if(x<=l && r<=y)
{
return sum[th];
}
ll mid=l+r >>1;
ll res=0;
pushdown(l,r,th);
if(x<=mid) res+=query(l,mid,x,y,lson);
if(y>mid) res+=query(mid+1,r,x,y,rson);
return res;
}
然而為什麽要用longlong呢,題中明明說是int數組
這時我們再通讀題面,會發現一行話
The sums may exceed the range of 32-bit integers.
區間和可能會超過32位int類型
這便是要用longlong的原因
標程
#include<cstdio>
#include<cstring>
#include<iostream>
#define lson th<<1
#define rson th<<1|1
#define maxn 100005
typedef long long ll;
using namespace std;
ll sum[maxn*4];
ll a[maxn];
ll n,m;
ll tag[maxn*4];
inline void add(ll l,ll r,ll th,ll k)
{
tag[th]+=k;
sum[th]+=(r-l+1)*k;
return;
}
inline void pushup(ll th)
{
sum[th]=sum[lson]+sum[rson];
return;
}
inline void pushdown(ll l,ll r,ll th)
{
if(tag[th]==0) return;
ll mid=l+r >>1;
add(l,mid,lson,tag[th]);
add(mid+1,r,rson,tag[th]);
tag[th]=0;
}
inline void build(ll l,ll r,ll th)
{
if(l==r)
{
sum[th]=a[l];
return;
}
ll mid=l+r >> 1;
build(l,mid,lson);
build(mid+1,r,rson);
pushup(th);
}
inline void update(ll l,ll r,ll x,ll y,ll th,ll k)
{
if(x<=l && r<=y)
{
add(l,r,th,k);
return;
}
pushdown(l,r,th);
ll mid=l+r >>1;
if(x<=mid) update(l,mid,x,y,lson,k);
if(y>mid) update(mid+1,r,x,y,rson,k);
pushup(th);
}
ll query(ll l,ll r,ll x,ll y,ll th)
{
if(x<=l && r<=y)
{
return sum[th];
}
ll mid=l+r >>1;
ll res=0;
pushdown(l,r,th);
if(x<=mid) res+=query(l,mid,x,y,lson);
if(y>mid) res+=query(mid+1,r,x,y,rson);
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
build(1,n,1);
while(m--)
{
char str[5];
scanf("%s",str);
if(str[0]=='C')
{
ll x,y,k;
scanf("%lld%lld%lld",&x,&y,&k);
update(1,n,x,y,1,k);
}
else{
ll x,y;
scanf("%lld %lld",&x,&y);
printf("%lld\n",query(1,n,x,y,1));
}
}
}
理解了之後代碼難度並不是非常大,畢竟只是最最基礎的應用而已。
數據結構的半夜——線段樹學習筆記1