线段树超详细笔记3

先看一道必须用到懒标记的线段树水题~

给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:

  1. C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
  2. Q l r,表示询问数列中第 l∼r个数的和。

对于每个询问,输出一个整数表示答案。

输入格式

第一行两个整数 N,M。

第二行 NN 个整数 A[i]。

接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围

1≤N,M≤10^5,
|d|≤10000,
|A[i]|≤10^9

输入样例:

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

前面我们说到:凡是不涉及区间修改的题目是没有必要用到懒标记的,这道题可以用树状数组来做,也就是把区间内都加上一个数的操作改变一下,但是本质上还是单点修改。线段树的功能还是蛮强大的,我们看看怎么用线段树解决这道题

但是为什么区间修改就必须用到懒标记呢?如果我们单纯把单点修改的操作应用多次,我们发现时间复杂度是O(n)的

线段树的研究者从query函数中得到灵感,思考能不能在查询的同时更新子区间的值

这样我们不用专门暴力一个函数去更新每个点,也不需要写个循环把单点修改变成区间修改

因此我们在修改操作中每次仅仅改变根节点的sum值并给根节点加上懒标记,并不去递归改变每个懒标记的值

那么什么时候去掉懒标记并给子节点赋值呢?

其一就是修改操作,可能有人会有疑问:不是应该在查询操作中更新标记吗?

但是实际上我们在进行修改操作(modify)操作的时候也要更新懒标记,这里解释一下:

假设我们第一次在区间s上加上了懒标记,第二次修改时在s的子区间上也加上了懒标记,并且在第二次更新s子区间的时候没有对第一次加上的懒标记做pushdown操作,这样会导致什么结果呢?

我们发现虽然第一次在s区间上加上了懒标记也更新了s的sum值,但是由于没有更新子区间,所以在第二次对s子区间打懒标记后会有一次pushup操作,若是两次操作都是让区间加上一个值,那么显然在用s的子区间做pushup操作来更新s区间的时候只进行了一次更新!这样结果是一定会出错的

其二就是我们在查询操作的时候需要把懒标记pushdown一下,这是很容易想到的

还要注意的一点就是我们在做pushdown的时候是一层层做的:真不愧是懒标记,更新全靠白嫖,懒标记是真的懒我去

 

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m;
int w[N];
struct Node
{
    int l, r;
    LL sum, add;
}tr[N*4];
void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void pushdown(int u)
{
    auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
    if (root.add)
    {
        left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;          //懒标记是一层层传递的 
        right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;      //每次把懒标记向下传递,注意是让左右儿子的懒标记加上父节点的懒标记的值 
        root.add = 0;                                                                    //然后把根节点的懒标记清除 
    }
}
void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[r], 0};
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}
void modify(int u, int l, int r, int d)
{
    if (tr[u].l >= l && tr[u].r <= r)
    {
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;                                 //这只是加标记的过程 
    }
    else    
    {
        pushdown(u);                                    //你在加新的标记的时候能保证其间再没有其他标记吗? 所以要先把之前的标记都给处理了 
        int mid = tr[u].l + tr[u].r >> 1;                
        if (l <= mid) modify(u << 1, l, r, d);          
        if (r > mid) modify(u << 1 | 1, l, r, d);
        pushup(u);
    }
}
LL query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;

    else
    {
		pushdown(u);
   		int mid = tr[u].l + tr[u].r >> 1;
   		LL sum = 0;
   		if (l <= mid) sum = query(u << 1, l, r);
   		if (r > mid) sum += query(u << 1 | 1, l, r);
    	return sum;
	}
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i=1;i<=n;i++) scanf("%d",&w[i]);
    build(1,1,n);
    char op[2];
    int l, r, d;
    while (m -- )
    {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'C')
        {
            scanf("%d", &d);
            modify(1, l, r, d);
        }
        else printf("%lld\n", query(1, l, r));
    }
    return 0;
}

要加油啊我去!!

主函数一定要加build!!!!!!!

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值