树状数组
来源:互联网 发布:信息化管理平台软件 编辑:程序博客网 时间:2024/05/01 21:47
树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n],
那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,
支持随时修改某个元素的值,复杂度也为log级别。
来观察这个图:
令这棵树的结点编号为C1,C2...Cn。令每个结点的值为这棵树的值的总和,那么容易发现:
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
...
C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16
这里有一个有趣的性质:
设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。因为这个区间最后一个元素必然为Ax,
所以很明显:Cn = A(n – 2^k + 1) + ... + An
算这个2^k有一个快捷的办法,定义一个函数如下即可:
int lowbit(int x){
// return x&(x^(x–1));
return x&(-x);
}
当想要查询一个SUM(n)时,可以依据如下算法即可:
step1: 令sum = 0,转第二步;
step2: 假如n <= 0,算法结束,返回sum值,否则sum = sum + Cn,转第三步;
step3: 令n = n – lowbit(n),转第二步。
可以看出,这个算法就是将这一个个区间的和全部加起来,为什么是效率是log(n)的呢?以下给出证明:
n = n – lowbit(n)这一步实际上等价于将n的二进制的最后一个1减去。而n的二进制里最多有log(n)个1,所以查询效率是log(n)的。
那么修改呢,修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。
所以修改算法如下(给某个结点i加上x):
step1: 当i > n时,算法结束,否则转第二步;
step2: Ci = Ci + x, i = i + lowbit(i)转第一步。
i = i +lowbit(i)这个过程实际上也只是一个把末尾1补为0的过程。
对于数组求和来说树状数组简直太快了!
典题分析
【例】数列操作
给定一个初始值都为0的序列,动态地修改一些位置上的数字,加上一个数,减去一个数,或者乘上一个数,然后动态地提出问题,问题的形式是求出一段数字的和.
【算法分析】
如果直接做的话,修改的复杂度是O(1),询问的复杂度是O(N),M次询问的复杂度是M*N.M,N的范围可以有100000以上,所以这样做会超时,但是如果用线段树的话,还是很不错的!
线段树解法分析:
可以看出,这棵树的构造用二分便可以实现.复杂度是2*N.
每个结点用数组a来表示该结点所表示范围内的数据之和.
修改一个位置上数字的值,就是修改一个叶子结点的值,而当程序由叶子结点返回根节点的同时顺便修改掉路径上的结点的a数组的值.
对于询问的回答,可以直接查找i..j范围内的值,遇到分叉时就兵分两路,最后在合起来.也可以先找出0..i-1的值和0..j的值,两个值减一减就行了.后者的实际操作次数比前者小一些.
这样修改与维护的复杂度是logN.询问的复杂度也是logN,对于M次询问,复杂度是MlogN.
然而不难发现,线段树的编程复杂度大,空间复杂度大,时间效率也不高!!!!
所以我们来想用树形数组来实现:
那么,何为树形数组呢??
下图中的C数组就是树状数组,a数组是原数组;
对于序列a,我们设一个数组C定义C[t] = a[t – 2^k + 1] + … + a[t],k为t在二进制下末尾0的个数。
K的计算可以这样:
2^k=t and (t xor (t-1))
以6为例
(6)10=(0110)2
xor 6-1=(5)10=(0101)2
(0011)2
and (6)10=(0110)2
(0010)2
所以问题变的很简单,重要写几个函数就可以了;
求2^k的函数代码如下:
int Lowbit(int t)
{
return t & ( t ^ ( t - 1 ) );
}
Function Lowbit(t: longint): longint;
Begin
Lowbit := t and (t xor (t - 1));
End;
求1 -- end和的函数代码如下:
int Sum(int end)
{
int sum = 0;
while (end > 0)
{
sum += in[end];
end -= Lowbit(end);
}
return sum;
}
Function Sum(tail: longint): longint;
Var
s: longint;
Begin
s:=0;
while tail > 0 do
begin
inc(s, in[tail]);
dec(tail, Lowbit(tail));
end;
sum := s;
End;
对某位进行操作函数如下(以加法为例)
void plus(int pos , int num)
{
while(pos <= n)
{
in[pos] += num;
pos += Lowbit(pos);
}
}
Procedure plus(p, num: longint);
Begin
while p <= n do
begin
inc(in[p], num);
inc(p, Lowbit(p));
end;
End.
有了这三个函数整个树形数组也就基本构建成功啦!!
对于刚才的一题,每次修改与询问都是对C数组做处理.空间复杂度有3N降为N,时间效率也有所提高.编程复杂度更是降了不少.
对于lowbit可有优化 P :lowbit:=X AND(not x); C: lowbit:=x &(-x);
与线段树的比较
树状数组是一个可以很高效的进行区间统计的数据结构。在思想上类似于线段树,比线段树节省空间,编程复杂度比线段树低,但适用范围比线段树小。
以简单的求和为例。设原数组为a[1..N],树状数组为c[1..N],其中c[k] = a[k-(2^t)+1] + ... + a[k]。比如c[6] = c[5] + c[6]。也就是说,把k表示成二进制1***10000,那么c[k]就是1***00001 + 1***00010 + ... + 1***10000这一段数的和。设一个函数lowestbit(k)为取得k的最低非零位,容易发现,根据上面的表示方法,从a[1]到a[k]的所有数的总和即为sum[k] = c[k] + c[k-lowestbit(k)] + c[k-lowestbit(k)-lowestbit(k-lowestbit(k))] + ... 于是可以在logk的时间内求出sum[k]。当数组中某元素发生变化时,需要改动的c值是c[k],c[k+lowestbit(k)], c[k+lowestbit(k)+lowestbit(k+lowestbit(k))] ... 这个复杂度是logN (N为最大范围)
扩展到多维情况:以二维为例,用c[k1][k2]表示c[k1-(2^t1)+1][k2-(2^t2)+1] + ... + c[k1][k2]的总和。可以用类似的方法进行处理。复杂度为(logn)^k (k为维数)
树状数组相比线段树的优势:空间复杂度略低,编程复杂度低,容易扩展到多维情况。劣势:适用范围小,对可以进行的运算也有限制,比如每次要查询的是一个区间的最小值,似乎就没有很好的解决办法。
多维情况的几道题目:
POJ 2155 Matrix
URAL 1470 UFOs
其中POJ 2155是一道很不错的题目,表面上看,这题的要求似乎和树状数组的使用方法恰好相反,改变的是一个区间,查询的反而是一个点。实际上可以通过一个转化巧妙的解决。
首先对于每个数A定义集合up(A)表示{A, A+lowestbit(A), A+lowestbit(A)+lowestbit(A+lowestbit(A))...} 定义集合down(A)表示{A, A-lowestbit(A), A-lowestbit(A)-lowestbit(A-lowestbit(A)) ... , 0}。可以发现对于任何A<B,up(A)和down(B)的交集有且仅有一个数。
于是对于这道题目来说,翻转一个区间[A,B](为了便于讨论先把原问题降为一维的情况),我们可以把down(B)的所有元素的翻转次数+1,再把down(A-1)的所有元素的翻转次数-1。而每次查询一个元素C时,只需要统计up(C)的所有元素的翻转次数之和,即为C实际被翻转的次数。
实际实现时,由于只考虑奇偶,因此无须统计确切的翻转次数。另外,如果翻转up(A)和up(B+1),查询down(C),也是同样的效果。这种方法可以很容易地扩展到二维情况。比起线段树、四分树之类的常规思路,无论编程复杂度还是常数速度上都有很大优势。
树状数组(Binary Indexed Tree)是又一种静态的树结构。它的首要用途是用于维护前缀和,也即:一数组a[1..n],随时会改变其中某a[i],还会询问s[i]=a[1]+a[2]+…+a[i],树状数组可完美解决这一问题。
定义数组c[0..n],其中c[i]=a[i-2^k+1]+a[i-a^k+2]+…+a[i],其中k为i在二进制下末尾0的个数。当我们改变一个a[i]时,会有很多c[i]随之改变;若需查询某个s[i],需要累加多个c[i]。好在确定需要改变或累加的元素都可以用比较简便的方法得出,这方法的核心就是lowbit值。
定义lowbit(x)=x&(x^(x-1)),它相当于将最右边的1左边的东西全部去掉。若需改变a[i],则c[i]、c[i+lowbit(i)]、c[i+lowbit(i)+lowbit(i+lowbit(i)]……就是需要改变的c数组中的元素。若需查询s[i],则c[i]、c[i-lowbit(i)]、c[i-lowbit(i)-lowbit(i-lowbit(i))]……就是需要累加的c数组中的元素。这看上去有些玄妙,我觉得其实也可以不用透彻理解。
一维的树状数组的每个操作的复杂度都是O(logn)的,非常高效。它可以扩充为n维,这样每个操作的复杂度就变成了O((logn)^n),在n不大的时候仍然完全可以接受。扩充的方法就是将原来改变和查询的函数中的一个循环改成嵌套的n个循环在n维的c数组中操作。
要注意树状树组能处理的是下标为1..n的数组,绝对不能出现下标为0的情况。因为lowbit(0)=0,这样会陷入死循环。对于我这个从来都用C语言思考的家伙来说,这一点格外需要注意。
似乎树状数组也可以用来解决一些与前缀和关联不大的问题,例如NOI2004的cashier,但我还不太会(那题我只会用平衡树或线段树或虚二叉树解)。
示例程序:ural1470.cpp(三维的树状数组)
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- 树状数组
- C# string 的==和Equals的比较
- 网页中嵌入Yahoo Yui日历
- 读后感:麦田守望者--走出软件作坊:三五个人十来条枪 如何成为开发正规军(四十三)
- 收缩强连通分量
- 关于css 以及网站导航条 杂记
- 树状数组
- 孙鑫java无难事视频-日志(第7讲)
- 平衡二叉查找树
- 求最大流的使用距离标号的最短增广路算法
- jdk1.5新特性
- 去除浮躁 静一些 再静一些
- 求最大流的Relabel-to-Front算法
- 求最大权二分匹配的KM算法
- POJ 1119 Start Up the Startup