一:線段樹
線段樹是一種二叉搜索樹,與區間樹類似,它將一個區間劃分紅一些單元區間,每一個單元區間對應線段樹中的一個葉結點。使用線段樹能夠快速的查找某一個節點在若干條線段中出現的次數,時間複雜度爲O(log2N)。
線段樹的每一個節點都表示一個區間[L, R],對於一個線段樹的區間:
若L < R,則必能被分爲[L, M]和[M+1, R],其中M = (L + R) / 2。
若L = R,則爲葉子節點。
實現方法:
數組實現:節點T的左兒子是2T,表明[L, M]區間,右兒子是2T+1,表明[M+1,R]區間。
結構體指針實現:左右子樹爲*l,*r。
三個重點:
1.線段樹的構建node
int create_tree(int h,int x,int y)
{
tree[h].l=x;tree[h].r=y;//當前節點的區間賦值爲[x,y];
if(x==y)//若當前節點爲葉子節點,則更新該點權值,返回給父親節點。
{
tree[h].s=a[x];
return tree[h].s;
}
int mid=(x+y)/2;//向下
int x1=create_tree(h*2,x,mid);//更新當前節點;
int x2=create_tree(h*2+1,mid+1,y);
tree[h].s=max(x1,x2);//更新權值
return tree[h].s;
}
2線段樹的查詢ios
int query(int 當前節點,int L,int R)
{
若是[L,R]與當前節點區間無交集,則返回;
若[L,R]包含當前節點區間,則返回所求值;
搜索左右子樹;
返回值;
}
3.線段樹的更新c++
void update(int 當前節點,int L,int R)
{
若是[L,R]與當前節點區間無交集,則返回;
若[L,R]包含當前節點區間,則返回所求值,中止遞歸;
搜索左右子樹;
從新計算本節點信息;
返回;
}
下面有道例題:
例1 I hate it(hdu 1754)
題目描述:
不少學校流行一種比較的習慣。老師們很喜歡詢問,從某某到某某當中,分數最高的是多少。
這讓不少學生很反感。無論你喜不喜歡,如今須要你作的是,就是按照老師的要求,寫一個程序,模擬老師的詢問。固然,老師有時候須要更新某位同窗的成績。
本題目包含多組測試
在每一個測試的第一行,有兩個正整數 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。
對於每一次詢問操做,在一行裏面輸出最高成績。算法
輸入
本題目包含多組測試,請處理到文件結束。
在每一個測試的第一行,有兩個正整數 N 和 M 分別表明學生的數目和操做的數目。
學生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。數組
輸出
對於每一次詢問操做,在一行裏面輸出最高成績。ruby
樣例輸入
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5markdown
樣例輸出
5
6
5
9函數
分析
最容易想到的算法是將成績存到數組裏,而後對於每一條查詢,遍歷數組的每個元素。總時間複雜度是O(NM),實在是太大了。根據題目,咱們能夠用線段樹來存儲[x,y]區間中成績的最大值,這樣作的時間複雜度只有O(MlogN)。測試
參考代碼:ui
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=200000+10;
struct node//定義線段樹
{
int s;//權值
int l,r;//左右子樹權值
};
struct node tree[maxn*10];
int a[maxn];
int create_tree(int h,int x,int y)//建樹(h爲樹編號)
{
tree[h].l=x;tree[h].r=y;//記錄區間[l,r]
if(x==y)//葉子結點
{
tree[h].s=a[x];//記錄權值
return tree[h].s;//返回權值
}
int mid=(x+y)/2;//取中點(int自動取整)
int x1=create_tree(h*2,x,mid);//左子樹權值
int x2=create_tree(h*2+1,mid+1,y);//右子樹權值
tree[h].s=max(x1,x2);//取更大值
return tree[h].s;//返回權值
}
int query(int h,int x,int y)//查詢
{
if(y<tree[h].l||x>tree[h].r)//...x2---y2...l——r...x1---y1...
return 0;
if(x<=tree[h].l&&tree[h].r<=y)//達到範圍...x---l——r---y...
return tree[h].s;//返回權值
int x1=query(2*h,x,y);//左子樹
int x2=query(2*h+1,x,y);//右子樹
return max(x1,x2);//返回權值
}
int update(int h,int x)//維護線段樹
{
if(x<tree[h].l || x>tree[h].r)//超過範圍...x1...l——r...x2...
return tree[h].s;//返回權值
if(tree[h].l==tree[h].r)//左右子樹相同
{
tree[h].s=a[tree[h].l];//改權值
return tree[h].s;//返回權值
}
int x1=update(2*h,x);//左子樹
int x2=update(2*h+1,x);//右子樹
tree[h].s=max(x1,x2);//改權值
return tree[h].s;//返回權值
}
int main()
{
int i,j,k,m,n;int x,y;char c;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++) scanf("%d",&a[i]);
create_tree(1,1,n);
for(i=1;i<=m;i++)
{
getchar();//過濾換行
scanf("%c%d%d",&c,&x,&y);//取得指令
if(c=='Q')
{printf("%d\n",query(1,x,y));}
else
{a[x]=y;update(1,x);}
}
return 0;
}
二:Lazy-Tag
lazy-tag思想,記錄每個線段樹節點的變化值,當這部分線段的一致性被破壞咱們就將這個變化值傳遞給子區間,大大增長了線段樹的效率。
在此通俗的解釋我理解的Lazy意思:
如今須要對[a,b]區間值進行加c操做,那麼就從根節點[1,n]開始調用update函數進行更新操做;若是恰好執行到一個rt節點,並且tree[rt].l == a && tree[rt].r == b,這時咱們就應該一步更新此時rt節點的sum[rt]的值(sum[rt]+=c* (tree[rt].r - tree[rt].l + 1))。
關鍵來了,若是此時按照常規的線段樹的update操做,這時候還應該更新rt子節點的sum[]值,而Lazy思想偏偏是暫時不更新rt子節點的sum[]值,而是在這裏打一個tag,直接return。直到下次須要用到rt子節點的值的時候纔去更新,這樣避免許多可能無用的操做,從而節省時間 。
另外咱們常常在樹裏面用到位運算,簡單介紹一下:
(i<<n)==(i*2n) (i>>n)==(⌊i/2n⌋)
在找子樹的時候,若父親結點編號爲i,則左右子結點分別表示爲2i,2i+1,而樹中就直接寫爲i<<1和i<<1|1(「|」詳細自行百度),而尋找子節點能夠表示爲i>>1;
申請結構體的時候,要開到四倍長度空間,直接表示爲i<<2;
這裏再說明一下爲何要開四倍空間
假設咱們用一個數組來頭輕腳重地存儲一個線段樹,根節點是1,孩子節點分別是2n, 2n+1, 那麼,設線段長爲L(即[1..L+1))
設樹的高度爲H,對H,有:H(L)={1,1+H(⌈L2⌉)L>=1;
這是一個很簡單的遞歸式,並用公式逐次代換,就等到
H(L)=k+H(⌈L2k⌉),其中 k 是知足2k≥L的最小值,因此H(L)=⌈lgL⌉+1.
因此顯然所需空間爲
2^H−1=2^(⌈lgL⌉+1)−1
=2×2^(⌈lgL⌉)−1 =2×2(L−1)−1 =4L−5,L≥2
來看一道題:
例2:一個簡單的問題與整數 [POJ 3468]
題目描述
你有N個整數,A1,A2,…,AN。 你須要處理兩種操做。 一種類型的操做是在給定間隔中向每一個數字添加一些給定數目。 另外一個是要求給定間隔內的數字之和。
輸入
第一行包含兩個數字N和Q (1≤N,Q≤100000)
第二行包含N個數字,即A1,A2,…,AN的初始值。(-1000000000≤Ai≤1000000000)。
接下來的Q行中的每一行表示操做。
「C a b c」意味着把Aa,Aa+1,…,Ab中的每個都加上C(-10000≤c≤10000)。
「Q a b」表示查詢Aa,Aa+1,…,Ab的和。
輸出
按順序回答全部的「Q」命令。 一行中有一個答案。
樣例輸入
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
樣例輸出
4
55
9
15
*提示:可能超出int範圍
參考代碼
#include<cstdio>
using namespace std;
#define maxn 100000+10
typedef long long LL;
struct node{
int l,r,m;//左右中點
LL sum,mark;//權值、tag
}T[maxn<<2];
int a[maxn];
void build(int id,int l,int r){
T[id].l=l;//左端點
T[id].r=r;//右端點
T[id].m=(l+r)>>1;//中點
T[id].mark=0;//初始化標記
if(l==r)//達到端點
{T[id].sum=a[l];return;}//標記和,中止遞歸併返回
build(id<<1,l,T[id].m);//遞歸左子樹
build(id<<1|1,T[id].m+1,r);//遞歸右子樹
T[id].sum=(T[id<<1].sum+T[id<<1|1].sum);//記錄和
}
void update(int id,int l,int r,int val){
if(T[id].l==l&&T[id].r==r)//肯定是這一段了
{T[id].mark+=val;return;}//沒必要遞歸到葉子結點,打tag
T[id].sum+=(LL)val*(r-l+1);//更新權值
if(T[id].m>=r)//只要更新左子樹
update(id<<1,l,r,val);
else if(T[id].m<l)
update((id<<1)+1,l,r,val);//只要更新右子樹
else
{
update(id<<1,l,T[id].m,val);//更新左右子樹
update(id<<1|1,T[id].m+1,r,val);
}
}
LL query(int id,int l,int r){
if(T[id].l==l&&T[id].r==r)//找到結點
return T[id].sum+T[id].mark*(LL)(r-l+1);//權值+tag
if(T[id].mark)//原來更新到這裏的時候沒有繼續更新下去了(有tag)
{
T[id<<1].mark+=T[id].mark;//tag下傳
T[id<<1|1].mark+=T[id].mark;
T[id].sum+=(LL)(T[id].r-T[id].l+1)*T[id].mark;//把tag加回sum
T[id].mark=0;//去掉tag
}
if(T[id].m>=r){
return query(id<<1,l,r);//只有左子樹
}
else if(T[id].m<l){
return query(id<<1|1,l,r);//只有左子樹
}
else{
return query(id<<1,l,T[id].m)+query((id<<1)+1,T[id].m+1,r);//左右子樹都有
}
}
int main(){
int n,Q;
char str[8];
int b,c,d;
scanf("%d%d",&n,&Q);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build(1,1,n);//建樹
for(int i=0;i<Q;i++)
{
scanf("%s",str);
if(str[0]=='Q')
{
scanf("%d%d",&b,&c);
printf("%lld\n",query(1,b,c));//查詢
}
else
{
scanf("%d%d%d",&b,&c,&d);
update(1,b,c,d);//更新
}
}
return 0;
}
更大的挑戰:
例3 Count color[POJ 2777]
題目描述
有一個很是長的板,長度L釐米,L是一個正整數,因此咱們能夠均勻地劃分爲L段,他們從左到右標記爲1,2,… L,每一個是1釐米長。如今咱們必須着色板 - 一段只有一種顏色。咱們能夠在板上進行如下兩個操做:
1.「C A B C」使板材從板材A到板材C着色C.
2.「P A B」輸出在段A和段B(包括)之間繪製的不一樣顏色的數量。
在咱們的平常生活中,咱們有不多的詞來描述一種顏色(紅色,綠色,藍色,黃色…),因此你能夠假設不一樣顏色T的總數是很是小的。爲了簡單起見,咱們將顏色的名稱表示爲顏色1,顏色2,…顏色T.在開始時,板子以顏色1繪製。如今剩下的問題留給你。
輸入
第一行輸入包含L(1≤L≤100000),T(1≤T≤30)和O(1≤O≤100000)。這裏O表示操做的數量。在O行以後,每一個包含「C A B C」或「P A B」(這裏A,B,C是整數,A能夠大於B)做爲先前定義的操做。
輸出
輸出結果按順序輸出操做,每行包含一個數字。
樣例輸入
2 2 4
C 1 1 2
P 1 2
C 2 2 2
P 1 2
樣例輸出
2
1
分析
根據題目的數據規模,暴力求解顯然超時。因此就考慮用線段樹作。
說明
本題運用了線段樹中「區間修改」的思想,只修改目標區間而再也不繼續修改其子節點(lazy)
參考代碼:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=100010;
#define L(rt) (rt<<1)
#define R(rt) (rt<<1|1)
struct Tree{
int l,r;
int col; //用一個32位的int型,每一位對應一種顏色,用位運算代替bool col[32]
bool cover; //表示這個區間都被塗上同一種顏色提升效率
}tree[N<<2];
void build(int L,int R,int rt){
tree[rt].l=L;//左區間
tree[rt].r=R;//右區間
tree[rt].col=1; //開始時都爲塗有顏色1
tree[rt].cover=1;//固然只有一種顏色
if(tree[rt].l==tree[rt].r)
return ;//葉節點直接返回
int mid=(L+R)>>1;//取中點
build(L,mid,L(rt));//建左子樹
build(mid+1,R,R(rt));//建右子數
}
void PushDown(int rt){//下推
tree[L(rt)].col=tree[rt].col;
tree[L(rt)].cover=1;
tree[R(rt)].col=tree[rt].col;
tree[R(rt)].cover=1;
tree[rt].cover=0;//標記取消
}
void PushUp(int rt){//最後遞歸回來再更改父節點的顏色
tree[rt].col=tree[L(rt)].col | tree[R(rt)].col;//相加
}
void update(int val,int L,int R,int rt){
if(L<=tree[rt].l && R>=tree[rt].r){//區間在要求範圍內
tree[rt].col=val;//刷顏色
tree[rt].cover=1;//打標記
return;//不須要更新子樹了
}
if(tree[rt].col==val)//剪枝
return;//不須要更新子樹了
if(tree[rt].cover)//這裏面只有一種顏色
PushDown(rt);//下推
int mid=(tree[rt].l+tree[rt].r)>>1;
if(R<=mid)
update(val,L,R,L(rt));
else if(L>=mid+1)
update(val,L,R,R(rt));
else{
update(val,L,mid,L(rt));
update(val,mid+1,R,R(rt));
}
PushUp(rt); //上載
}
int sum;
void query(int L,int R,int rt)
{
if(L<=tree[rt].l && R>=tree[rt].r){
sum |= tree[rt].col;//把顏色加進和裏
return;
}
if(tree[rt].cover){//這個區間所有爲1種顏色,就沒有繼續分割區間的必要了
sum |= tree[rt].col;//顏色種類相加的位運算代碼
return;
}
int mid=(tree[rt].l+tree[rt].r)>>1;
if(R<=mid)
query(L,R,L(rt));
else if(L>=mid+1)
query(L,R,R(rt));
else
{
query(L,mid,L(rt));
query(mid+1,R,R(rt));
}
}
int solve()//位運算
{
int ans=0;
while(sum)
{
if(sum&1)
ans++;
sum>>=1;
}
return ans;
}
void swap(int &a,int &b)
{
int tmp=a;a=b;b=tmp;
}
int main()
{
int n,t,m;
scanf("%d%d%d",&n,&t,&m);
build(1,n,1);//建樹
char op[3];
int a,b,c;
while(m--)
{
scanf("%s",op);
if(op[0]=='C')
{
scanf("%d%d%d",&a,&b,&c);
if(a>b)
swap(a,b);
update(1<<(c-1),a,b,1); // int型的右起第c位變爲1,即2的c-1次方。
}
else
{
scanf("%d%d",&a,&b);
if(a>b)
swap(a,b);
sum=0;
query(a,b,1);
printf("%d\n",solve());
}
}
return 0;
}