本篇博客在上一篇博客基础上讲解,链接:线段树
引例
来看一道题目:A Simple Problem with Integers
这道题的题意是给定一个大小为N的数组,现在每次对数组进行以下两种操作中的一种:
- 如果是Q,输入a,b,则求区间[a,b]的元素和
- 如果是C,输入a,b,c,则对区间[a,b]中的每个元素进行加c操作
首先如果只是区间求和查询操作比较好处理,在构造线段树的时候预先求和即可,但题目关键在于对于一段区间的元素进行更新之后的区间求和。
如果我们每次的更新操作都处理到叶子结点,那么所花费的代价是不可接受的。例如对于一个区间[1,10],每次都对整个区间进行加1操作,然后再求某个区间的和,那么每次的更新操作都相当于一次线段树的构造操作,在O(n)时间内才能处理完,显然极其费时。
如果我们每次的更新操作不处理到叶子结点,具体见下图,第一次更新区间[1,3]内每个元素加4的操作,如果接下来要求的区间[3,3]的和,那么由于没有向下更新,[3,3]区间的和必然为0。

由以上总结,我们需要一种 根据情况进行向下更新的 方法,由此引入lazy-tag处理区间求和的问题。
lazy-tag在区间求和中的应用
lazy-tag顾名思义,就是懒惰标记,这像是上面两种方法的折中:更新只在可能需要的时候更新。
下面贴出代码,来分析一下懒惰标记的应用。
void update(int x, int y, int c, int num)
{
if(T[num].left == x && T[num].right == y)
{
T[num].add += c;
T[num].sum += c * (T[num].right - T[num].left + 1);
return ;
}
if(T[num].add)
{
T[2*num].add += T[num].add;
T[2*num].sum += T[num].add * (T[2*num].right - T[2*num].left + 1);
T[2*num+1].add += T[num].add;
T[2*num+1].sum += T[num].add * (T[2*num+1].right - T[2*num+1].left + 1);
T[num].add = 0;
}
int mid = (T[num].left + T[num].right) / 2;
if(x > mid) update(x, y, c, 2*num+1);
else if(y <= mid) update(x, y, c, 2*num);
else
{
update(x, mid, c, 2*num);
update(mid+1, y, c, 2*num+1);
}
T[num].sum = T[2*num].sum + T[2*num+1].sum;
}
首先讲讲update的参数,[x,y]表示想要更新的区间,c表示对于区间内的所有元素进行加c操作。
我们可以发现,函数里有两个选择语句,如果本次递归中当前的线段树区间和想要更新的区间正好相等,那么更新这个区间的和,并且给这个结点打上懒惰标记;否则说明更新的区间被线段树的区间所包含,因此如果该结点有懒惰标记应该立即往下传,传给它的左右子树,并且更新左右子树的和,同时将该结点的懒惰标记清零。
就拿上面的图片来举例子。如果本次更新区间[1,3]中每次元素都加4,就相当于在[1,3]这个结点打了懒惰标记,那么它起作用的时机就在于下次查询,例如查询区间[3,3]的时候,首先应该检查[1,3]这个结点有没有懒惰标记,如果有应该往下传,这样才能保证查询的结果是正确的。
因此从上面就可以看出 懒惰标记更新只在可能需要的时候更新 的意思。 当前的区间如果有懒惰标记,而查询的区间是当前区间的子区间,那么很可能这次查询需要更新之后才有正确的结果,因此懒惰标记应该立即传给该区间的左右子树。
完整代码
贴出引例中的完整代码,需要注意该题懒惰标记在区间求和的时候会爆int,因此用long long。
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#define maxn 1000010
using namespace std;
struct node
{
int left;
int right;
long long add;
long long sum;
}T[maxn*4];
long long ans;
long long val[maxn];
void build(int left, int right, int num)
{
T[num].left = left;
T[num].right = right;
T[num].add = 0;
if(T[num].left == T[num].right)
{
T[num].sum = val[left];
return;
}
int mid = (left + right) / 2;
build(left, mid, 2*num);
build(mid+1, right, 2*num+1);
T[num].sum = T[2*num].sum + T[2*num+1].sum;
}
void query(int x, int y, int num)
{
if(T[num].left == x && T[num].right == y)
{
ans += T[num].sum;
return ;
}
if(T[num].add)
{
T[2*num].add += T[num].add;
T[2*num].sum += T[num].add * (T[2*num].right - T[2*num].left + 1);
T[2*num+1].add += T[num].add;
T[2*num+1].sum += T[num].add * (T[2*num+1].right - T[2*num+1].left + 1);
T[num].add = 0;
}
int mid = (T[num].left + T[num].right) / 2;
if(x > mid) query(x, y, 2*num+1);
else if(y <= mid) query(x, y, 2*num);
else
{
query(x, mid, 2*num);
query(mid+1, y, 2*num+1);
}
}
void update(int x, int y, int c, int num)
{
if(T[num].left == x && T[num].right == y)
{
T[num].add += c;
T[num].sum += c * (T[num].right - T[num].left + 1);
return ;
}
if(T[num].add)
{
T[2*num].add += T[num].add;
T[2*num].sum += T[num].add * (T[2*num].right - T[2*num].left + 1);
T[2*num+1].add += T[num].add;
T[2*num+1].sum += T[num].add * (T[2*num+1].right - T[2*num+1].left + 1);
T[num].add = 0;
}
int mid = (T[num].left + T[num].right) / 2;
if(x > mid) update(x, y, c, 2*num+1);
else if(y <= mid) update(x, y, c, 2*num);
else
{
update(x, mid, c, 2*num);
update(mid+1, y, c, 2*num+1);
}
T[num].sum = T[2*num].sum + T[2*num+1].sum;
}
int main()
{
int N, Q;
while(~scanf("%d%d", &N, &Q))
{
for(int i = 1; i <= N; i++) scanf("%lld", &val[i]);
build(1, N, 1);
for(int i = 1; i <= Q; i++)
{
char str[5];
int a, b;
scanf("%s%d%d", &str, &a, &b);
if(!strcmp(str, "Q"))
{
ans = 0;
query(a, b, 1);
printf("%lld\n", ans);
}
else
{
int c;
scanf("%d", &c);
update(a, b, c, 1);
}
}
}
return 0;
}
这篇博客探讨了如何使用线段树解决区间求和问题,特别是在区间元素更新后的查询。文章通过一个实例展示了当区间更新操作过于频繁时,直接更新叶子节点会导致效率低下。为了解决这个问题,引入了懒惰标记(lazy-tag)的概念,这是一种在需要时才进行更新的策略,以提高效率。文章详细解释了懒惰标记的工作原理,并给出了具体的C++实现代码,包括区间更新和查询的函数。最后,通过示例演示了懒惰标记如何确保正确性和效率。
882

被折叠的 条评论
为什么被折叠?



