算法介绍
在线段树的 “ask
区间查询” 指令中,每当遇到被询问区间[L,r]
完全覆盖的节点 时,可以立即把 该节点上存储的信息 作为候选答案 返回。可以证明,被询问区间[L,r]
在线段树上会被分成O(log N)
个小区间(节点),从而在 O(log N)
的时间内求出答案。
不过,在 “modify
区间修改” 指令中,如果某个节点被修改区间[L,r]
完全覆盖,那么 以 该节点为根 的 整棵子树 中的 所有节点存储的信息 都会发生 变化,如果 逐一更新,将使得 一次区间修改指令 的 时间复杂度 增加到O(N)
,时间复杂度太高,这是我们不能接受的。
试想,如果我们在 一次修改指令 中发现 节点p
代表的区间[pl, pr]
被 指定modify
的区间[l,r]
完全覆盖,并且 逐一更新 了子树p
中的所有节点,但是在之后的查询指令中却 根本没有用到[l,r]
的子区间作为候选答案,那么更新 p
的整棵子树就是徒劳的。
换言之﹐我们在执行 modify
修改指令 时,同样可以和 ask
查询 一样在l≤pl≤pr≤r
的情况下 立即返回。
如何做到上面这一点呢?
我们可以 在回溯之前向节点 p
增加一个懒标记(延迟标记),标识:“该节点曾经被修改,但其子节点尚未被更新”。(核心定义一定要清楚)
如果在 后续的指令 中,需要 从节点p
向下递归,我们再检查 p
是否具有标记:
-
若有标记,就根据标记信息 更新
p
的两个子节点,同时为p
的两个子节点增加标记,然后清除p
的标记。(一定要记得清除!因为此时该节点的直接左右儿子节点已被更新) -
若无标记,则不作处理。
也就是说,除了在 modify
修改指令 中直接划分成的O(logN)
个节点之外,对 任意节点的修改 都 延迟 到“在后续操作中 递归进入它的父节点时” 再执行。
这样一来,每条 ask
查询 或 modify
修改指令 的时间复杂度都降低到了O(log N)
。
这些标记被称为“懒标记”,也叫作延迟标记。懒标记提供了线段树中 自父向子传递信息 的方式。这种“延迟”是设计算法与解决问题的一个重要思路。
例题 AcWing 243. 一个简单的整数问题2
题意
算法
(线段树、懒标记) 单次操作 O ( l o g n ) O(logn) O(logn)
本题需要解决区间修改、区间查询两个问题,因此如果运用之前的线段树模板是行不通的。
我们之前用过树状数组来解决过这道题,现在,我们改用带懒标记的线段树来求解。
除了保存左右端点l
、r
之外,线段树中的每个节点还保存了sum
区间和、add
懒标记(或者说是增量延迟标记)两个值。
建树、查询和修改的框架 不变,我们利用pushdown
函数实现懒标记的 自父向子 传递。
注意,一个节点的懒标记含义为:该节点曾经被修改过,但其子节点尚未被更新,即 延迟标记标识的是子节点等待更新的情况。
因此,一个节点被打上懒标记的同时,它自身保存的信息应该被修改完毕。
一定要注意“更新信息”与“打标记”之间的关系,要避免出现错误。
关于pushdown
函数的代码片段:
struct node
{
int l, r, sum, add;
} t[N<<2];
void pushdown(int u)
{
auto &root = t[u], &left = t[u<<1], &right = t[u<<1|1];
if(root.add)//节点有标记 才 下传各种信息
{
left.add += root.add;//给左节点打上add懒标记
left.sum += (left.r - left.l + 1) * root.add;//更新左儿子节点的sum信息
right.add += root.add;//给右儿子节点打上add懒标记
right.sum += (right.r - right.l + 1) * root.add;//更新右儿子节点的sum信息
root.add = 0;//记得清除父节点u的add懒标记
}
}
关于modify
和ask
函数的代码要注意的细节:
我们在 “向下分裂” 的时候,都一定要记得使用pushdown
。其中modify
函数中 递归完后进行回溯 时要用pushup
“向上合并”,由于ask
函数中,节点的sum
信息不变,因此回溯时无需pushup
。
总时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn)
参考文献 《算法竞赛进阶指南》
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+10;
int n, m;
int a[N];
struct node
{
int l, r, sum, add;
} t[N<<2];
void pushup(int u){
t[u].sum = t[u<<1].sum + t[u<<1|1].sum;
}
void pushdown(int u)
{
auto &root = t[u], &left = t[u<<1], &right = t[u<<1|1];
if(root.add)
{
left.add += root.add, left.sum += (left.r - left.l + 1) * root.add;
right.add += root.add, right.sum += (right.r - right.l + 1) * root.add;
root.add = 0;
}
}
void build(int u, int l, int r)
{
t[u] = {l, r};
if(l==r) {t[u].sum = a[l], t[u].add = 0; return ;}
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 v)
{
if(t[u].l>=l&&r>=t[u].r)
{
t[u].sum+=(t[u].r - t[u].l + 1) * v;//注意是更新整个区间的元素
t[u].add+=v;
return ;
}
pushdown(u);
int mid = t[u].l+t[u].r>>1;
if(l<=mid) modify(u<<1, l, r, v);
if(r>=mid+1) modify(u<<1|1, l, r, v);
pushup(u);
}
int ask(int u, int l, int r)
{
if(t[u].l>=l&&r>=t[u].r) return t[u].sum;
pushdown(u);
int mid = t[u].l+t[u].r>>1;
int res = 0;//从根节点往下递归查询,树中一旦有能完全包裹的区间则收入res囊中
if(l<=mid) res += ask(u<<1, l, r);
if(r>=mid+1) res += ask(u<<1|1, l, r);
return res;
}
signed main()
{
scanf("%lld%lld", &n, &m);
for(int i=1;i<=n;++i)
{
scanf("%lld", &a[i]);
}
build(1, 1, n);
while(m--)
{
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if(*op=='C')
{
scanf("%lld", &d);
modify(1, l, r, d);
}
else printf("%lld\n", ask(1, l, r));
}
return 0;
}