【例 2】A Simple Problem with Integers(信息学奥赛一本通- P1548)

【题目描述】

这是一道模板题。

给定数列 a[1],a[2],...,a[n],你需要依次进行 q 个操作,操作有两类:

1、lrx:给定 l,r,x,对于所有 i∈[l,r],将 a[i] 加上 x(换言之,将 a[l],a[l+1],...,a[r] 分别加上 x);

2、lr:给定 l,r,求 ∑ri=la[i] 的值(换言之,求 a[l]+a[l+1]+⋯+a[r] 的值)。

【输入】

第一行包含 2 个正整数 n,q,表示数列长度和询问个数。保证 1≤n,q≤106 。

第二行 n 个整数 a[1],a[2],...,a[n],表示初始数列。保证 ∣∣a[i]∣≤106 。

接下来 q 行,每行一个操作,为以下两种之一:

1、lrx:对于所有 i∈[l,r],将 a[i] 加上 x;

2、lr:输出 ∑ri=la[i] 的值。

保证 1≤l≤r≤n,∣x∣≤106 。

【输出】

对于每个 2lr 操作,输出一行,每行有一个整数,表示所求的结果。

【输入样例】

复制代码
5 10
2 6 6 1 1
2 1 4
1 2 5 10
2 1 3
2 2 3
1 2 2 8
1 2 3 7
1 4 4 10
2 1 2
1 4 5 6
2 3 4

【输出样例】

复制代码
15
34
32
33
50

【提示】

数据范围与提示:

对于所有数据,1≤n,q≤10^6,∣a[i]∣≤10^6,1≤l≤r≤n,∣x∣≤10^6 。

一、 题目背景与分析

核心需求:给定一个长度为N的数组,要求支持两种极其频繁的操作:

  1. 区间修改 :给区间[L,R]内的每一个数加上一个值x

  2. 区间查询 :求区间[L,R]内所有数的总和。

数据规模:数组长度N≤10^6,操作次数Q≤10^6。

面对这种百万级别的数据,任何带有O(N)层级的操作都会遭遇超时。普通数组区间修改太慢,前缀和数组单点/区间修改太慢。我们需要一种能将修改和查询都死死压制在O(logN) 级别的数据结构------线段树

二、 核心与思考过程:为什么需要"懒标记"?

线段树的区间查询很容易理解,但区间修改 是初学者最大的噩梦。 如果我们要给 [1,100000] 的每个数加1,难道要顺着线段树跑到最底层的100000个叶子节点去逐一修改吗?那时间复杂度直接退化成O(N),线段树就失去了意义。

破局点:懒标记(Lazy Tag)------ 伟大的"非必要不执行"思想

  1. 截留 :当修改区间完全包围 了当前节点的管辖区间时,我们直接修改当前节点的总账,并在该节点门上贴一个"懒标记"(记录下面每个小弟应该加多少钱),然后**直接return,**不往下走。

  2. 下放(PushDown) :只有当下一次操作(无论是修改还是查询)被迫要劈开当前区间、访问其子节点时,我们才把门上的懒标记撕下来,传给左右两个儿子。

通过懒标记,我们将极其昂贵的底层遍历,转化为了精准的O(1)局部更新。

三、 算法设计:线段树的两大实战流派

在竞赛实战中,线段树有两种极其经典的写法,它们在时间复杂度上完全一致,但在工程风格上各有千秋:

  • 写法一:传统传参 。节点只存vallazy。每次递归都把当前管辖的[l, r]作为参数传下去。优点是极限省空间,是OI届最主流的写法。

  • 写法二:胖节点封装 。在结构体里直接存入该节点管辖的lrlen。优点是函数传参极其干净清爽(只需传目标区间和节点编号),大幅减轻大脑的记忆负担。

四、 时空复杂度分析

  • 时间复杂度:建树O(N);单次区间修改O(logN);单次区间查询O(logN)。整体时间复杂度 O(QlogN)。

  • 空间复杂度 :由于是二叉树的底层铺设,需要开辟原数组大小4倍的空间,空间复杂度O(N)。

五、 易错点总结

  1. 乘法溢出 :更新总账时val+=x*len,如果xlen都是int,乘积会撑爆临时变量导致变负数,必须写成1ll*x*len

  2. 新官不理旧账PushDown下放标记时,子节点的标记必须是累加lazy+=),绝不能是直接赋值(lazy=),否则会覆盖掉历史账单。

  3. 忘报总账update函数最后必须pushup(rt),否则底层数据改了,高层情报不同步。


