[模板] - 线段树 - Lazy标记 - 单点/区间更新 - 模板

线段树 - Lazy标记 - 单点/区间更新

目录:

  1. 前言
  2. 在这篇文章的代码中用到的宏定义
  3. Lazy标记
  4. 区间更新
  5. 单点更新
  6. 模板
  7. 例题

1. 前言:

  线段树我花了整整两天的时间去啃,进度很慢,但终究还是坚持下来了,在涉及到Lazy标记的部分卡了很久,刚开始看了一大堆理论,发现很晦涩,也看不懂,后来结合代码一点一点的慢慢就看懂了。


2. 在这篇文章的代码中用到的宏定义:

  PS:注意宏定义部分,以免对理解代码产生阻力
#define il inline
#define ull unsigned long long
#define ll long long
#define ls (u<<1)
//等价于u*2
#define rs (u<<1|1)
//等价于u*2+1

3. Lazy标记

  Lazy标记也称延迟标记,通常和pushdown()操作配合使用,我喜欢讲这两者合称为Lazy操作。

  Lazy操作一般用于线段树的区间更新。因为区间更新涉及到的叶子节点不止一个,而叶子节点会影响到其它的非叶父子节点,那么回溯需要更新的非叶子节点就会非常多,时间复杂度肯定远大于O(lgn),为此,线段树引入了延迟标记(Lazy)的概念,也是线段树之所以这么快的精华所在。

  延迟标记:每有一个节点新增加一个标记,说明这个节点所对应的区间被更新过,但是这个节点的左右子节点却没有被更新过。如果我们在操作过程中要涉及到这个节点的子节点,而这个节点又存在Lazy标记,那么我们就必须调用pushdown()函数来清除这个节点的Lazy标记,否则会导致结果出错。

  通俗来讲,比如说操作:把区间[3,6]中的每一个数都加上3,而在实现过程中我们发现某个节点u它所覆盖的区间刚好就是[3,6],常规线段树的操作是:给这个节点添加一个标记Lazy标记(laz[u] += 3),然后再更新这个节点的值(node[u] += 3 * (6 - 3 + 1)),然后直接return。

  如果没有Lazy标记会发生什么事呢,在更新完这个节点的值(node[u] += 3 * (6 - 3 + 1))之后,我们开始递归调用update()函数分别进入它的左子树,右子树,左子树的左子树,右子树,右子树的左子树,右子树,如此往复循环,直到更新到叶子节点,然后再一层一层的回溯。

  可想而知,在数据量很大的时候这将会是怎样的一种操作量,个人来说感觉效率还不如线性数组要高。

  所以,Lazy标记被称为线段树的精髓所在。


4. 区间更新:

  在大部分的区间更新操作中,都会涉及到区间的长度,比如说把区间[2,9]中的每一个数都加上3,结合Lazy标记,我们只需要把区间的权值权值加上3乘以这个区间的长度就可以,因为区间的长度代表着这个区间包含有几个元素。


5. 单点更新:

  单点更新其实就是区间更新的特殊情况,只要在调用区间更新函数的时候让左节点等于右节点就可以。

  显然,单点查询和区间查询的关系也是如此。


6. 模板(结构体版本+数组版本)

结构体版本:

  • 一些“前置代码”:
const int maxn = 200000+7;
ll n,m,ans;

struct node {//线段树成员
    ll l,r,sum,add;//左端点,右端点,权,lazy标记
}tree[maxn<<2];

il void pushup(ll u) {//向上回溯更新
    tree[u].sum = tree[ls].sum + tree[rs].sum;
}

il void pushdown(ll u) {//去除lazy标记并向下更新
    tree[ls].add += tree[u].add;
    tree[rs].add += tree[u].add;
    tree[ls].sum += tree[u].add*(tree[ls].r-tree[ls].l+1);
    tree[rs].sum += tree[u].add*(tree[rs].r-tree[rs].l+1);
    tree[u].add = 0;
}
  • 建树:
il void build(ll u, ll l, ll r) {
    tree[u].l = l;
    tree[u].r = r;
    tree[u].add = 0;
    if(l==r) {
        scanf("%lld",&tree[u].sum);
        return ;
    }
    ll mid = (l+r) >> 1;
    //递归建树
    build(ls,l,mid);//左子树
    build(rs,mid+1,r);//右子树
    pushup(u);
    //已经更新完这个节点的左右儿子了,最后再更新这个节点本身
}
  • 区间更新:
