线段树-懒标记详解

首先,我们可以想到用线段树来做这道题。不会告诉你其实我不会树状数组

数据太大就会超时,怎么办?这样就要引出我们的好帮手懒标记了!

一个支持区间修改的线段树, 就要用到lazy标记. 用到哪一个结点, 有效数据就更新到哪一个结点, 避免浪费更新那些不必要的结点的时间. 从根部向下找一个结点, 一路下来根据lazy的设定进行相关的更新操作, 就能快速完成任务.

下面就示范一组样例:

8 2

3 3 3 3 3 3 3 3

1 2 7 1

2 3

相信大家对线段树已经有了一定的了解(没了解过的看这里洛谷日报4)那我们就开始构造线段树,如图:
在这里插入图片描述

这就是基本的线段树了,然后开始进行第一步,从2到7,一次给区间加上1:

在这里插入图片描述

来解释这一幅图,首先看我们存线段树的下标1,L(左节点)=1,R(右节点)=8,没有被包含且懒标记=0,所以取中点mid=(1+8)/2=4,查找左右子树。

来到左子树2,L=L(father’s),R=mid(father’s),没有被完全包含且懒标记=0,所以继续查找左右子树。

来到2的左子树4,没有被包含且懒标记=0,所以查找左右子树,因为左子树的右节点大于2,所以不进入。来到右子树9,被包含,所以dis[9]+=1,懒标记+=1,返回。

返回到2,查找右子树5,被包含,所以dis[5]+=(4-3+1)* 1,懒标记+=1,返回。

回到1,查找右节点3……以此类推,dis[6]+=(6-5+1)* 1,懒标记+=1;dis[14]+=1,懒标记+=1;

这样,区间修改就完成了。

来到第二步,查找3:
在这里插入图片描述

从1开始找到2,在找到5,发现有懒标记,所以把懒标记下传到10和11, dis[10]+=1;懒标记[10]+=1;dis[11]+=1,懒标记[11]+=1,懒标记[5]=0; 到10,L=R=3,return dis[10];查询就完成了。

可能有人会说懒标记看起来没有什么用,但数据一旦很大,递归传递就显得尤为方便,可以为我们节省很多时间。

接下来是对代码的详细讲解:

初始化:

struct Tree{
    long long l,r;//l:左节点 r:右节点 
    long long dat,laze_tag;//dat:当前节点的值 laze_tag:懒标记,记录改变的值,递归传值 
}t[2000001];//四倍n 

然后是我们的懒标记传递:


inline void f(long long p,long long k){
    t[p].laze_tag+=k;//懒标记传递 
    t[p].dat+=k*(t[p].r-t[p].l+1);//当前值加上所有节点总数*值 
} 
inline void pushdown(long long p){
    f(p*2,t[p].laze_tag);
    f(p*2+1,t[p].laze_tag);
    //将懒标记的值传给下面的左右儿子节点
    t[p].laze_tag=0;//复原懒标记 
}

(下面会有再查找和修改时的懒标记处理)

开始先要构造我们的线段树

void js(int p,int l,int r){//建树 
    t[p].l=l;t[p].r=r;//记录左右节点 
    if(l==r){//到达底部返回值 
        t[p].dat=a[l];return;
    }
    long long mid=(l+r)/2;//中点 
    js(p*2,l,mid);
    js(p*2+1,mid+1,r);
    //递归初始化 
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
    //加上左右儿子节点 
}

第二步,就要写我们的区间加减了

void pushs(long long p,long long l,long long r,long long v){//区间加减 
    if(t[p].l>=l&&t[p].r<=r){//如果区间被包含就修改并打上懒标记 
        t[p].dat+=v*(t[p].r-t[p].l+1);//加上所有值 
        t[p].laze_tag+=v;//懒标记修改 
        return;
    }
    pushdown(p);//查询懒标记,因为下面要递归 
    long long mid=(t[p].r+t[p].l)/2;//取中点 
    if(l<=mid)pushs(p*2,l,r,v);//修改左边 
    if(r>mid) pushs(p*2+1,l,r,v);//修改右边 
    t[p].dat=t[p*2].dat+t[p*2+1].dat;//回溯时加上左右儿子节点的值 
}

最后是最容易的单点查询


long long outt(long long p,long long l){//单点查询 

    if(t[p].l==l&&t[p].r==l)return t[p].dat;//找到目标点就返回 
    pushdown(p);//先回复懒标记的值再传递,因为下面可能递归(要判断是否到了底部,就是这里出了问题QwQ)
    long long mid=(t[p].l+t[p].r)/2;//记录中点 
    if(l<=mid)return outt(p*2,l);//找左边 
    if(l>mid)return outt(p*2+1,l);//找右边 
}

看完了,那就贴上完整的代码啦!

