线段树进阶之延迟标记 (~详细整理)区间修改
线段树进阶之延迟标记 (~详细整理)区间修改
一、为什么需要添加延迟标记?
引入:我们如果要对某一区间的元素都进行更改,即是区间修改,不是单点修改。那么,对于没加延迟标记的线段树,我们要这样子进行修改:
for(int i=l;i<=r;i++){
UpdateTree(i,value)
}
这要进行r-l+1
此单点修改来实现此区间修改操作,我们仔细想想,如果我们是这样进行的,单点修改的时间复杂度为O(
l
o
g
2
n
log_2n
log2n),则对区间修改的时间复杂度高达O(
n
l
o
g
2
n
nlog_2n
nlog2n),这比最朴素模拟算法都要慢!
这当然是我们不愿看到的,所以我们就会使用一种新方法:延迟标记,也叫懒惰标记(Lazy Tag)。
延迟标记就是在递归的过程中,如果当前区间被需要修改的目标区间完全覆盖,那么就要停止递归,并且在上面做个标记。由于这个信息是没有更新到每个元素的(即叶子结点),我们在下次查询时就可能无法得到正确的信息。不过,我们不要忘了,我们在区间打了一个标记,这个标记不仅仅是这个结点的性质,此性质还作用域整个子树上。假设我们另一个查询中包含了当前区间的孩子区间,显然,这个标记也要对之后的查询产生作用。
我们来看下面这颗线段树,如果我们要对区间[3,9]的元素进行修改,那么所以绿色的部分都要修改,这显然是很麻烦的,如果线段树的高度大,进行区间修改的操作是非常之多,这样显然是熬不过的。
我们来看看用了Lazy Tag的效果。这样我们只要修改橙色的部分,并在完全覆盖的地方停止递归,打上了Lazy Tag。
你可能会问:可这些就是要修改的呀?你明明就是没有修改!
因为我懒呀!如果老板没有叫我做什么的话,我只要修改表面的就行,老板看不出的,更何况我打上了标记,我知道我以后要怎样做,如果老板让我查询的话,我判断该区间有没有标记即可,(修改完之后也一定要去掉标记哦!)当然,也是一样,我只要更改我查询过的,没查询的标记还是不去掉,因为我完成了老板的要求,结果也是正确的,我也省了力,何乐而不为呢?要记住,因为懒,想要省力才会有进步。
那么经过延迟标记优化后的线段树,它的时间复杂度是多少呢?简单的说,延迟标记在我们需要的时候,才会向下传递信息,如果没有用到,则不进行操作。这个思想使处理的时间复杂度依然保持和单点修改的时间复杂度一样,大概在O( l o g 2 n log_2 n log2n)左右,这大大降低了时间复杂度。
为了完成这种操作,我们可以在结构体数组中,增加一个lazy数组存储区间的延迟变化量。
这里我们也要用到一个函数来使用lazy标记:(rt表示当前子树的根),也就是当前所在结点。
- PushDown(rt):就是解决标记的函数,也就是这里我们要用到了,要清楚标记使得结果正确,则把自己的标记归零,并给自己的儿子作上标记(备孙子使用),然后让自己的儿子加上 ( l − r + 1 ) ∗ l a z y (l-r+1)*lazy (l−r+1)∗lazy。注意这个函数,它是在我们需要查询某个结点的子树时需要用到,也就是这一部分老板要检查了,我这里还有标记(表示事情没做完),则这次就做完,当然不是全部做完,是做表面要用到的东西,也就是老板要检查的东西。
这个函数就是核心了,不断下移标记,如果理解了这个函数和lazy标记的话,我们就可以解决区间修改的问题了,我们以区间求和为例。
二、区间修改具体实现
-
建树BuildTree
建树和普通线段树是没有什么区别的,仔细理解这个建树过程。
const int maxn=1e5;
typedef long long ll;
struct node{
ll left,right; //左右端点。
ll sum;//区间[left,right]的和
ll lazy;//lazy标记。
}SegTree[maxn<<2];
void BuildTree(int rt,int l,int r){
SegTree[rt].left=l,SegTree[rt].right=r;
SegTree[rt].lazy=0;
if(l==r){
cin>>SegTree[rt].sum;//赋值。
return;
}
BuildTree(rt<<1,l,(l+r)>>1); //递归建立左子树
BuildTree(rt<<1|1,((l+r)>>1)+1,r);//递归建立右子树。
SegTree[rt].sum=SegTree[rt<<1].sum+SegTree[rt<<1|1].sum;
}
- Pushdown函数(lazy标记下移)
void PushDown(int rt){
//rt代表要下移的父结点。
if(SegTree[rt].lazy){
//如果是有标记的。
SegTree[rt<<1].lazy+=SegTree[rt].lazy;//标记下移给左孩子。
SegTree[rt<<1|1].lazy+=SegTree[rt].lazy;//标记下移给右孩子。
//左右孩子将欠下的给补上。
SegTree[rt<<1].sum+=(SegTree[rt<<1].r-SegTree[rt<<1].l+1)*SegTree[rt].lazy;
SegTree[rt<<1|1].sum+=(SegTree[rt<<1|1].r-SegTree[rt<<1|1].l+1)*SegTree[rt].lazy;
SegTree[rt].lazy=0;//把欠的给清除。
}
}
- Update更新函数。
void UpDate(int rt,int c,int l,int r){
//对区间修改的函数也同样可以对单点进行修改。
if(SegTree[rt].left==l&&SegTree[rt].right==r){
SegTree[rt].lazy+=c; //记下标记,
SegTree[rt].sum+=(SegTree[rt].right-SegTree[rt].left+1)*c;
return; //停止递归。
}
if(SegTree[rt].left==SegTree[rt].right){
//到了叶子结点,不能往下了,也返回。
return;
}
PushDown(rt);//到了这步发现区间没有全覆盖我们自然要先标记下移,再去寻找左右孩子
int mid=(SegTree[rt].left+SegTree[rt].right)/2;
if(r<=mid){
//更新区间全部在左孩子。
UpDate(rt<<1,c,l,r);
}
else if(l>mid){
//更新区间全部在右孩子。
UpDate(rt<<1|1,c,l,r);
}
else{
//否则左右区间都有。
UpDate(rt<<1,c,l,mid); //更新左孩子
UpDate(rt<<1|1,c,mid+1,r); //更新右孩子。
}
SegTree[rt].sum=SegTree[rt<<1].sum+SegTree[rt<<1|1].sum;
}
- QueryTree函数(区间查询)
ll QueryTree(int rt,int l,int r){
//区间查询,对[l,r]区间查询
if(SegTree[rt].left==l&&SegTree[rt].right==r){
return SegTree[rt].sum;
}
PushDown(rt);
int mid=(SegTree[rt].right+SegTree[rt].left)>>1;
ll ans=0;
if(r<=mid){
//说明全部在左孩子
ans+=QueryTree(rt<<1,l,r);
}
else if(l>mid){
//说明全部在右孩子
ans+=QueryTree(rt<<1|1,l,r);
}
else{
ans+=QueryTree(rt<<1,l,mid);
ans+=QueryTree(rt<<1|1,mid+1,r);
}
return ans;
}
OK,我们所有的函数都写完了,我们来看看主函数部分和测试结果。
- 主函数
int main(){
//freopen("in.txt", "r", stdin);//提交的时候要注释掉
IOS;
int n,m;
while(cin>>n>>m){
BuildTree(1,1,n);
while(m--){
string op;
int a,b,c;
cin>>op;
if(op=="Q"){
cin>>a>>b;
cout<<QueryTree(1,a,b)<<endl;
}
else{
cin>>a>>b>>c;
UpDate(1,c,a,b);
}
}
}
return 0;
}
测试用例:
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
测试结果:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!