il void update(ll u, ll l, ll r, ll num) {
    if(rtree[u].r) return ;
    //如果这个节点的区间里根本没有需要更新的区间就直接return
    if(l <= tree[u].l && r >= tree[u].r) {
    //如果当前区间完全包含在需要更新的区间里
        tree[u].add += num;
        //添加lazy标记
        tree[u].sum += num * (tree[u].r-tree[u].l+1);
        //更新当前节点的权值,因为是给区间里每一个数都增加num
        //所以这个节点权值的总增量是num乘以区间长度
        //也就是num*(r-l+1)
        return ;
    }
    if(tree[u].add) pushdown(u);//如果当前区间含有lazy标记
    update(ls,l,r,num);//更新左子树
    update(rs,l,r,num);//更新右子树
    pushup(u);
    //更新完左右子树,向上回溯
}
  • 单点更新:
il void oneupdate(ll u, ll x, ll num) {
    update(u,x,x,num);
    //让区间更新的左右端点相等即可,没有验证过这样写的效率
}
  • 查询区间和:
il void query(ll u, ll l, ll r) {
    if(r < tree[u].l || l > tree[u].r) return ;
    //如果这个节点的区间里根本没有需要更新的区间就直接return
    if(l <= tree[u].l && r >= tree[u].r) {
        //如果当前区间完全包含在需要更新的区间里
        ans += tree[u].sum;
        return ;
    }
    if(tree[u].add) pushdown(u);
    //如果当前区间存在lazy标记,更新
    ll mid = (tree[u].l+tree[u].r)>>1;
    if(r<=mid) query(ls,l,r);
    //如果查询区间完全在左子树
    else if(l>mid) query(rs,l,r);
    //如果查询区间完全在右子树
    else {//如果查询区间跨越了mid
        query(ls,l,mid);
        query(rs,mid+1,r);
    }
}

数组版本

  PS: 数组版本因为没有直接存储每个节点的左右区间,所以需要将根节点u,左右节点lr作为参数放入每一个线段树操作函数中。显然,可以通过和数组版本的线段树操作对比得到这个结论。
  此外,因为比较好敲的原因,在数组版本中我略去了il void pushup(ll u)函数,直接将其写成了node[u] = node[ls] + node[rs];
  因为结构类似,数组版本的代码我略去了很多注释,看不懂的地方参照结构体版本的注释。(其实是我懒)
  • 一些“前置代码”:
const int maxn = 200000+7;
ll n,m,ans;

ll node[maxn<<2],laz[maxn<<2];//node是树节点,laz是lazy标记

il void pushdown(ll u, ll l, ll r) {
    ll mid = (l+r)>>1;
    laz[ls] += laz[u];
    laz[rs] += laz[u];
    node[ls] += laz[u]*(mid-l+1);
    node[rs] += laz[u]*(r-mid);
    laz[u] = 0;
}
  • 建树:
il void build(ll u, ll l, ll r) {
    if(l==r) {
        scanf("%lld",node+u);
        return ;
    }
    ll mid = (l+r)>>1;
    build(ls,l,mid);
    build(rs,mid+1,r);
    node[u] = node[ls] + node[rs];
}
  • 区间更新:
il void update(ll u, ll l, ll r, ll b, ll e, ll num) {
    if(er) return ;
    if(b<=l && r<=e) {
        laz[u] += num;
        node[u] += num * (r-l+1);
        return ;
    }
    if(laz[u]) pushdown(u,l,r);
    ll mid = (l+r)>>1;
    update(ls,l,mid,b,e,num);
    update(rs,mid+1,r,b,e,num);
    node[u] = node[ls] + node[rs];
}
  • 单点更新:
/*略,参考数组版本*/
  • 区间查询:
il void query(ll u, ll l, ll r, ll b, ll e) {
    if(er) return ;
    if(b<=l && r<=e) {
        ans += node[u];
        return ;
    }
    if(laz[u]) pushdown(u,l,r);
    ll mid = (l+r)>>1;
    if(e<=mid) query(ls,l,mid,b,e);
    else if(b>mid) query(rs,mid+1,r,b,e);
    else {
        query(ls,l,mid,b,e);
        query(rs,mid+1,r,b,e);
    }
}

7. 例题:

  一个裸线段树区间查询/更新的题,题意很简单,代码也比较简单。需要注意的点就是不用long long的话会爆。

  传送门:A Simple Problem with Integers POJ - 3468

  题解:A Simple Problem with Integers - POJ 3468 - 线段树 区间更新

你可能感兴趣的:(数据结构-线段树,模板)