数据结构——树状数组篇

Posted WuliWuliiii

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构——树状数组篇相关的知识,希望对你有一定的参考价值。

原地址:My CSDN Blog  那边还未通过审核,先看这边的吧…… 

  线段树是一个很好的维护区间关系的这样的一个数据结构,但是,很多时候我们可以用更小空间、更快速度(更大尺寸呢、,全景天窗,五菱宏光?)的数据结构来维护一个前缀关系。

 ?技术图片技术图片??

  上面的这张图是表示的一个有8个叶子节点的线段树,接下去,我们给它进行一个变形:

?技术图片技术图片?? 

然后呢,我们把2、4、6、8、……这样的元素推到最顶端的空上边去,想让2表示1~2这段区间,让4表示1~4这段区间,让6表示5~6这段区间,让8表示1~8这段区间。

然后,这张图就变成了这样:

 ?技术图片技术图片??

那么,我们知道了2、4、6、8可以表示的区间,诶~那其他的区间(前缀关系)呢!古人(比我老的都是古人!)想到了一种很巧妙的解决的方法,我们来举例说明:
我们想求1~1的时候,是不是就是A[1];

想求1~3的时候,是不是A[3]和A[2]两个这样的关系式,因为A[3]只表示3~3这个区间,如图;

想求1~5的时候,我们就要用A[5]、A[4]这两个关系式了;

求1~6的时候,要用A[6]、A[4]这两个式子了;

1~7呢,是A[7]、A[6]、A[4]这三个关系的统筹了。

从中,我们能否找到什么规律?

  首先,我们知道树状数组仍然是跟线段树一样是一棵二叉搜索树(BIT),性质仍然没有改变。那么,我们不妨溯本求源从二进制的角度来思考问题。

我们将这8个数都转换成二进制来看,

1 = (0001)—— Q[1] = A[1];

2 = (0010)—— Q[2] = A[2];

3 = (0011)—— Q[3] = A[3] + A[2];

4 = (0100)—— Q[4] = A[4];

5 = (0101)—— Q[5] = A[5] + A[4];

6 = (0110)—— Q[6] = A[6] + A[4];

7 = (0111)—— Q[7] = A[7] + A[6] + A[4];

8 = (1000)—— Q[8] = A[8];

从中我们是不是可以发现,我们想求1~x的一个前缀的关系,是不是就可以把x看成二进制再进行求解:看到7 = (0111)是不是可以把7看成Q[7] = Q[ (0111) ] = A[ (0111) ] + A[ (0110) ] + A[ (0100) ]这样的一个关系,我们不难发现,对于7是不是每次去除的都是其二进制上的最后一位1,直到为0停止运算,譬如(0111)->(0110)我们去除了最末尾的1,(0110)->(0100)也是去除了最末尾的那个1。

Problem:怎么去除某数最末尾的那个1?
  Q1:从0开始跑一遍for(i, 0~31)循环判断到某一位"(x>>i) & 1"是不是为1?——好浪费时间的说~(留给……的时间不多了!)(脱口而出!

  Q2:new algorithm?新的算法?不是新的哦~是一个我们平时没有关注到的一个小玩意!

方法一:

  如何消除二进制中最后一个位1?利用n &= (n - 1)就可以做到。(手动模拟下下哦,31为二进制好长的说呢QAQ)(雾

方法二:

  这时候就要回到C语言的课本知识了,我们知道负数的补码是怎样计算的呢?

  举个例子:6 = (0110),那么(-6)=(1001 + 1)=(1010)

  也就是先对所有位置取反,然后再"+1"这样的一个过程。那么,好巧喔,是不是发现“x&(-x)”就是我们所要删除的最后一位的1的对应的值?

给个好听的名字吧,就叫lowbit()吧!
这里还有一道还不错的题!lowbit()的「藏起来

#define lowbit(x) ( x&(-x) )

 

  我习惯把它放在头文件里边,因为真的很常用的!

到这里,我们是不是知道了大致上该怎样去查找,那么,更新哩,一开始的树状数组是不是空的?所以,我们需要去更新它!

看到图片,再手动模拟一下给大家看看,我们要更新以下的序号,那么哪几位是需要改变的呢?(我们把图放下来继续看着图)

 

1(0001)—— A[1]、A[2]、A[4]、A[8];

2(0010)—— A[2]、A[4]、A[8];

3(0011)—— A[3]、A[4]、A[8];

4(0100)—— A[4]、A[8];

5(0101)—— A[5]、A[6]、A[8];

6(0110)—— A[6]、A[8];

7(0111)—— A[7]、A[8];

8(1000)—— A[8];

  在这里,我们不难发现,更新的时候与查询相反,我们是去给最低位的1上加上1,然后不断的向前进位,就可以把对应的关系我们存进去。

  这里给出一个求前缀和的模板(My Code):

#define lowbit(x) ( x&(-x) )
int A[maxN], N, M;    //maxN是根据题目的呢
inline void add(int x, int v)    //第x位,给它加上v的权
{
    if(!x) return;    //没有这个有可能会死循环哦,牢记!!!
    while(x <= N)
    {
        A[x] += v;
        x += lowbit(x);
    }
}
inline int query(int i)    //查询1~i的和
{
    int ans = 0;
    while(i)
    {
        ans += A[i];
        i -= lowbit(i);
    }
    return ans;
}

 

对了,就像Code里面讲的那样,我们更新的时候,如果没有:

if(!x) return;


是有可能会死循环的(有些地方会改变0处的值,我们得想办法去处理它的!)。

为什么会死循环!?是因为lowbit(x)也是0呀。然后就永远永远的加不到N了(雾

 

好啦。树状数组的基本操作就讲到这里叭,自我感觉良好~(膨胀ing???)

 ?技术图片技术图片??

真的没有彩蛋了,别往下看了。

没彩蛋了…… 

没彩蛋了。

没了哦。。

 

 

以上是关于数据结构——树状数组篇的主要内容,如果未能解决你的问题,请参考以下文章

树状数组整理

树状数组总结篇

[POI2015]LOG

树状数组

树状数组 小白篇暨区间修改

树状数组