树状数组
树状数组
武钢三中 吴豪
【引言】
在解题过程中,我们有时需要维护一个数组的前缀和 S[i]=A[1]+A[2]+...+A[i]。但是不难发现,
如果我们修改了任意一个A[i],S[i]、S[i+1]...S[n]都会发生变化。可以说,每次修改A[i]后,调
整前缀和 S[]在最坏情况下会需要 O(n)的时间。当n 非常大时,程序会运行得非常缓慢。因此,这里我
们引入“树状数组”,它的修改与求和都是 O(logn)的,效率非常高。
【理论】
为了对树状数组有个形 象的认识,我们先看下面这张图。
如图所示,红色矩形表示的数组 C[]就是树状数组。
这里,C[i]表示A[i-2^k+1]到A[i]的和,而k 则是 i在二进制时末尾 0 的个数,或者说是 i用
的幂方和表示时的最小指数。( 当然,利用位运算,我们可以直接计算出2^k=i&(i^(i-1)) )同时,
我们也不难发现,这个 k 就是该节点在树中的高度,因而这个树的高度不会超过 logn。所以,当我们修
改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有 C[]即可,这个操作的复杂度在最
坏情况下就是树的高度即 O(logn)。另外,对于求数列的前n 项和,只需找到 n 以前的所有最大子树,
把其根节点的 C 加起来即可。不难发现,这些子树的数目是 n 在二进制时 1 的个数,或者说是把n 展开
成 的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。
接着,我们考察这两种操作下标变化的规律:
首先看修改操作:
已知下标 i,求其父节点的下标。我们可以考虑对树从逻辑上转化:
如图,我们将子树向右对称翻折,虚拟出一些空白结点(图中白色),将原树转化成完全二叉树。
有图可知,对于节点 i,其父节点的下标与翻折出的空白节点下标相同。
因而父节点下标 p=i+2^k (2^k 是 i用 的幂方和展开式中的最小幂,即 i为根节点子树的规模)
即 p = i + i&(i^(i-1)) 。
接着对于求和操作:
因为每棵子树覆盖的范围都是 的幂,所以我们要求子树 i 的前一棵树,只需让 i减去 的最小幂
即可。即 p = i - i&(i^(i-1)) 。
至此,我们已经比较详细的分析了树状数组的复杂度和原理。
在最后,我们将给出一些树状数组的实现代码,希望读者能够仔细体会其中的细节。
【代码】
求最小幂 2^k:
int Lowbit(int t)
{
return t & ( t ^ ( t - 1 ) ) ;
}
求前 n 项和:
int Sum (int end)
{
int sum = 0 ;
while(end > 0)
{
sum += in[end];
end -= Lowbit(end);
}
return sum;
}
对某个元素进行加法操作:
void plus (int pos , int num)
{
while(pos <= n)
{
in[pos] += num;
pos += Lowbit(pos);
}
}
以下内容转 自OIBH
http://oibh.org/bbs/viewthread.php?tid= 3806&highlight=%CA%F7%D7%B4%CA%FD
%D7%E9
作者 sai901013
话说我学了这个树状数组 N 个 日子了,还没有用过,今天一用,果然不凡...-_-只是 PKUOJ 的
E 文让我比较抓狂...看了半天才知道它讲什么.这个题目乃入门题目,不赘述了...下面说一下我用树状
数组 的体会 (语言比较难理解,字体比较难忍受= =,请原谅)..
什么叫艺术
做了这个题目之后,我对算法的艺术性又有了进一步的认识。跟线段树的又长又臭相比,树状数
组可谓娇小玲珑。那简洁的代码,与Treap、并查集等数据结构一样的美丽,清秀。不仅如此,它有着
简单易用的特点,容易记忆。
最大的艺术就在于 k+=(-k)k 这个变化量的设计,我们可以看到, (-k)&k完全等价于 k&(k^(k-
1)),两个可以随便记一 个,不过显然第一个更容易记忆。虽然到现在我还不知道这两句是如何计算到
“将k 化为二进制数时末尾 0 的个数”(补充 :可以参考本贴#17)=。 =! ,但我很享受这一种奇特的
美妙。
什么叫速度
除了 insert()和 get()时间复杂度都是O(log n)之外,其实这个很正常,树状数组特别的是
它编程复杂度离奇的小-_-//... .據说它的时间复杂度常数还比线段树小很多。除此之外,它的空间复
杂度也比线段树明显小约 /3。下面分析一下我写的Code。
1.
2. //先定义 c[x]为树状数组,a[x]为原数组。
3.//其实这里叫remodify()函数比较合理,因为这里是修改某个元素而不是插入
4.//detal是将 a[k]变化 detal,而不是“变为detal”
5.//mix 是要十分注意的,因为常常有人会被输入个数N 所迷惑了
6.//因为修改c[k]的话对 c[k+1]....c[mix]有影响,所以不难想到 k 的变化为增变化
7.
8.void insert(int *c,int k,int detal){
9. for (; k<=mix ;k+=(-k)&k ) c[k] +=detal;
10. }
11.
12.//顾名思义,getsum()就 自然是某一段的和
13.//不过比较奇怪,返回的是a[1]...a[k]
14.//其实并不难为,如果你想要 a[j]...a[k],你可以 getsum(k)-getsum(j)
15.
16.int getsum(int *c,int k) {
17. int t;
18. for (t=0 ; k>0 ;k-=(-k)k ) t +=c[k];
19. return t;
20. }
复制代码
可能你看某幅图会容易搞清楚啥是树状数组 ,但是我不打算贴这一幅图片,因为百度一下就可以找
到。
---//这个这个....绝对不是我懒 !
算了,还是贴一下
:
什么要注意
世界上没有免费的午餐,你喜欢上树状数组 ,你就一定要承受它的缺点,-_-//貌似喜欢 MM 也一
样。
不过有一点我还没有绝对地搞清楚,有人说树状数组可以解“最长不下降子序列”问题,但这个应该会
涉及到求某一个区间范围内最值。但是貌似树状数组要符合减法原则,也就是说不能求某一个区间范围
内最值。初步来讲,我认为“树状数组解最长不下降子序列”和“减法原则”可能并不矛盾,可能有一
种符合 “减法原则”解法,我不做深入讨论探究了。
下面引用一段话:
在很多的情况下,线段树都可以用树状数组实现.凡是能用树状数组 的一定能用线段树.
当题目不满足减法原则的时候,就只能用线段树,不能用树状数组.例如数列操作如果让我们求出一段数
字中最大或者最小的数字,就不能用树状数组 了。
除了上面的“减法原则”之外,还需要说明的是:树状数组 由 1开始。
习惯用 C/C++的同志们申请地址的时候必须从 0 开始,自然而然地,我们习惯了第一个元素是 0,但是
树状数组必须由 1开始。具体细节想一下就会知道,那是因为跟那个“将 k 化为二进制数时末尾 0 的个
数”有关。
最后要说的是,干看是不行的,练习才是正道。 PKUOJ 352 Stars 是一道不错的入门题目。
可以参考以下代码。不过最好还是 自己做啦...-_-///..
本文个人原创,如有雷同,肯定抄我 sai90.!
此外本人语言是比较难理解的,若各位高手们看得不顺眼,请见谅...
1.
2. #include <stdio.h>
3. int c[32003];
4. int a[32003];
5. int n,mix=32003;
6.void insert(int k,int detal)
7. {
8. for (; k<=mix ;k+=(-k)k ) c[k] +=detal;
9. }
10.int getsum(int k)
11.{
12. int t;
13. for (t=0 ; k>0 ;k-=(-k)k ) t +=c[k];
14. return t;
15.}
16.int main()
17.{
18. memset (c,0,sizeof(c));
19. memset (a,0,sizeof(a));
20. int i,x,y;
21. scanf("%d",n);
. for (i=0; i<n ;i++)
3. {
4. scanf("%d%d",x,&y);
5. x++;
6. a[getsum(x)]++;
7. insert(x,1);
8. }
9. for (i=0; i<n ;i++) printf("%d\n",a);
30. return 0 ;
31.}
Just do it:
简单:
POJ 2299 Ultra-QuickSort
POJ 2352 Stars
POJ 1195 Mobile phones
中等
POJ 2155 Matrix
POJ 3321 Apple Tree
POJ 1990 MooFest
难题:
POJ 2464 Brownie Points II
浙公网安备 33010602011771号