线段树 + 懒标记: 一个简单的整数问题2

先说懒标记的具体含义:

 给以当前节点为根的子树中的所有节点都加上懒标计的值。(不包括根节点自己,当然也可以包括根节点自己)

当进行区间修改的时候,线段树为什么需要加懒标记呢?

当进行区间修改的时候,最坏的情况就是每一个点都需要修改,导致线段树中所有的节点都被迫修改,这样的话,时间复杂度就为O(N)的情况,因为我们的N开了4倍n。遍历到了所有的节点。这个时间复杂度是无法接受的。

所以我们就需要加入懒标记(借鉴线段树的查询操作):

懒标记的思想主要是借鉴了线段树的查询操作,在线段树的查询操作中,我们要查询l~r的区间的值,当我们遍历到了一个区间的时候,此区间的.l 和 此区间.r都在我们需要查询的l~r区间内的时候,可以将我们需要的值直接返回。

而懒标记在进行区间修改的时候就是借鉴了这个思想。

当修改区间L~R的时候,如果遍历到的区间.L,区间.R在 L~R范围内,则先将这个区间的属性值进行修改,并加上一个懒标记。但是不会在继续往下遍历而是直接返回。

比如:给某一段区间加c. 现在有一个区间1~10,给3~5加2

而给3~5加2,

则会使得3~3的区间.add(懒标记)加v, .sum + v

使得4~5的区间.add(懒标记) 加v , .sum + 2 * v.但是4~5的区间不会在分裂往下,而是直接返回。这就是懒标记的主要思想。使得区间修改时所处理的节点个数最多是 4*logn

修改操作使用到了懒标记,那么怎么进行查询呢?以及修改操作具体怎么操作呢?

那么如果我们要找3~4的值的时候怎么。

虽然3的sum值修改了没有问题,但是4的sum值并没有修改,只修改了4~5的sum值。

所以4需要加上父结点4~5(实际上是所有对应的父辈爷爷辈上)的懒标记的值。(也就是查询某个区间,就要加这个区间即这个区间的所有父辈即爷爷辈等的懒标记的值)。

这个应该怎么操作呢?

实际上这个操作有一个常用的方式,就是当我们进行查询操作的时候,需要进行区间分裂(也就是遍历左边或者遍历右边的时候),这个时候我们将懒标记的值往下传给左右两个区间(切记是左右两个区间都传),并将懒标记的值按照对应的题目要求修改属性值,然后把自己节点的懒标记清空为0.(这个操作实际上就是pushdown操作)。因为这个节点的属性是准确的,往下传了以后,那么左右子树的属性也是准确的。(实际上含义也就是懒标记的值只影响儿子节点以及孙子节点,不影响自己当前的节点。)

那么修改操作呢?

实际上修改操作也是一样的,就是当要进行区间分裂的时候,那么就进行一次pushdown操作。(这个时候我们将懒标记的值往下传给左右两个区间(切记是左右两个区间都传),并将懒标记的值按照对应的题目要求修改属性值,然后把自己节点的懒标记清空为0.)

而如果不需要区间分裂,则直接修改当前区间的属性值和懒标记值即可。

题目链接:243. 一个简单的整数问题2 - AcWing题库

题目:

给定一个长度为 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≤1e5
|d|≤10000,
|A[i]|≤1e9

输入样例:

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

分析:

线段树对应结构体:

struct Node

{

  int l,r;

long long sum; //总和,要求的属性

long long add; //懒标记

// 此题会爆int,所以用long long.

}

然后就是常规的操作:

pushup():   root.sum = left-son.sum + right-son.sum

build(): 常规操作一样, 赋值l,r. 如果为叶子节点,赋值sum.然后返回

如果不是叶子节点,int mid = ( l +r ) / 2,  然后build(l,mid), build(mid + 1 , r),  pushup()l

注意:pushdown()操作 :

由父结点更新子节点:

if(root.add)  //如果存在懒标记值的话

{

        修改左子树懒标记值

        左子树通过懒标记值 修改属性值

        修改右子树懒标记值

        右子树通过懒标记值 修改属性值

        最后父结点懒标记值清空

}

modify()操作进行了修改:

如果查询的l,r区间 包含 当前区间的.l,.r那么直接进行修改

