从一道水题来从头介绍树状数组

树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。特别用于在数组内的参数变换后,再次求和所使用的时间复杂度减少问题。
下面是一道水题:POJ上的stars,题目大意为:按照纵坐标y从小到大的顺序依次输入几个星星的横纵坐标,横纵坐标小于等于该星星的星星数量即为该星星的等级,求每个等级的星星数量。
由于输入的顺序是按照纵坐标从小到大的顺序(y相同时x的坐标也是由小到大输出),那么一个星星的满足条件的星星一定在输入的这个星星的前方,故在这道题中,y坐标是没有用处的,我们可以每次输入一个x值,就判断之前输入过的数中,小于x的星星的个数,例如:输入坐标:
5 1
1 2
2 2
3 2
那么第三个数据之前有第二和第三两个符合条件(横坐标是1,2都小于5),所以第三颗星星的等级是2,但是如果按照这种方法逐个统计的话,在星星的数量达到一定数量时,遍历一次并且每个都判断一次的话,是一定超时的,所以,可以想到,开辟一个数组ar,下标1:ar[1]代表的是在输入到当前时,横坐标为1的个数,ar[2]即是横坐标为2的星星的个数,依次类推,加入当前输入的横坐标是4,那么结果就是ar[0]+ar[1]+ar[2]+ar[3]+arr[4](纵坐标按顺序输入,那么则表明已经输出的星星纵坐标一定小于等于当前纵坐标,只要横坐标满足条件则整体一定满足),结果保留,并将ar[4]++。
但是,如果当前输入的横坐标n达到了数万位,那么还要继续从ar[0]遍历求和到ar[n]吗?当然是行不通的,所以,又需要将该模型进行优化。
我们的方法是:如果每间隔一段距离,一个arr数空间存放的就是前面的一段数组的和是不是会简便很多?那样就不需要每次都使用遍历所有的节点来求和了,至少会遍历较少的数,那么,这就涉及到了树状数组,不妨回忆一下二叉树的原型,最上面一个根结点,下面每个节点都展开分支,我们假设每个叶子节点代表ar[i],他们的根结点来记录和,那么整棵树的根节点就是和了,想想是不是方便了很多?


如果当前横坐标是3,那么只要输出C的值,这样减少了很多遍历的时间,但是同时存在的问题是,如果当前横坐标是0的话,还可以输出ar[0],一的话输出A,那么如果2的话呢?输出的又该怎么处理呢?还有应该怎样在输出的时候顺利得找到A,或C点呢?树状数组的处理将会解决这些问题:
下面是从网上转载的一些关于树状数组的详细推断,以二进制的形式方便的找到每一个范围内的非叶子节点:

假设树状数组C[],序列为A[1]~A[8]
网络上面都有这个图,但是我将这个图做了2点改进。


(1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。
(2)C[i]为A[i]对应的那一列的最高的节点。
那么C[]如何求得?
C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
C[6]=A[5]+A[6];
C[7]=A[7];
C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:

因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i]=C[i]的所有叶子的和。
把具体的数标上不知道有没有好点


现在不得不引出关于二进制的一个规律:

先仔细看下图:


将十进制化成二进制,然后观察这些二进制数最右边1的位置:

1 --> 00000001

2 --> 00000010

3 --> 00000011

4 --> 00000100

5 --> 00000101

6 --> 00000110

7 --> 00000111

8 --> 00001000


1的位置其实从我画的满二叉树中就可以看出来。但是这与C[]有什么关系呢?

接下来的这部分内容很重要:
在满二叉树中,

以1结尾的那些结点(C[1],C[3],C[5],C[7]),其叶子数有1个,所以这些结点C[i]代表区间范围为1的元素和;

以10结尾的那些结点(C[2],C[6]),其叶子数为2个,所以这些结点C[i]代表区间范围为2的元素和;

以100结尾的那些结点(C[4]),其叶子数为4个,所以这些结点C[i]代表区间范围为4的元素和;

以1000结尾的那些结点(C[8]),其叶子数为8个,所以这些结点C[i]代表区间范围为8的元素和。

扩展到一般情况:
i的二进制中的从右往左数有连续的x个“0”,那么拥有2x个叶子,为序列A[]中的第i-2x+1到第i个元素的和。

终于,我们得到了一个C[i]的具体定义:
C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

利用树状数组求前i个元素的和S[i]
理解了C[i]后,前i个元素的和S[i]就很容易实现。
从C[i]的定义出发:

C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。
我们可以知道:C[i]是肯定包括A[i]的,那么:
S[i]=C[i]+C[i-2^x]+…

也许上面这个公式太抽象了,因为有省略号,我们拿一个具体的实例来看:

S[7]=C[7]+C[6]+C[4]

因为C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]

(1)i=7,求得x=0,那么我们求得了A[7];
(2)i=i-2^x=6,求得x=1,那么求得了A[6]+A[5];
(3)i=i-2^x=4,求得x=2,那么求得了A[4]+A[3]+A[2]+A[1]。

也就是说,每个C[i]所存放的是2^x个数据的和,或者说,每个C[i]内和的下标在 i~i-2x+1,下一个C[i-1]的第一个数为i-2x,C[i-1]=C[i-2x],将i=i-2x,不断递归,结果则为总的和。

讲到这里其实有点难度,因为S[i]的求法,如果要讲清楚,那么得写太多的东西了。所以不理解的同学,再反复多看几遍。

从(1)(2)(3)这3步可以知道,求S[i]就是一个累加的过程,如果将2^x求出来了,那么这个过程用C++实现就没什么难度。

现在直接告诉你结论:2^x=i&(-i)
证明:设A’为A的二进制反码,i的二进制表示成A1B,其中A不管,B为全0序列。那么-i=A’0B’+1。由于B为全0序列,那么B’就是全1序列,所以-i=A’1B,所以:

