【例 1】数列操作(信息学奥赛一本通- P1535)

【题目描述】

给定n个数列,规定有两种操作,一是修改某个元素,二是求子数列[a,b]的连续和。数列元素个数最多10万个,询问操作最多10万次。

【输入】

第一行2个整数n,m(n表示输入n个数,m表示m操作)

第二行n个整数

接下来m行,每行三个数k,a,b(k=0,表示求子数列[a,b]的连续和;k=1,表示第a个数加b)。

【输出】

若干行,表示k=0时,对应子数列[a,b]连续和。

【输入样例】

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

【输出样例】

复制代码
11
30
35

一、 题目分析

【核心需求】 维护一个最多包含 105 个元素的序列,支持两种交替进行的操作(总操作数高达 105 次):

  1. 单点修改: 将序列中第x个数加上val。

  2. 区间查询: 求序列中第a个数到第b个数的连续和。

【性能瓶颈】 对于10^5级别的数据,任何单次操作复杂度为O(n)的算法都会在1s的时限内遭遇 超时。我们必须寻找一种能在O(logn)时间内同时兼顾"修改"与"查询"的数据结构。

二、 思考过程

面对这类动态查询问题,初学者通常会经历三次思维迭代:

  1. 纯暴力数组: 用普通数组存数据。单点修改只需O(1),但求区间和需要for循环遍历,时间复杂度为O(n)。10^5 次操作必超时。

  2. 静态前缀和: 预处理出前缀和数组。此时区间查询降到了极速的 O(1),但一旦发生"单点修改",该位置之后所有的前缀和都要重新计算,修改复杂度退化为 O(n)。依然超时。

  3. 树状数组: 既然绝对的O(1)无法兼顾,我们就选择折中,树状数组通过二进制拆分,将一段连续的区间和"打包"交给特定的节点管理。无论是"向上更新"还是"向下求和",都能在极其稳定的O(logn)时间内完成。

三、 解题思路

树状数组的核心,全部浓缩在一个只有一行的函数里:lowbit(x)

lowbit(x)=x&(−x)

利用计算机底层的补码机制(按位取反,末位加一),lowbit(x)能精准提取出 x 二进制下最右边的1所代表的整数。在树状数组的物理模型中,lowbit(x)代表了第x个"片区老大"所管辖的原数组元素的个数。

基于此,我们建立起一套严密的上下级体系:

  • 当原数组第x个数发生改变时(单点修改): 它不仅要更新自己,还要向上级汇报。通过不断执行 x+=lowbit(x),程序会精准地跳跃到所有包含了该元素的上级节点,并同步更新它们的值。

  • 当我们需要前 x 个数的和时(前缀查询): 我们只需要通过 x-=lowbit(x),不断向左上方寻找上一个相邻的"片区老大",把他们手里的管辖总和拼凑起来,就能完美拼出一个完整的前缀区间。

四、 算法设计

  1. 建树: 抛弃物理上的原数组。在读取初始值时,我们直接将其视作对一个全0树状数组的n 次单点修改(update),代码优雅。

  2. 操作流转:

    • 若指令为修改操作:直接调用update(a,b)

    • 若指令为查询操作:利用容斥原理,求区间 [a,b] 的和,等价于求出从 1 至 b 的数列和,减去从1至a−1的数列和,即sum(b)-sum(a-1)

五、 时空复杂度分析

  • 时间复杂度: 建树阶段相当于执行n次单点修改,耗时O(nlogn)。

    • 联机操作阶段共m次,每次操作耗时O(logn)。

    • 总体时间复杂度:O((n+m)logn)。对于10^5的数据规模,运算量在 2×10^6 级别,安全稳定。

  • 空间复杂度: 仅需开辟一个长度为n+1的long long数组,总体空间复杂度为O(n)

六、 易错点总结

在信奥实战中,能写出树状数组只是及格线,能避开以下三个学生易错的点,才能确保AC:

  1. IO 竞速陷阱(易 TLE): 操作数达到 10 万级别,建议在主函数开头加上 ios::sync_with_stdio(false); cin.tie(0); 解除绑定,并且输出换行必须用 \n,严禁用 endl 强刷缓冲区。

  2. 数据类型溢出(极易 WA): 题目中随时可能出现高频次的极大数值累加。凡是涉及到树状数组本体、更新的值(val)、查询的返回值,请一律使用long long声明,彻底杜绝隐形溢出。

  3. 下标从0开始的惨剧(极易死循环): 树状数组的下标绝对不能传入 0。因为 lowbit(0)=0,会导致updatesum函数中的while循环陷入死循环跳不出来。

七、完整代码

cpp 复制代码
//单点修改 区间查询
#include <iostream>
using namespace std;
typedef long long ll;
int n,m;
ll c[100010];//树状数组

//求x在二进制表示下最右边的最低位的1所代表的整数
int lowbit(int x){
    return x&(-x);
}

//单点修改 x代表修改原数列第x个数
//到树状数组中则是要修改所有包含原数列第x项的位置
//val代表原数列第x项增加val
void update(int x,ll val){
    for(int i=x;i<=n;i+=lowbit(i)){
        c[i]+=val;
    }
}

//求原数列从第一项到第x项的和
ll sum(int x){
    ll ret=0;//记录和
    while(x>0){
        ret+=c[x];//树状数组c[x]一定包含原数列第x项
        //重点:减去c[x]所管辖的原数列项数
        //意义:精准地往回跳跃到一个不重叠的、相邻的上一级区块
        x-=lowbit(x);//要减去c[x]包含的原数列的项数
    }
    return ret;
}

int main(){
    //io加速
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    //不需要给原数组赋值,直接把每次赋值变为对树状数组
    //所有包含a[i](原数组)的项的更新
    for(int i=1;i<=n;i++){
        ll x;
        cin>>x;
        update(i,x);
    }
    //接下来执行m次操作
    while(m--){
        int k,a;
        ll b;
        cin>>k>>a>>b;
        if(k==0){//求子数列[a,b]的连续和
        //即算出从1至b的数列和减去1至(a-1)的数列和
            cout<<sum(b)-sum(a-1)<<"\n";
        }
        //第a个数加b
        else  update(a,b);
    }
    return 0;
}
相关推荐
big_rabbit05022 小时前
[算法][力扣222]完全二叉树的节点个数
数据结构·算法·leetcode
张李浩2 小时前
Leetcode 15三题之和
算法·leetcode·职场和发展
2301_793804693 小时前
C++中的适配器模式变体
开发语言·c++·算法
x_xbx3 小时前
LeetCode:206. 反转链表
算法·leetcode·链表
abant23 小时前
leetcode 138 复制随机链表
算法·leetcode·链表
ab1515174 小时前
3.17二刷基础112 118 完成进阶52
数据结构·算法
美式请加冰4 小时前
链表的介绍和使用
数据结构·链表
旖-旎4 小时前
二分查找(1)
c++·算法·二分查找·力扣·双指针
困死,根本不会4 小时前
【C 语言】指针学习笔记:从底层原理到实战应用
c语言·开发语言·笔记·学习·算法