1.问题来源
动态连续和查询问题。给定一个 n个元素的数组 A1,A2,...,An,你的任务是
设计一个数据结构,支持以下两种操作。
★ Add(x,d)操作:让 Ax增加 d.
★ Query(L,R):计算 AL+AL+1+...+AR.
对普通数组进行一次修改或特定区间求和,时间复杂度为 O(N),N为修改
或求和需要扫描的数组区间大小。但有一种称为树状数组(又称二叉索引树)
的数据结构可以很好的解决这个问题,时间复杂度则为 O(logN)。加了一个
log,学过数学的我们应该都知道效率优化有多大。
2.相关的定义与概念
在讲实现之前,我们需要先理解一个函数 lowbit(x),这是一个自定义的函
数,函数名是约定成俗的,作用就是 x的二进制表达式中最右边的 1所对应的
值(而不是这个比特的序号)。比如,38288的二进制是 1001010110010000,
所以 lowbit(38288)=16(二进制是 10000)。
代码实现:
int lowbit(int x)//位运算,利用计算机补码特性
{
return x&-x;
}
写个长注释,假设 x=10
10的二进制:1010,我们知道 -x就是 x的二进制按位取反,末尾加一以后的
结果。
-10的二进制:0101+1=0110
然后位运算符&(按位与),1010&0110=10,十进制表示就是 2
所以 lowbit(10)=2
这个函数很重要,所以先在这里交代清楚原理。
下面上图,就正式开讲树状数组了。
下图是一棵典型的 BIT,由 15个结点组成,编号为 1~15.
灰色结点是树状数组中的结点,每一层结点的 lowbit相同,而且 lowbit
越大,越靠近根。对于结点 i,如果它是左子结点,那么父结点的编号就是
i+lowbit(i);如果它是右子结点,那么父结点的编号是 i-lowbit(i).接着构造
一个辅助数组 C,其中
Ci=Ai-lowbit(i)+1+Ai-lowbit(i)+2+...+Ai
换句话说,C的每一个元素都是 A数组中的一段连续和。在 BIT中,每个灰
色结点 i都属于一个以它自身结尾的水平长条(对于 lowbit=1的那些点,“长
条”就是那个结点自己),这个长条中的数之和就是 Ci。
比如结点 12的长条就是从 9~12,即 C12=A9+A10+A11+A12。同理,
C6=A5+A6。这个等式极为重要,请大家花一些时间验证一下“Ci就是以 i结尾
的水平长条内的元素之和”这一事实。
有了 C数组之和,计算前缀和 Si就变得简单了。顺着结点 i往左走,边走
边“往上爬”(注意并不一定沿着树中的边爬),把沿途经过的 Ci累加起来。
就可以了。(请大家验证,沿途经过的 Ci所对应的长条不重复不遗漏地包含了
所有需要累加的元素),如下图所示。
而如果修改了一个 Ai,需要更新 C数组中的哪些元素呢?从 Ci开始往右
走,边走边“往上爬”(同样不一定沿着树中的边爬),沿途修改所有结点对
应的 Ci即可(请大家验证,有且仅有这些结点对应的长条包含被修改的元
素),如下图所示。
说了那么多,大家发现了吗?树状数组其实就是利用二进制。
3.伪代码实现
树状数组的第 i个元素 Tree[i]表示 A[lowbit(i)+1..i]的和,其中 lowbit
(i)表示 i的最低二进制位。
3.1当想查询一个 A[1]+...+A[i]的和,可以依据如下算法:
(1)令 sum=0,转第(2)步。
(2)假如 i≤0,算法结束,返回 sum值,否则 sum+=Tree[i],转第(3)步。
(3)i-=lowbit(i),转第(2)步。 往左走,肯定是右节点。
可以看出,这个算法就是将一个个区间的和全部加起来,并且 i-
=lowbit(i)这一步实际上等价于将 i的二进制的最后一个 1减去,而 i的二进
制里最多有 logn个 1,所以查询效率是 O(logn).
3.2而给 A[i]加上 x的算法如下:
(1)当 i>n时,算法结束,否则转第(2)步。
(2)Tree[i]+=x,i+=lowbit(i),转第(1)步。
i+=lowbit(i)这个过程实际上是一个把末尾 1补为 0的过程。
两种应用
1.
单点修改,区间查询。
add(pos,x)->在下标 pos 处加上 x
query(pos)->返回 1 到 pos 的前缀和
那么查询区间 [ l , r ]的操作就是query( r ) - query( l - 1 )
2.
区间修改,单点查询
add 和 query 的操作同运用一,但是使用方式不同:
区间修改(给区间 [ l , r ]加上 d)->
add(l,d);
add(r+1,-d);
单点查询(查询 pos 的值)->
query(pos);
模板
int lowbit(int x) // 利用了神奇的二进制 特性
{
return x&(-x);
}
int add(int i,int x) // 加数,往右走,肯定是左子节点,
{
for(;i<=n;)
{
d[i]+=x;
i=i+lowbit(i);
}
}
int sum(int c) // 计算和,往左走
{
int s=0;
while(c>0)
{
s+=d[c];
c=c-lowbit(c);
}
return s;
}
例题:
士兵杀敌1
#include<iostream>
using namespace std;
#define range 1000005
int n,d[range];
int lowbit(int x) // 利用了神奇的二进制 特性
{
return x&(-x);
}
int add(int i,int x) // 加数,往右走,肯定是左子节点,
{
for(;i<=n;)
{
d[i]+=x;
i=i+lowbit(i);
}
}
int sum(int c) // 计算和,往左走
{
int s=0;
while(c>0)
{
s+=d[c];
c=c-lowbit(c);
}
return s;
}
int main()
{
int m,t1,t2;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&t1);
add(i,t1);
}
for(int i=0;i<m;i++)
{
scanf("%d %d",&t1,&t2);
printf("%d\n",sum(t2)-sum(t1-1));
}
return 0;
}