树状数组————BIT(Binary Index Tree),又称二分索引树。不得不承认,二分索引树的名字更能反应它的本质,而树状数组这个名字则更加的直观。
与其说是一种算法,不如说是一种数据结构。从名字可以直观的看出,这是一种像是树一样的数组。这就具有很多优点,能够在log(n)的时间内进行查询、求和等操作。
下面看这张图(提到树状数组必须离不开这个图)
其中以A表示原数组,C表述树状数组。
下面给出一个构造的原则(或者说树状数组的每一个结点的含义吧):C[i] = A[i-2^k+1] + .........+A[i]
其中这个k的含义是什么呢?
即表示将i转化为2进制后,从右往左数,0的个数。(大家大可不必问为什么,这个证明应该是数学问题了,找了一下没有发现相关资料于是我就作罢了...)
比如说,i = 8
8的二进制表示为00001000,从右往左数到第一个1为止共有三个0,则k = 3
那么C[i]表示的是从A[8 - 2 ^ 3 + 1] 到 A[8]也即A[1......8]的值
下面给出求一个函数LowBit(int index)用来求2^k
相信这个地方还是很难理解,我举几个例子看看。
例如,index = 6 经过LowBit运算后得出结果为2那么C[6] = A[5] + A[6]
(k == 2 刚好是表示A[i-1 + 1] + A[i - 1 + 1]这两个数的和,和前边的构造原则是一致的)
有一个简单的记忆方式,6的因子有1,2,3,6,这四个因子中有一个2是2的n次方,所以这个2就是我们想要求的那个k
在打一个比方,8的因子有1,2,4,8,其中有三个数可以表示成2的n次方(2,4,8),但那个最大的8是我们想要的k.
又如7,其因子1,7没有一个数可以表示成2的n次方,得出的结果k = 1,那么,可以得出一致结论,当index为奇数时,C[i] = A[i]
(以上红字部分可以加深对LowBit的理解)
<span style="font-family:Microsoft YaHei;font-size:14px;">int LowBit(int index) { return index & (-index); }</span>
(至于为什么这么算,可以看BYvoid的链接https://www.byvoid.com/blog/binary-index-tree)
要时刻明白,LowBit这个函数,求得的是index的所有因子中满足以下条件的那个数值:
1.最大的因子
2.该因子可以表示成2的整数次幂。
巧合的是,这个LowBIt求得的值,就是当前结点的管辖范围。
什么意思呢?
大家看上图:同样以index = 8 为例。index = 8的这个叶节点的管辖范围是原数组中的1----8,这8个结点都可以看做是index结点的子结点
实际上,我们有这样的重要定理:
index += LowBit(index)得到的值,正是子节点index的父节点。
如:index = 4时 index += LowBit(index) = 8,也就是管辖4这个结点的父节点。
相应的index -= LowBit(index)可以得到index管辖的子节点。
下面看一个例子:
敌兵布阵
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 45999 Accepted Submission(s): 19553
中央情报局要研究敌人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工兵营地一共有多少人,例如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但敌兵营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.
每组数据第一行一个正整数N(N<=50000),表示敌人有N个工兵营地,接下来有N个正整数,第i个正整数ai代表第i个工兵营地里开始时有ai个人(1<=ai<=50)。
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
每组数据最多有40000条命令
对于每个Query询问,输出一个整数并回车,表示询问的段中的总人数,这个数保持在int以内。
1 10 1 2 3 4 5 6 7 8 9 10 Query 1 3 Add 3 6 Query 2 7 Sub 10 2 Add 6 3 Query 3 10 End
Case 1: 6 33 59
这里,再给出两个重要的函数。
1.
____________________________________________________________________________________________________________________________________
<span style="font-family:Microsoft YaHei;font-size:14px;">void Update(int index,int value) { while(index <= N){ tree[index] += value;//这里不一定要加,可以给出必要的更改 index += LowBit(index); } }</span>
其中N是表示原数组的容量
(这个地方其实有一容易被忽略的地方,就是C语言数组下标一般从0开始。不过LowBit(0) == 0 ,那么在这个函数中 0 += LowBit(0) 会一直得到0的结果始终<=N就会变成死循环!所以在构造原数组的时候一定要注意!)
好了,下面说明一下这个函数的功能。
因为树状数组的特殊性——某一个结点的管辖结点可能有很多个,修改子节点的时候,必须上溯到它的父节点,这一条路径的上的值都要更改,所以有了这个Update函数。
____________________________________________________________________________________________________________________________________
2.
<span style="font-family:Microsoft YaHei;font-size:14px;">int Sum(int index) { int sum = 0; while(index){ sum += tree[index]; index -= LowBit(index); } return sum; }</span>
函数很简单,求index结点所对应的子节点和。
(其实就是从1----index的原数组的和)
看懂了图就看懂了一大半,下面给出题目代码,有兴趣的可以研究一下
HDU 1166
<span style="font-family:Microsoft YaHei;font-size:14px;">/*-------------------------------------------------------------------- 12二进制: 00001100 取反: 11110011 负数补码形式哪负数补码呢 我先看看负数补码何表示【负数补码其原码逐位取反符号位除外;整数加1】 我返弄: 先11110011-1=11110010 符号位外取反:10001101 看看除符号外数:0001101 13 所数-13 PS: 1 & (-1) == 1 */ #include <stdio.h> #include <string.h> #define maxn 50005 int num[maxn]; int TreeC[maxn],N;//树状数组 void Initialize() { memset(TreeC,0,sizeof(TreeC)); memset(num,0,sizeof(num)); } int LowBit(int index) { return index & (-index); } int Sum(int index) { int ans = 0; while(index > 0){ ans += TreeC[index]; index -= LowBit(index); } return ans; } void Update(int index,int value)//维护树状数组 { while(index <= N){ TreeC[index] += value; index += LowBit(index); } } int main() { int Casenum,a,b; char str[10]; scanf("%d",&Casenum); for(int cases = 1 ; cases <= Casenum ; ++cases){ Initialize(); scanf("%d",&N); bool over = false; for(int i = 1 ; i <= N ; ++i){ scanf("%d",&num[i]); Update(i,num[i]);//读取一组数构建一次树状数组 } printf("Case %d:\n",cases); while(scanf("%s",str) != EOF){ switch(str[0]){ case 'Q' : { scanf("%d%d",&a,&b) ; printf("%d\n",Sum(b)-Sum(a-1)) ; } ; break; case 'A' : { scanf("%d%d",&a,&b) ; Update(a,b) ;} ; break; case 'S' : { scanf("%d%d",&a,&b) ; Update(a,-b) ;} ; break; case 'E' : over = true;break; } if(over) break; } } return 0; }</span>