六、 完整代码

写法一:传统传参
cpp 复制代码
//区间修改 区间查询 线段树模版1
#include <iostream>
using namespace std;
const int maxn=1000010;
int n,q;
int a[maxn];//原数列
struct node{//线段树节点封装
    long long val;//节点值
    long long lazy;//懒标记
}tree[maxn*4];//段树必须开4倍空间

//向上更新 汇总左右儿子的情报给父亲
void pushup(int rt){
    //父节点的值等于左右儿子值之和
    tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
}

//建树
void build(int l,int r,int rt){
    //初始化懒标记为0
        tree[rt].lazy=0;
    if(l==r){
        //已经递归到叶子节点 直接赋予原数列的值
        tree[rt].val=a[l];
        return ;
    }
    int m=(r+l)>>1;
    build(l,m,rt<<1);//递归构造左子树
    build(m+1,r,rt<<1|1);//递归构造右子树
    pushup(rt);//左右子树构造完毕,向上更新
}

//下放懒标记 清理历史账单 确保子节点数据是最新的
//ln 左儿子区间长度 rn 右儿子区间长度
void pushdown(int ln,int rn,int rt){
    if(tree[rt].lazy){//只有有标记才需要下放
        //左子树
        tree[rt<<1].val+=tree[rt].lazy*ln;
        //右子树
        tree[rt<<1|1].val+=tree[rt].lazy*rn;
        //左子树
        tree[rt<<1].lazy+=tree[rt].lazy;
        //右子树
        tree[rt<<1|1].lazy+=tree[rt].lazy;
    }
    //清除当前节点懒标记
    tree[rt].lazy=0;
}


//[L,R]区间每个数加上x  当前节点为rt  rt所管辖区间为[l,r]
void update(int L,int R,int x,int l,int r,int rt){
    //如果当前管辖区间被目标区间完全包围
    if(L<=l&&R>=r){
        tree[rt].val+=1ll*x*(r-l+1);
        tree[rt].lazy+=x;//贴上懒标记
        return;
    }
    int m=(l+r)>>1;
    //没有被完全包围 说明必须劈开 劈开前必须先下放历史标记
    //下推以后 才能准确更新子节点
    pushdown(m-l+1,r-m,rt);
    if(L<=m) update(L,R,x,l,m,rt<<1);
    if(R>m) update(L,R,x,m+1,r,rt<<1|1);
    pushup(rt);
}

//区间查询
long long query(int L,int R,int l,int r,int rt){
    //完全包围 直接交出总账
    if(L<=l&&R>=r) return tree[rt].val;
    if(L>r||R<l) return 0;
    int m=(l+r)>>1;
    //下推以后才可以准确查询子节点
    pushdown(m-l+1,r-m,rt);
    long long ans=0;
    if(L<=m) ans+=query(L,R,l,m,rt<<1);
    if(R>m) ans+=query(L,R,m+1,r,rt<<1|1);
    return ans;
}

int main(){
    //IO加速
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>q;
    //原数列赋值
    for(int i=1;i<=n;i++) cin>>a[i];
    //建树
    build(1,n,1);
    //总共q次操作
    while(q--){
        int k;
        cin>>k;
        if(k==1){//对于所有i∈[l,r],将a[i]加上x
            int l,r,x;
            cin>>l>>r>>x;
            update(l,r,x,1,n,1);
        }
        else{//求a[l]+a[l+1]+⋯+a[r]的值
            int l,r;
            cin>>l>>r;
            cout<<query(l,r,1,n,1)<<"\n";
        }
    }
}
写法二:胖节点封装
cpp 复制代码
//区间修改 区间查询 线段树模版2
#include <iostream>
using namespace std;
int n,q;
const int maxn=1000010;
//线段树封装节点
struct node{
    long long val;
    long long lazy;
    //当前节点管辖的左边界、右边界、区间长度
    int l,r,len;
}tree[maxn<<2];
int a[1000010];//原数列

//向上更新
void pushup(int rt){
    tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
}

