線段樹及Lazy-Tag

一:線段樹
線段樹是一種二叉搜索樹,與區間樹類似,它將一個區間劃分紅一些單元區間,每一個單元區間對應線段樹中的一個葉結點。使用線段樹能夠快速的查找某一個節點在若干條線段中出現的次數,時間複雜度爲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,則左右子結點分別表示爲2i2i+1,而樹中就直接寫爲i<<1i<<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(L1)−1   =4L−5,L2

來看一道題:
例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;
}
相關文章
相關標籤/搜索