1166-敌兵布阵(树状数组)

来源:互联网 发布:php java des加密解密 编辑:程序博客网 时间:2024/06/05 12:31

http://www.cnblogs.com/justforgl/archive/2012/07/27/2612364.html

现在来说明下树状数组是什么东西?假设序列为A[1]~A[8]


网络上面都有这个图,但是我将这个图做了2点改进。

(1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。

(2)C[i]为A[i]对应的那一列的最高的节点。

现在告诉你:序列C[]就是树状数组。

那么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”,那么拥有2^x个叶子,为序列A[]中的第i-2^x+1到第i个元素的和。

终于,我们得到了一个C[i]的具体定义:

C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。

第03讲 利用树状数组求前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]


现在直接告诉你结论: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的值。


x 为 i 的二进制的最后一位1之后0的个数

假如 i=7 ,二进制0000 0111 ,那么x=0,s[7]=c[7]+c[7-2^0]+...

               i=6             0000 0110          x=1   s[7]=c[7]+c[7-2^0]+c[6-2^1]     

               i=4             0000 0100          x=2    s[7]=c[7]+c[7-2^0]+c[6-2^1]+c[4-2^2]

               s=c[7]+c[6]+c[4];

               i -= 4-2^2;  //不满足i>0的条件退出循环      

                

int Sum(int i) //返回前i个元素和

{

       int s=0;

       while(i>0)

       {

              s+=C[i];

              i-=i&(-i);

       }

       return s;

}

第04讲 更新C[](更新原理同上)

正如第01讲提到的小石块问题,如果数组A[i]被更新了怎么办?那么如何改动C[]?

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

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



如上图:

假如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”的左子树和右子树的叶子数是相同的。 因为2^x代表的就是叶子数,所以C[3]的父亲是A,A的父亲是C[i+2^0],即C[3]改变,那么C[3+2^0]也改变。

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

推广到一般情况就是:

如果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 <bits/stdc++.h>
using namespace std;
int a[10005];
int n;
int lowbit(int t)
{
    return t&(-t);
}
void insert(int t, int d)
{
    while(t<=n)
    {
        a[t]+=d;
        t+=lowbit(t);
    }
}
int getsum(int t)
{
    int sum = 0;
    while(t>0)
    {
        sum+=a[t];
        t-=lowbit(t);
    }
    return sum;
}
int main() {
    int T, i, j, k, t;
    scanf("%d", &T);
    t = 0;
    while(T--) {
        memset(a, 0, sizeof(a));
        scanf("%d", &n);
        for(i = 1; i <= n; i++) {
            scanf("%d", &k);
            insert(i, k);
        }
        char str[10];
        scanf("%s", str);
        printf("Case %d:\n", ++t);
        while(strcmp(str, "End") != 0) {
            int x, y;
            scanf("%d%d", &x, &y);
            if(strcmp(str, "Query") == 0) {
                printf("%lld\n", getsum(y) - getsum(x-1));
            }
            else if(strcmp(str, "Add") == 0) {
                insert(x, y);
            }
            else if(strcmp(str, "Sub") == 0) {
                insert(x, (-1) * y);
            }
            scanf("%s", str);
        }
    }
    return 0;
}


原创粉丝点击