【 例 1】区间和(信息学奥赛一本通- P1547)(基础线段树和单点修改区间查询树状数组模版)

【题目描述】

给定一个全部为零的数列,规定有两种操作,一是修改某个元素,二是求区间的连续和。

【输入】

输入数据第一行包含两个正整数n,m(n≤100000,m≤500000),以下是m行,

每行有三个正整数k,a,b(k=0或1,a,b≤n).k=0时表示将a处数字加上b,k=1时表示询问区间[a,b]内所有数的和。

【输出】

对于每个询问输出对应的答案。

【输入样例】

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

【输出样例】

复制代码
10
6
0
6
16
6
24
14
50
41

题目分析与思考过程

面对"修改与查询交替进行"的序列问题,我们首先要在脑海中进行暴力算法的推演与排除:

  1. 普通数组(暴力法)

    • 单点修改极其迅速,时间复杂度O(1)。

    • 区间求和需要使用for循环遍历,时间复杂度O(N)。

    • 在m=500000的数据量下,最坏时间复杂度逼近O(N×M),即5×10^10次运算,绝对会 超时

  2. 前缀和数组

    • 区间求和利用前缀和相减(sum[R]-sum[L-1]),时间复杂度降为O(1)。

    • 但单点修改灾难了,改了一个位置,它后面的所有前缀和全都要重新计算,时间复杂度退化为O(N),同样会超时

解法 :我们需要一种数据结构,能够在这两个极端之间找到平衡。树状数组线段树应运而生,它们都能将修改和查询的时间复杂度同时稳定在极速的O(logN)级别。


解题思路与算法设计

方法一:树状数组

树状数组的核心在于**lowbit(位运算)**。它巧妙地利用二进制特征,将一个数字划分为多个不同长度的管辖区。

  • 单点修改 :不断向右上方寻找包含当前位置的"大管家"(x+=lowbit(x)),并给它们加上该数值。

  • 区间求和 :依然是经典的前缀和思想query(R)-query(L-1)。求前缀和时,不断向左下方聚拢小管家(x-=lowbit(x))把账本加起来。

  • 特点 :代码精简,常数小,速度快,是本题的最优解

方法二:线段树

线段树运用了经典的二分分治思想。将区间不断从中间劈开,直到叶子节点(长度为 1)。

  • 建树(Build) :递归到叶子节点赋值,再自底向上 PushUp 汇总结点信息。

  • 单点修改(Update) :像狙击枪一样,一路根据中点劈开,直插到对应的叶子节点修改,归来时顺路PushUp更新沿途的父节点。

  • 区间查询(Query):如果当前节点的管辖区间被查询区间完全包围,直接交出总账;否则继续向下劈开查找。

  • 特点:虽然代码较长、常数略大,但它是后续处理"区间修改"、"区间取最值"等复杂操作的万能框架。


时空复杂度分析

算法 单次修改时间 单次查询时间 总体时间复杂度 空间复杂度
树状数组 O(logN) O(logN) O(MlogN) O(N) (单倍数组)
线段树 O(logN) O(logN) O(MlogN) O(N) (需开 4 倍数组)

易错点总结

  1. 十年OI一场空,不开long long见祖宗 :100000 个数累加,哪怕每个数不大,多次修改后的区间和也很容易突破int范围(约 21 亿)。前缀和与线段树的节点值必须使用long long

  2. 线段树的空间血泪史 :线段树是一棵二叉树,底层叶子节点为了完全铺开,数组大小必须开到原数组大小的4倍maxn<<2)。

  3. 输入输出流引发的血案 :本题查询与修改总次数m高达500000。如果使用C++原生的 cin/cout,可能因为I/O太慢而超时,在main函数开头加上 ios::sync_with_stdio(false); cin.tie(0); 来解除同步。

  4. 递归的死亡黑洞 :线段树单点修改寻找到叶子节点(l==r)并修改后,必须return。 如果不写return,程序会继续劈开区间进入死循环,导致栈溢出。


完整代码

版本一:树状数组
cpp 复制代码
//树状数组做法
#include <iostream>
using namespace std;
int n,m;
long long c[100010];//树状数组 维护前缀和


//树状数组核心 返回x二进制表示下最低位1所代表的整数
int lowbit(int x){
    return x&(-x);
}

//单点修改 在a的位置上加b
void update(int a,int b){
    //从底层一路向右上方更新所有管辖着位置a的上级节点
    while(a<=n){
        c[a]+=b;
        a+=lowbit(a);
    }
}

