透析 树状数组

来源:互联网 发布:王宝强 日本 知乎 编辑:程序博客网 时间:2024/04/30 08:29


 

 

简单地研究了一下这个数据结构,在这做个总结摘要。

 

树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1...n],那么查询a[1] + …… + a[i] 的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。

 

来观察一下这个图:

 

令这棵树的结点编号为C1C2……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(其中kx二进制末尾0的个数)个元素。因为这个区间最后一个元素必然为Ax,所以很明显:

Cn = A(n – 2^k + 1) +…… + An

算这个2^k有一个快捷的办法,定义一个函数如下即可:

int lowbit(int x){return x & (x ^ (x – 1));}

查询

当想要查询一个SUM(n)时,可以依据如下算法即可:

Cn = A(n – 2^k + 1) +…… + An;

Sum(n) = A1+A2+A3+…+An = Cn+C[n-lowbit(n)]+C[(n-lowbit(n))-lowbit(n-lowbit(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的过程。

//修改过程必须满足减法规则!

 

所以整个程序如下:

int lowbit(int t){     return t & (t^(t-1)); }void modify(int pos,int num){    while (pos<=n) {        in[pos]+=num;        pos+=lowbit(pos);    }}int query(int end){    int sum=0;    while (end>0) {        sum+=in[end];        end-=lowbit(end);    }    return sum;}memset(in,0,sizeof(in)); //初始化for (i=1;i<=n;i++) modify(i,a[i]);

 

 

以下拿这次上海预选赛的一道题做为一个例子好了。

题意:求一个序列逆序数的奇偶性。

如果初看此题,大概要乐死了,不是很轻松?看看限制条件吧:n <= 1000,000,数为1...n

内存限制:5MB

嗯……这下好了,随便怎么做都超内存。

数状数组怎么解决呢?

首先来看一下一个数的逆序数怎么求?一个数的逆序数为排在其前面又比其大的数的个数总和。

那么可以这么做:每查到一个数时,总能知道现在已经插入了多少个数,利用树状数组,可以很轻松地知道比它小的数有多少个,那么这个数的逆序数不是手到擒来?

此题只求奇偶性,所以开一个布尔数组是最省空间的,读完所有数之后,答案也就出来了。

 

 

0 0