记录117
cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+10;
int a[MAXN],c[MAXN];// a[] 是原始数组,c[] 是树状数组(核心数组)
int n,m; // n 是数列长度,m 是操作次数
int lowbit(int x){//计算 x 的二进制表示中最低位的 1 所代表的值
return x&(-x);
}//用途:用于确定当前节点管辖的范围,以及在树中向上/向下跳转
int sum(int x){// sum 函数:查询前缀和
int res=0;
for(int i=x;i>0;i-=lowbit(i)) res+=c[i];
return res;
}// 作用:计算原数组 a 中区间 [1, x] 的元素之和
void add(int x,int y){// add 函数:单点修改
for(int i=x;i<=n;i+=lowbit(i)) c[i]+=y;
}// 作用:将原数组 a 的第 x 个元素加上 y,并维护树状数组 c
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]);
}
while(m--){
int op,x,y;
cin>>op>>x>>y;
if(op==1) add(x,y);
else cout<<sum(y)-sum(x-1)<<endl;// 利用前缀和思想求区间和
}
return 0;
}
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
题目传送门
https://www.luogu.com.cn/problem/P3374
突破口
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某一个数加上 x;
求出某区间每一个数的和。
输入格式
第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:
1 x k含义:将第 x 个数加上 k;
2 x y含义:输出区间 [x,y] 内每个数的和。输出格式
输出包含若干行整数,即为所有操作 2 的结果。
思路
这道题是**树状数组(Binary Indexed Tree, BIT)**的入门模板题。
💡 核心思路:为什么要用树状数组?
题目中有两种操作:
- 单点修改:给某个数加上 xx 。
- 区间查询:求一段区间的和。
暴力做法的痛点:
- 如果用普通数组,修改 是 O(1)O(1) ,但求和需要遍历区间,是 O(N)O(N) 。
- 如果用前缀和数组,求和 是 O(1)O(1) ,但一旦某个数变了,后面的前缀和都要重新算,修改变成了 O(N)O(N) 。
- 题目中 NN 和 MM 都高达 5×1055×105 ,如果是 O(N)O(N) 的操作,总复杂度会达到 O(NM)O(NM) ,必然超时。
树状数组的优势:
它通过一种巧妙的"二进制分组"方式,将修改 和查询 的时间复杂度都降低到了 O(logN)O(logN)。
核心原理:
树状数组 c[i] 并不直接存储原数组 a[i] 的值,而是存储一段区间 的和。这段区间的长度由 lowbit(i) 决定。
lowbit(x):取出 xx 二进制表示中最低位的 1。例如lowbit(4)(100) = 4,lowbit(6)(110) = 2。c[i]的职责 :管理区间[i - lowbit(i) + 1, i]的和。
树状数组的样子:
因为是按照二进制的1来拆分,所以会根据层级形成一棵树
代码分析
cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+10;
int a[MAXN], c[MAXN]; // a[] 是原始数组,c[] 是树状数组(核心数组)
int n, m; // n 是数列长度,m 是操作次数
解析:
a[MAXN]:虽然题目有原始数组,但在树状数组的很多实现中,其实可以省略a数组,只维护c数组。这里保留a是为了逻辑清晰。c[MAXN]:这是树状数组 本体。注意c的下标通常从 1 开始,因为lowbit(0)会导致死循环。
cpp
int lowbit(int x) {
// 计算 x 的二进制表示中最低位的 1 所代表的值
return x & (-x);
}
解析:这是树状数组的灵魂。
- 原理 :利用补码特性。
-x在计算机中等于~x + 1(按位取反加 1)。 - 效果 :
x和-x进行按位与运算(&),除了最低位的 1 以外,其他位都会变成 0。 - 作用:它告诉我们要"跳"多远。比如在查询时,它决定了我们要减去多少才能跳到下一个负责更小范围的节点。
cpp
int sum(int x) {
// sum 函数:查询前缀和
int res = 0;
for(int i = x; i > 0; i -= lowbit(i)) res += c[i];
return res;
}
解析 :查询操作(区间求和的基础)。
- 目标 :计算原数组
a[1]到a[x]的总和。 - 逻辑 :我们将区间
[1, x]拆分成若干个由c数组管理的子区间。 - 过程 :
- 先把
c[x]加上(它管理着以x结尾的一段)。 - 然后
i -= lowbit(i),跳到剩下的前缀的结尾。 - 重复直到
i变为 0。
- 先把
- 复杂度:因为每次至少减去二进制的一位,所以循环次数是 log2Nlog2N 。
cpp
void add(int x, int y) {
// add 函数:单点修改
for(int i = x; i <= n; i += lowbit(i)) c[i] += y;
}
解析 :修改操作。
- 目标 :给
a[x]加上y。 - 逻辑 :因为
a[x]被包含在多个c数组的节点中,所以a[x]变了,所有包含它的c[i]都要更新。 - 过程 :
- 更新
c[x]。 i += lowbit(i):跳到包含当前区间的父节点(范围更大的节点)。- 一直更新直到超出数组长度
n。
- 更新
cpp
int main() {
cin >> n >> m;
// 初始化
for(int i = 1; i <= n; i++) {
cin >> a[i];
add(i, a[i]); // 将初始值插入树状数组
}
while(m--) {
int op, x, y;
cin >> op >> x >> y;
if(op == 1) add(x, y); // 操作1:单点修改,a[x] += y
else cout << sum(y) - sum(x - 1) << endl; // 操作2:区间查询
}
return 0;
}
解析:
- 初始化 :通过循环调用
add,将初始数组构建成树状数组。时间复杂度 O(NlogN)O(NlogN) 。 - 操作 1 :直接调用
add(x, y),表示位置x的值增加了y。 - 操作 2 :利用前缀和思想 。
- 要求区间
[x, y]的和,等价于:(1 到 y 的和) - (1 到 x-1 的和)。 - 即
sum(y) - sum(x - 1)。
- 要求区间
📌 总结
这段代码展示了树状数组最标准、最简洁的写法:
lowbit负责导航(找爸爸或找前一个兄弟)。add负责自底向上更新(修改自己,通知所有包含自己的父节点)。sum负责自顶向下(或跳跃式)累加(把大区间拆成几个小区间加起来)。main中利用sum(y) - sum(x-1)巧妙解决任意区间求和问题。
