线段树学习笔记(单点更新+区间查询最大值+lazy标记+pushdown操作+区间更新+求区间和)

目录


  • 什么是线段树?

  • 线段树基本操作:

  • 创建线段树

  • 线段树单点更新

  • 区间查询最大最小值

  • 延迟标记(懒人标记)+pushdown操作

  • 区间更新

  • 求区间和

注:以下所有代码都是针对维护区间和的。


  • 什么是线段树?

    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
    性质:对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

  • 举个例子

线段树学习笔记(单点更新+区间查询最大值+lazy标记+pushdown操作+区间更新+求区间和)_第1张图片

  • 创建一个线段树

线段树成员:

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 (midreturn 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].rreturn;
    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].rreturn 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
#include
#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].rreturn 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].rreturn;
    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));
        }
    }
}

你可能感兴趣的:(总结,线段树)