先说懒标记的具体含义:
给以当前节点为根的子树中的所有节点都加上懒标计的值。(不包括根节点自己,当然也可以包括根节点自己)
当进行区间修改的时候,线段树为什么需要加懒标记呢?
当进行区间修改的时候,最坏的情况就是每一个点都需要修改,导致线段树中所有的节点都被迫修改,这样的话,时间复杂度就为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 条指令,每条指令可能是以下两种之一:
C l r d
,表示把 A[l],A[l+1],…,A[r] 都加上 d。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;
}