i&(-i)= A1B& A’1B=1B,即2^x的值。

所以根据(1)(2)(3)的过程我们可以写出如下的函数:

int Sum(int i) //返回前i个元素和
{
       int s=0;
       while(i>0)
      {
              s+=C[i];
              i-=i&(-i);
       }
       return s;
}

更新C[]
当有星星不断输入,那么A的数目也会随之改变,所以,如果数组A[i]被更新了怎么办?那么如何改动C[]?

如果改动C[]也需要O(n)的时间复杂度,那么树状数组就没有任何优势。所以树状数组在改动C[]上面的时间效率为O(logn),为什么呢?

因为改动A[i]只需要改动部分的C[]。这一点从图中就可以看出来:


如上图:
假如A[3]=3,接着A[3]+=1,那么哪些C[]需要改变呢?

答案从图中就可以得出:C[3],C[4],C[8]。因为这些值和A[3]是有联系的,他们用树的关系描述就是:C[3],C[4],C[8]是A[3]的祖先。

那么怎么知道那些C[]需要变化呢?
我们来看“A”这个结点。这个“A”结点非常的重要,因为他体现了一个关系:A的叶子数为C[3]的2倍。因为“A”的左子树和右子树的叶子数是相同的。 因为2x代表的就是叶子数,所以C[3]的父亲是A,A的父亲是C[i+20],即C[3]改变,那么C[3+2^0]也改变。

我们再来看看“B”这个结点。B结点的叶子数为2倍的C[6]的叶子数。所以B和C[6+21]在同一列,所以C[6]改变,C[6+21]也改变。

推广到一般情况就是:
如果A[i]发生改变,那么C[i]发生改变,C[i]的父亲C[i+2^x]也发生改变。
这一行的迭代过程,我们可以写出当A[i]发生改变时,C[]的更新函数为:

void Update(int i,int value)  //A[i]的改变值为value
{
       while(i<=n)
       {
              C[i]+=value;
              i+=i&(-i);
       }
}

经过以上推断,我们便可以据此得到该题的具体代码实现了:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <math.h>
#include <string.h>
#define lowbit(x)  (x&(-x))
#define MMAX 15005
#define NMAX 32005
using namespace std;

int C[NMAX];

int sum(int i)
{
    int ans=0;
    while (i>0)
    {
        ans+=C[i];
        i-=lowbit(i);
    }
}

void add(int num,int i)
{
    while (i<=NMAX)
    {
        C[i]+=num;
        i+=lowbit(i);
    }
}


void main()
{
    int arr[MMAX]={0};
    int i,j,k,n,m,x,y;
    scanf("%d",&n);
    m=n;
    while (n--)
    {
        scanf("%d%d",&x,&y);
        arr[sum(++x)]++;
        add(1,x);
    }
    for (i=0;i<=m;i++)
        printf("%d\n",arr[i]);
}

以上是一维树状数组的详解,关于二维的树状数组,则与一维的相似,主要在于元素数组为一个二维的A[i][j],相对应的是,树状数组同样对应的一个二维数组C[i][j],不同的是C[i][j]代表的是0到i行和0到j列的和。
下面是在网络上找到的相关注解:

一维树状数组很容易扩展到二维,在二维情况下:
数组A[][]的树状数组定义为:
C[x][y] = ∑ a[i][j], 其中,
x-lowbit(x) + 1 <= i <= x,
y-lowbit(y) + 1 <= j <= y.
例:举个例子来看看C[][]的组成。
设原始二维数组为:A[][]={{a11,a12,a13,a14,a15,a16,a17,a18,a19},
{a21,a22,a23,a24,a25,a26,a27,a28,a29},
{a31,a32,a33,a34,a35,a36,a37,a38,a39},
{a41,a42,a43,a44,a45,a46,a47,a48,a49}};
那么它对应的二维树状数组C[][]呢?
记: B[1]={a11,a11+a12,a13,a11+a12+a13+a14,a15,a15+a16,...}
这是第一行的一维树状数组
B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,...}
这是第二行的一维树状数组
B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,...}
这是第三行的一维树状数组
B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,...}
这是第四行的一维树状数组
那么:
C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a1
6,...
这是A[][]第一行的一维树状数组
C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a2
2+a23+a24,
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,...
这是A[][]数组第一行与第二行相加后的树状数组
C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a3 6,...
这是A[][]第三行的一维树状数组
C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,...
这是A[][]数组第一行+第二行+第三行+第四行后的树状数组
搞清楚了二维树状数组C[][]的规律了吗?
仔细研究一下,会发现:

(1)在二维情况下,如果修改了A[i][j]=delta,
则对应的二维树状数组更新函数为:
private void Modify(int i, int j, int delta){
A[i][j]+=delta;
for(int x = i; x< A.length; x += lowbit(x))
for(int y = j; y <A[i].length; y += lowbit(y)){
C[x][y] += delta; } }

(2)在二维情况下,求子矩阵元素之和∑ a[i]j的函数为
int Sum(int i, int j){
int result = 0;
for(int x = i; x > 0; x -= lowbit(x)) {
for(int y = j; y > 0; y -= lowbit(y)) {
result += C[x][y]; } }
return result; }
比如:
Sun(1,1)=C[1][1]; Sun(1,2)=C[1][2]; Sun(1,3)=C[1][3]+C[1][2];...
Sun(2,1)=C[2][1]; Sun(2,2)=C[2][2]; Sun(2,3)=C[2][3]+C[2][2];...
Sun(3,1)=C[3][1]+C[2][1]; Sun(3,2)=C[3][2]+C[2][2];

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容