首先,有一个问题:给定一个含有
个元素的集合
,问题是怎样才能在比较短的时间内进行以下操作:
① 查询区间和:
即计算
② 单点查询和增加:得到 或者在 基础上增加
③(拓展内容)区间增加:将一个区间 的值统一增加
朴素的解决办法:对于区间和,可以利用前缀和思想,在 内事先计算
那么
,单次查询
但是这样还是不够,于是就衍生出了一种叫做树状数组的数据结构来解决这类问题。树状数组可以做到一次操作
,
次就是
。
树状数组
①引子:所谓数据结构,实现了对数据的一系列操作,比较高级的数据结构往往在数据的布局中隐含一些数学关系,通过这些对数学关系的利用,就实现了降低时空复杂度的目的。
②那么树状数组里隐藏了一些什么数学关系呢?我们可以先看看下图:
如图:这个看着像颗二叉树的东西,其实只是把编号的规则改了一下,仔细研究一下子结点和父节点编号的关系,比如4是8的左子节点,12是8的右子结点。有什么关系呢?
这几个数的二进制如下:
(左子)
(右子)
(父)
于是聪明的发明者就发现了
(下标2表示二进制)那么这个
怎么来的?
以
为例,可以看出
是
二进制下最右边的
所对应的值,。比如
,
的最右边的
所对应的值就是
。
③
我们将
的二进制表达式最右边的
所对应的值定义为一个函数叫
,在程序实现中,Lowbit(x) = x & -x
,为啥会这样写呢?因为计算机中的整数采用补码表示,因此
实际上是把
按位取反,然后末尾加
的结果,比如:
二者按位“与”之后,前面部分为
,之后的
保持不变,这样就得到了结果。
做完准备工作之后,我们来讲讲上图:
第一:可以发现,对于一个结点
,如果它是左子结点,那么它的父结点的编号就是
;如果它是右子结点,那么它的父结点的编号就是
(请在草稿上验证)。
第二:在图中,每一层的
值相同,并且
越大,越靠近树根。我用一些线段将这些灰色结点连了起来以便理解,需要注意的是编号为
的点是虚拟结点,为了方便理解而设定。
在搞清楚树是怎么构成的之后,开始解决问题,不过在这之前我们需要先构造一个数组
,其中:
为什么会构造这么一个数组呢,可以从图中看出,每个灰色结点都有一个属于它的白色长条(对于 的点,就是它本身),而每一段“白色长条”所覆盖的结点中的数的总和就是 。例如: 。(请在草稿上验证)
④计算前缀和
:
根据
的性质,从图中可以看出,顺着某个结点
往上走(不一定经过树的边)一直到
,一路上把沿途的
加上就行了。下面用图示来说明。
⑤单点增加
在这样的结构下修改一个
是会对其他结点有影响的,所以我们需要同时修改另一些结点,其实我们只需修改包含了
的点就行了,从
点往右上走(同样不一定经过树的边),沿途修改对应的
就行,如图所示:
⑥代码:
单点增加:
void add(int x, int c){
while(x <= n){
C[x] += c;
x += lowbit(x);
}
}
区间求和:
int query(int x){//前缀和,区间和只需query[R]-query[L-1]即可
int ans = 0;
while(x > 0){
ans += C[x];
x -= lowbit(x);
}
return ans;
}
⑦:以上就介绍了树状数组支持的两个基本操作:“单点增加”+“区间查询”。那么,怎么用树状数组实现“区间增加”+“单点查询”呢?请见下文:
先给出一道例题:
①:给定长度为
的数列
,然后输入
行操作指令.
指令形如“
”,
表示把数列中第
个数都加
。
表示询问
的值(忽略
)
②思路分析:本题的指令是“区间增加”和“单点查询”,而树状数组仅支持“单点增加”,所以需要做一下转换。
新建一个数组
初始化为0,
就是
的差分,以此把区间操作改为单点操作。
对于指令一,转化为一下两个操作:
1.把
加上
。
2.把
减去
。
为什么会执行这两条呢,我们来考虑一下
的前缀和,
1.对于
,前缀和不变。
2.对于
,前缀和增加了
。
3.对于
,前缀和不变(
处加
,
处减
,抵消了)
通过以上分析,可以发现
数组的前缀和就反映了区间增加产生的影响。
于是,我们可以用树状数组维护
的前缀和,又因为这些操作具有累加性,所以在树状数组上查询前缀和
,就得到了区间增加指令在
上增加的数值总和。再加上
的初始值,就可以得到单点查询的答案。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int n;
int C[50005];
int lowbit(int x){
return x & -x;
}
int query(int x){
int ans = 0;
while(x > 0){
ans += C[x];
x -= lowbit(x);
}
return ans;
}
void add(int x, int c){
while(x <= n){
C[x] += c;
x += lowbit(x);
}
}
int main(){
scanf("%d", &n);
memset(C, 0, sizeof(C));
int b, e = 0;
for(int i = 1; i <= n; i++){
scanf("%d", &b);
add(i, b - e);
e = b;
}
for(int i = 1; i <= n; i++){
int opt, l, r, c;
scanf("%d %d %d %d", &opt, &l, &r, &c);
if(opt == 0){
add(l, c);
add(r + 1, -c);
}
else if(opt == 1){
printf("%d\n",query(r));
}
}
return 0;
}
文末总结:
本文借鉴了刘汝佳的《算法竞赛入门经典 训练指南》和李煴东的《算法竞赛进阶指南》。
个人所作,难免疏漏,希望大家发现问题能够指出,也希望大家能够从这篇文章中学到东西。