//建树,[l,r]为当前节点rt所代表的区间,rt为当前节点
void build(int l,int r,int rt){
    //初始化每个节点懒标记为0
    tree[rt].lazy=0;
    //每个节点所代表区间的长度
    tree[rt].len=r-l+1;
     //当前节点左端点
    tree[rt].l=l;
    //当前节点右端点
    tree[rt].r=r;
    if(l==r){
        tree[rt].val=a[l];
        return;
    }
    int m=(l+r)>>1;
    build(l,m,rt<<1);//递归构造左子树
    build(m+1,r,rt<<1|1);//递归构造右子树
    //左右子树构造完毕,向上更新
    pushup(rt);
}

//rt为当前节点 ln为当前节点所代表左区间长度 
//rn为当前节点所代表右区间长度
void pushdown(int ln,int rn,int rt){
    //下推懒标记 更新rt左儿子节点值
    tree[rt<<1].val+=tree[rt].lazy*ln;
    //下推懒标记 更新rt右儿子节点值
    tree[rt<<1|1].val+=tree[rt].lazy*rn;
    //更新rt左儿子懒标记
    tree[rt<<1].lazy+=tree[rt].lazy;
    //更新rt右儿子懒标记
    tree[rt<<1|1].lazy+=tree[rt].lazy;
    //rt懒标记消除
    tree[rt].lazy=0;
}

//[l,r]区间增加x,rt为根节点
void update(int l,int r,int x,int rt){
    if(l<=tree[rt].l&&r>=tree[rt].r){
        tree[rt].val+=1ll*x*tree[rt].len;
        tree[rt].lazy+=x;
        return;
    }
    if(r<tree[rt].l||l>tree[rt].r) return;
    int m=(tree[rt].r+tree[rt].l)>>1;
    //懒标记下推
    if(tree[rt].lazy)
        pushdown(m-tree[rt].l+1,tree[rt].r-m,rt);
    if(l<=m) update(l,r,x,rt<<1);
    if(r>m) update(l,r,x,rt<<1|1);
    pushup(rt);
}

//rt为当前节点,[l,r]为要查询区间
long long query(int l,int r,int rt){
    if(l<=tree[rt].l&&r>=tree[rt].r) 
        return tree[rt].val;
    int m=(tree[rt].l+tree[rt].r)>>1;
    //懒标记下推
    if(tree[rt].lazy)
        pushdown(m-tree[rt].l+1,tree[rt].r-m,rt);
    long long ans=0ll;
    if(l<=m) ans+=query(l,r,rt<<1);
    if(r>m) ans+=query(l,r,rt<<1|1);
    return ans;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>q;
    for(int i=1;i<=n;i++) cin>>a[i];
    //建树
    build(1,n,1);
    //总共执行q次操作
    while(q--){
        int k;
        cin>>k;
        if(k==1){//对于所有i∈[l,r],将a[i]加上x
            int l,r,x;
            cin>>l>>r>>x;
            update(l,r,x,1);
        }
        else{//求a[l]+a[l+1]+⋯+a[r]的值
            int l,r;
            cin>>l>>r;
            cout<<query(l,r,1)<<"\n";
        }
    }
    return 0;
}
相关推荐
abant22 小时前
leetcode 148 排序链表 归并终极形态
算法·leetcode·链表
yang_B6212 小时前
光斑中心检测
人工智能·算法
苦瓜小生2 小时前
【Leetcode Hot 100刷题路线】| 找工作速刷 | 第23题 - [49] - 字母异位词分组
算法·leetcode·职场和发展
Yupureki2 小时前
《Linux系统编程》16.进程间通信-共享内存
linux·运维·服务器·c语言·数据结构·c++
Allen_LVyingbo2 小时前
自进化医疗智能体:动态记忆与持续运行的Python架构编程(上)
数据结构·python·架构·动态规划·健康医疗
炽烈小老头2 小时前
【每天学习一点算法 2026/03/26】合并区间
学习·算法
代码探秘者2 小时前
【算法篇】5.链表
java·数据结构·人工智能·python·算法·spring·链表
1104.北光c°2 小时前
Leetcode3.无重复字符的最长子串 HashSet+HashMap 【hot100算法个人笔记】【java写法】
java·开发语言·笔记·程序人生·算法·leetcode·滑动窗口
MicroTech20252 小时前
微算法科技(NASDAQ: MLGO)支持区块链的工业物联网隐私保护新方案:基于格的可链接环签名技术
科技·算法·区块链