否则需要分裂

则先pushdown()

然后看遍历左子树还是右子树,遍历完了后

pushup();

同时你会发现,区间修改和单点修改的时候的一个差别在modify的中就是

单调修改判断 x 在左区间 或者在右区间 if()  else()

区间修改判断l~r 是否在左区间,是否也在右区间 if() , if()

query()操作进行了修改:

如果查询的l,r区间 包含 当前区间的.l.r那么直接返回值

否则需要分裂

则先pushdown

看是否需要遍历左子树,是否需要遍历右子树

返回值。

代码实现:

# include <iostream>
using namespace std;

const int N = 1e5 + 10;
 
int n,m;

int a[N];

struct Node
{
    int l,r;
    long long sum; // 需要求的总和
    long long add; // 懒标记
}edgs[N * 4];

void pushdown(int u) // 主要就是将父结点的标记传给子节点,同时改变子节点的属性值,毕竟父结点的标记会被清为0,则子节点需要修改,毕竟父结点原标记值除了会影响子节点外,还会影响孙节点
{
    auto & root = edgs[u];
    auto & left_son = edgs[2 * u];
    auto & right_son = edgs[2 * u + 1];
    
    if(root.add)
    {
        left_son.add = (long long)root.add + left_son.add;
        left_son.sum =left_son.sum + (long long)( left_son.r - left_son.l + 1) * root.add;
        right_son.add =(long long)root.add + right_son.add;
        right_son.sum =right_son.sum + (long long)(right_son.r - right_son.l + 1) * root.add;
        
        root.add = 0;
    }
}

void pushup(int u)
{
    edgs[u].sum = edgs[2 * u].sum + edgs[2 * u + 1].sum;
}

void build(int u , int l , int r)
{
    edgs[u].l = l;
    edgs[u].r = r;
    edgs[u].add = 0; // 因为刚开始建树的时候不需要进行标记。所以赋值0代表没有标记它
    // 按照模板操作来看,猜测,build的时候,实际上懒标记的时候会初始化为初始值。比如https://www.luogu.com.cn/problem/P2023 这个题目就要将某个懒标记的值初始化为1而非0.根据未操作需要的初始值方式进行
    if(l == r)
    {
        edgs[u].sum = a[l];
        return;
    }
    else
    {
        int mid = ( edgs[u].l + edgs[u].r ) / 2;
        build(2 * u , l , mid);
        build(2 * u + 1 , mid + 1 , r);
        pushup(u);
    }
    // pushup(u); // 如果写在这的话,并且前面的if没有return 的话是错误的。
}

void modify(int u , int l ,int r ,int v)
{
    if(edgs[u].l >= l && edgs[u].r <= r)
    {
        edgs[u].sum += (long long)(edgs[u].r - edgs[u].l + 1) * v; // 最需要注意的是这个地方,由于是区间修改,所以对应的sum值修改的是这一个区间的总值,而不是单点修改时的一个值。容易犯错。写成单点修改一个值的情况,也就是 edgs[u].sum += v而产生错误。

        edgs[u].add += v;
        return;
    }
    else // 说明不在同一个区域,需要进行分裂,而分裂一般而言第一步都需要进行pushdown
    {
        pushdown(u);
        int mid = ( edgs[u].l + edgs[u].r ) /2;
        if(l <= mid)  
        {
            modify(2 * u , l , r , v);
        }
        if(r > mid) // 区间修改与单调修改的一个差别,单点修改为if..else .  区间修改为if...if
        {
            modify(2 * u + 1 , l , r , v);
        }
        pushup(u); // 注意,pushup()只能在else中写
    }
    // pushup(u); // 如果写在这的话,并且前面的if没有return 的话是错误的。
}

long long query(int u , int l , int r)
{
    if(l <= edgs[u].l && r >= edgs[u].r)
    {
        return edgs[u].sum;
    }
    
    pushdown(u);
    int mid = ( edgs[u].l + edgs[u].r ) / 2;
    long long res = 0;
    if(l <= mid)
    {
        res = query(2 * u , l , r);
    }
    if(r > mid)
    {
        res += query(2 * u + 1 , l , r);
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[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;
}

一个与此题基本相同的题目: 3468 -- A Simple Problem with Integers

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值