//查询前缀和:返回1到x的区间总和
long long query(int x){
    long long ret=0;//记录前缀和
    //从位置x一路向左下方累加经过的每一个管辖区
    while(x>0){
        ret+=c[x];
        x-=lowbit(x);
    }
    return ret;
}

int main(){
    //io加速
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    //总共m个操作
    while(m--){
        int k,a,b;
        cin>>k>>a>>b;
        if(k==0){//k=0时表示将a处数字加上b
            update(a,b);
        }
        else{//k=1时表示询问区间[a,b]内所有数的和
            cout<<query(b)-query(a-1)<<"\n";
        }
    }
    return 0;
}
版本二:线段树

(注:代码中的 lazy 标记在"单点修改"中无需使用,但为了向"区间修改"过渡,我予以保留并注释说明)

cpp 复制代码
//线段树
#include <iostream>
using namespace std;
const int maxn=100010;//元素总个数最大值

//线段树节点封装
struct node{
    long long val;//节点值(区间总和)
    //懒标记(单点修改不需要用到,作为区间修改的扩展预留)
    int lazy;
}tree[maxn<<2];//线段树开四倍最大总元素个数
int aa[maxn];//原数列
int n,m;

//PushUp向上更新
//子节点信息合并,向上更新父节点总和
void pushup(int rt){
    //父节点总和=左儿子总和+右儿子总和
    //rt<<1等价于rt*2, rt<<1|1等价于rt*2+1
    tree[rt].val=tree[rt<<1].val+tree[rt<<1|1].val;
}

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

//单点修改 将位置L的数字加上x 
//rt表示当前线段树的根节点编号
//l,r为当前节点管辖区间
void update(int L,int x,int l,int r,int rt){
    if(l==r){//叶子节点 代表已经找到要修改的位置 直接修改
        //已经找到最底层的单人节点,直接修改
        tree[rt].val+=x;
        return;
    }
    int m=(l+r)/2;
    //判断目标L在左半区还是右半区,精准递归搜索
    if(L<=m) update(L,x,l,m,rt*2);
    else update(L,x,m+1,r,rt*2+1);
    pushup(rt);//回溯 顺路向上更新沿途的父节点
}

//区间查询 求[L, R]的总和
//L R代表本次查询的区间 l r代表当前节点代表区间 rt表示当前线段树的根节点编号
long long query(int L,int R,int l,int r,int rt){
 //1:如果当前节点的管辖区间[l,r]被查询区间[L,R]完全包围,直接交出总账
    if(L<=l&&R>=r) return tree[rt].val;
 //2:区间无重叠部分
    if(L>r||R<l) return 0;
    int mid=(l+r)/2;
    long long ans=0;
    //左子区间与[L,R]有重合 递归处理
    if(L<=mid) ans+=query(L,R,l,mid,rt*2);
    //右子区间与[L,R]有重合 递归处理
    if(R>mid) ans+=query(L,R,mid+1,r,rt*2+1);
    return ans;
}

int main(){
    //IO加速
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    //原数列每个值都是0
    for(int i=1;i<=n;i++) aa[i]=0;
    build(1,1,n);//建树
    //总共执行m次操作
    while(m--){
        int k,a,b;
        cin>>k>>a>>b;
        if(k==0){//k=0时表示将a处数字加上b
            update(a,b,1,n,1);
        }
        else{//k=1时表示询问区间[a,b]内所有数的和
            cout<<query(a,b,1,n,1)<<"\n";
        }
    }
    return 0;
}
相关推荐
Book思议-2 小时前
【数据结构】栈与队列核心对比
数据结构·栈与队列对比
旺仔.2912 小时前
常用算法 详解
数据结构·算法
今儿敲了吗2 小时前
算法复盘——差分
数据结构·c++·笔记·学习·算法
qq_398586542 小时前
平衡三进制超前进位加法器
算法
西西弟2 小时前
最短路径之Dijkstra算法(数据结构)
数据结构·算法
沉鱼.442 小时前
树形DP题目
算法·深度优先
VelinX3 小时前
【个人学习||算法】多维动态规划
学习·算法·动态规划
AlenTech3 小时前
139. 单词拆分 - 力扣(LeetCode)
算法·leetcode·职场和发展
墨韵流芳3 小时前
CCF-CSP第41次认证第一题——平衡数
c++·算法·ccf·平衡数