1. 程式人生 > >線段樹學習筆記(單點更新+區間查詢最大值+lazy標記+pushdown操作+區間更新+求區間和)

線段樹學習筆記(單點更新+區間查詢最大值+lazy標記+pushdown操作+區間更新+求區間和)

目錄

  • 什麼是線段樹?

  • 線段樹基本操作:

  • 建立線段樹

  • 線段樹單點更新

  • 區間查詢最大最小值

  • 延遲標記(懶人標記)+pushdown操作

  • 區間更新

  • 求區間和

注:以下所有程式碼都是針對維護區間和的。

  • 什麼是線段樹?

    線段樹是一種二叉搜尋樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
    性質:對於線段樹中的每一個非葉子節點[a,b],它的左兒子表示的區間為[a,(a+b)/2],右兒子表示的區間為[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最後的子節點數目為N,即整個線段區間的長度。

  • 舉個例子

這裡寫圖片描述

  • 建立一個線段樹

線段樹成員:

struct node
{
    int val;  //具體數值
    int len;  //覆蓋區間長度
    int lazy; //延遲標記
    int l,r;  //左右兒子編號
}tree[300005];

建樹思想:遞迴建樹,遇到葉子節點直接賦值,否則遞迴遍歷左右建樹,最後回溯即可。

void build(int root,int l,int r)  //建樹 
{
    int mid;
    tree[root].l=l;tree[root].r=r;
    tree[root].len=r-l+1;
    if (l==r) tree[root].val=arr[l];
    else
{ mid=(l+r)/2; build(root*2,l,mid); //遞迴構造左子樹 build(root*2+1,mid+1,r); //遞迴構造右子數 tree[root].val=tree[root*2].val+tree[root*2+1].val;//儲存左右子樹的和 } }
  • 單點更新線段樹

遞迴更新,遞迴至要更新的葉子節點,回溯更改該葉子節點會導致其他節點的值

/*id: 待更新節點在原始陣列arr中的下標
  addVal: 更新的值(原來的值加上addVal)*/
void add(int
root,int id,int addval) //單點更新 { int mid; if (tree[root].l==tree[root].r) { tree[root].val+=addval; return; } else { mid=(tree[root].l+tree[root].r)/2; if (id<=mid) add(root*2,id,addval); else add(root*2+1,id,addval); tree[root].val=tree[root*2].val+tree[root*2+1].val; } }
  • 查詢區間最大值最小值

只需要在建樹的時候注意tree[].val儲存的是每個節點區間的最大值或最小值即可。

int ask(int root,int l,int r)
{
    int mid;
    if (tree[root].l==l&&tree[root].r==r)
        return tree[root].val;
    else 
    {
        mid=(tree[root].l+tree[root].r)/2;
        if (mid>=r)
            return ask(root*2,l,r);
        else if (mid<l)
            return ask(root*2+1,l,r); 
        else return ask(root*2,l,mid)+ask(root*2+1,mid+1,r);

    }
}
  • 區間更新

區間更新是指更新某個區間內的葉子節點的值,因為涉及到的葉子節點不止一個,而葉子節點會影響其相應的非葉父節點,那麼回溯需要更新的非葉子節點也會有很多,如果一次性更新完,操作的時間複雜度肯定不是O(lgn),例如當我們要更新區間[0,3]內的葉子節點時,需要更新出了葉子節點3,9外的所有其他節點。為此引入了線段樹中的延遲標記概念,這也是線段樹的精華所在。

延遲標記:每個節點新增加一個標記,記錄這個節點是否進行了某種修改(這種修改操作會影響其子節點),對於任意區間的修改,我們先按照區間查詢的方式將其劃分成線段樹中的節點,然後修改這些節點的資訊,並給這些節點標記上代表這種修改操作的標記。在修改和查詢的時候,如果我們到了一個節點p,並且決定考慮其子節點,那麼我們就要看節點p是否被標記,如果有,就要按照標記修改其子節點的資訊,並且給子節點都標上相同的標記,同時消掉節點p的標記。

核心操作(pushdown):

void pushdown(LL root)  //向下傳遞lazy標記 
{
    if (tree[root].lazy)
    {
        tree[root*2].lazy+=tree[root].lazy;
        tree[root*2+1].lazy+=tree[root].lazy;
        tree[root*2].val+=tree[root*2].len*tree[root].lazy;
        tree[root*2+1].val+=tree[root*2+1].len*tree[root].lazy;
        tree[root].lazy=0; 
    }
}

區間更新:

void update(LL root,LL l,LL r,LL addval)  //區間更新 
{
    if (tree[root].l>=l&&tree[root].r<=r)
    {
        tree[root].lazy+=addval;
        tree[root].val+=tree[root].len*addval;
        return;
    }
    if (tree[root].l>r||tree[root].r<l)
        return;
    if (tree[root].lazy) pushdown(root);   //pushdown操作
    update(root*2,l,r,addval);
    update(root*2+1,l,r,addval);
    tree[root].val=tree[root*2].val+tree[root*2+1].val;
}
  • 求區間和

遞迴求區間和,注意lazy標記的傳遞

LL query(LL root,LL l,LL r)  //計算區間和 
{
    LL mid;
    if (tree[root].l>=l&&tree[root].r<=r)
        return tree[root].val;
    if (tree[root].l>r||tree[root].r<l)
        return 0;
    if (tree[root].lazy) pushdown(root);    //pushdown操作
    return query(root*2,l,r)+query(root*2+1,l,r);
}

區間更新舉例說明:當我們要對區間[0,2]的葉子節點增加2,利用區間查詢的方法從根節點開始找到了非葉子節點[0-2],把它的值設定為1+2 = 3,並且把它的延遲標記設定為2,更新完畢;當我們要查詢區間[0,1]內的最小值時,查詢到區間[0,2]時,發現它的標記不為0,並且還要向下搜尋,因此要把標記向下傳遞,把節點[0-1]的值設定為2+2 = 4,標記設定為2,節點[2-2]的值設定為1+2 = 3,標記設定為2(其實葉子節點的標誌是不起作用的,這裡是為了操作的一致性),然後返回查詢結果:[0-1]節點的值4;當我們再次更新區間[0,1](增加3)時,查詢到節點[0-1],發現它的標記值為2,因此把它的標記值設定為2+3 = 5,節點的值設定為4+3 = 7。

注:

  • 其實當區間更新的區間左右值相等時([i,i]),就相當於單節點更新,單節點更新只是區間更新的特例。
/*不怕比我聰明的人,只怕比我聰明但比我還要努力的人*/
#include<iostream>
#include<cstdio>
#define INF 99999999
using namespace std;
typedef long long LL; 
struct node
{
    LL val;
    LL len;
    LL lazy;
    LL l,r;
}tree[300005];
LL arr[500005];
LL n,m;
void build(LL root,LL l,LL r)  //建樹 
{
    LL mid;
    tree[root].lazy=0;
    tree[root].l=l;tree[root].r=r;
    tree[root].len=r-l+1;
    if (l==r) tree[root].val=arr[l];
    else
    {
        mid=(l+r)/2;
        build(root*2,l,mid);
        build(root*2+1,mid+1,r);
        tree[root].val=tree[root*2].val+tree[root*2+1].val;
    }
}
void pushdown(LL root)  //向下傳遞lazy標記 
{
    if (tree[root].lazy)
    {
        tree[root*2].lazy+=tree[root].lazy;
        tree[root*2+1].lazy+=tree[root].lazy;
        tree[root*2].val+=tree[root*2].len*tree[root].lazy;
        tree[root*2+1].val+=tree[root*2+1].len*tree[root].lazy;
        tree[root].lazy=0; 
    }
}
void add(LL root,LL id,LL addval)  //單點更新 
{
    LL mid;
    if (tree[root].l==tree[root].r)
    {
        tree[root].val+=addval;
        return;
    }
    else
    {
        mid=(tree[root].l+tree[root].r)/2;
        if (id<=mid) add(root*2,id,addval);
        else add(root*2+1,id,addval);
        tree[root].val=tree[root*2].val+tree[root*2+1].val;
    }
}
LL query(LL root,LL l,LL r)  //計算區間和 
{
    LL mid;
    if (tree[root].l>=l&&tree[root].r<=r)
        return tree[root].val;
    if (tree[root].l>r||tree[root].r<l)
        return 0;
    if (tree[root].lazy) pushdown(root);
    return query(root*2,l,r)+query(root*2+1,l,r);
}
void update(LL root,LL l,LL r,LL addval)  //區間更新 
{
    LL mid;
    if (tree[root].l>=l&&tree[root].r<=r)
    {
        tree[root].lazy+=addval;
        tree[root].val+=tree[root].len*addval;
        return;
    }
    if (tree[root].l>r||tree[root].r<l)
        return;
    if (tree[root].lazy) pushdown(root);
    update(root*2,l,r,addval);
    update(root*2+1,l,r,addval);
    tree[root].val=tree[root*2].val+tree[root*2+1].val;
}
int main()
{
    LL i,x,y,z,k;
    scanf("%d%d",&n,&m);
    for (i=1;i<=n;i++)
    {
        scanf("%lld",&arr[i]);
    }
    build(1,1,n);
    for (i=1;i<=m;i++)
    {
        scanf("%lld",&z);
        if (z==1)
        {
            scanf("%lld%lld%lld",&x,&y,&k);
            update(1,x,y,k);
        }
        else 
        {
            scanf("%lld%lld",&x,&y);
            printf("%lld\n",query(1,x,y));
        }
    }
}