#include<bits/stdc++.h>
using namespace std;
long long n,m;//n:长度 m: 询问 
long long a[500001];
struct Tree{
    long long l,r;//l:左节点 r:右节点 
    long long dat,laze_tag;
}t[2000001];
inline long long read(){
    long long f=1,outt=0;char a=getchar();
    while(a>'9'||a<'0'){if(a=='-')f=-1;a=getchar();}
    while(a<='9'&&a>='0'){outt*=10;outt+=a-'0';a=getchar();}
    return f*outt;
}//读入优化 
inline void f(long long p,long long k){
    t[p].laze_tag+=k;
    t[p].dat+=k*(t[p].r-t[p].l+1);
} 
inline void pushdown(long long p){
    f(p*2,t[p].laze_tag);
    f(p*2+1,t[p].laze_tag);
    t[p].laze_tag=0;
}
void js(int p,int l,int r){
    t[p].l=l;t[p].r=r;
    if(l==r){
        t[p].dat=a[l];return;
    }
    long long mid=(l+r)/2;//中点 
    js(p*2,l,mid);
    js(p*2+1,mid+1,r);
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
}
long long outt(long long p,long long l){
    if(t[p].l==l&&t[p].r==l)return t[p].dat;
    pushdown(p);
    long long mid=(t[p].l+t[p].r)/2;
    if(l<=mid)return outt(p*2,l);
    if(l>mid)return outt(p*2+1,l);
}
void pushs(long long p,long long l,long long r,long long v){
    if(t[p].l>=l&&t[p].r<=r){
        t[p].dat+=v*(t[p].r-t[p].l+1);
        t[p].laze_tag+=v;
        return;
    }
    pushdown(p);
    long long mid=(t[p].r+t[p].l)/2;
    if(l<=mid)pushs(p*2,l,r,v); 
    if(r>mid) pushs(p*2+1,l,r,v);
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
}
void change(long long p,int x,int v){//单点修改,不必在意,是区间修改的子问题,连标记都不用(而且本题不需要)
    if(t[p].l==t[p].r){
        t[p].dat+=v;return;
    }
    int mid=(t[p].r+t[p].l)/2;
    if(x<=mid)change(p*2,x,v);
    else change(p*2+1,x,v);
    t[p].dat=t[p*2].dat+t[p*2+1].dat;
}
int main(){
    n=read();m=read();//读入 
    for(int i=1;i<=n;i++)
        a[i]=read();
    js(1,1,n);//建树 
    for(int i=1;i<=m;i++){
        long long pd=read();
        if(pd==2){
            long long ll=read();
            printf("%lld\n",outt(1,ll));//查询ll的值 
        }
        else 
        if(pd==1){
            long long ll=read(),rr=read(),x=read();
            pushs(1,ll,rr,x);//修改从ll到rr的值加上x 
        }
        else
        if(pd==3){
            int k=read(),y=read();
            change(1,k,y);
        }
    }
    return 0;//华丽丽的结束,可以A掉树状数组2了!!! 
}
### 权值线段树中的标记概念 标记(Lazy Tag)是一种用于优化区间操作的技术,广泛应用于线段树的数据结构中。其核心思想是在执行区间更新时,不立即逐个修改区间的每一个元素,而是在父节点上记录该次更新的信息,并推迟到真正需要访问这些子节点时才进行实际的修改[^3]。 #### 标记的作用 当需要对某个区间的所有元素进行相同的修改操作(如加法、乘法或其他统一的操作),如果直接遍历并逐一修改每个元素,则时间复杂度可能达到 \(O(n)\),这在大规模数据下效率较低。通过引入标记机制,可以在常数时间内完成一次区间更新操作,从而显著提高性能[^4]。 #### 标记的具体实现 以下是标记线段树中的基本实现方式: 1. **增加辅助数组 `lazy[]`** 定义一个额外的数组 `lazy[]`,用来存储尚未传递给子节点的延迟更新信息。初始状态下,所有 `lazy[i]` 的值均为默认值(通常为0表示无待处理更新)。 2. **构建线段树** 构建过程与普通的线段树相同,但在初始化阶段需确保所有的标记都设置为空状态。 3. **查询前先下传标记** 在每次向下递归之前,检查当前节点是否有未处理的标记。如果有,则将其影响施加于两个子节点对应的区间范围,并清除当前节点上的标记。 4. **更新操作** 当需要对某一段区间 `[L,R]` 进行批量更新时,找到覆盖此区间的最小数量的连续节点集合,在这些节点处打上相应的标记即可。 下面是一个简单的 Python 实现例子展示如何使用标记来支持快速区间求和以及单点查询功能: ```python class LazySegmentTree: def __init__(self, n): self.n = n self.tree = [0] * (4*n) self.lazy = [0] * (4*n) def push_down(self, rt, ln, rn): if self.lazy[rt]: self.tree[rt*2] += self.lazy[rt]*ln self.tree[rt*2+1] += self.lazy[rt]*rn self.lazy[rt*2] += self.lazy[rt] self.lazy[rt*2+1] += self.lazy[rt] self.lazy[rt] = 0 def update_range(self, l, r, val, L=0, R=None, rt=1): if R is None: R=self.n-1 if l<=L and R<=r: self.tree[rt] += (R-L+1)*val self.lazy[rt] += val return mid = (L+R)//2 self.push_down(rt, mid-L+1, R-mid) if l <= mid: self.update_range(l, r, val, L=L, R=mid, rt=rt<<1) if r > mid: self.update_range(l, r, val, L=mid+1, R=R, rt=(rt<<1)|1) self.tree[rt] = self.tree[rt*2]+self.tree[rt*2+1] def query_sum(self, l, r, L=0, R=None, rt=1): if R is None: R=self.n-1 if l<=L and R<=r: return self.tree[rt] mid = (L+R)//2 self.push_down(rt, mid-L+1, R-mid) res = 0 if l <= mid: res += self.query_sum(l, r, L=L, R=mid, rt=rt<<1) if r > mid: res += self.query_sum(l, r, L=mid+1, R=R, rt=(rt<<1)|1) return res ``` 上述代码展示了基于标记技术的一个典型应用场景——动态维护序列的部分和问题解决方案之一部分。 #### 总结 标记极大地提升了线段树应对大量重复性区域变更请求的能力,使得原本耗时巨大的任务得以高效完成。这种技巧不仅限于数值型运算场景,在字符串匹配等领域也有着广泛应用前景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值