【专题】树状数组(完整版)

       树状数组是对一个数组改变某个元素和求和比较实用的数据结构。两中操作都是O(logn)。 

传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。

给定序列(数列)A,我们设一个数组C满足

C[i] = A[i–2^k+ 1] + … + A[i]

其中,k为i在二进制下末尾0的个数,i从1开始算!

则我们称C为树状数组。

下面的问题是,给定i,如何求2^k?

答案很简单:2^k=i&(i^(i-1)) ,也就是i&(-i)    为什么呢?? 请看下面:

整数运算 x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。
       因为:x &(-x) 就是整数x与其相反数(负号取反)的按位与:1&1=1,0&1 =0, 0&0 =1。具体分析如下:
       □ 当x为0时,x&(-x) 即 0 & 0,结果为0;
       □ 当x不为0时,x和-x必有一个为正。不失一般性,设x为正。
       ●当x为奇数时,最后一个比特为1,取反加1没有进位,故x和-x除最后一位外前面的位正好相反,按位与结果为0。最后一位都为1,故结果为       1。
       ●当x为偶数,且为2的m次方(m>0)时,x的二进制表示中只有一位是1(从右往左的第m+1位),其右边有m位0,左边也都是0(个数由表示   x的字        节数决定),故x取反加1后,从右到左第有m个0,第m+1位及其左边全是1。这样,x& (-x) 得到的就是x。 
       ●当x为偶数,却不为2的m次方的形式时,可以写作x= y * (2^k)。其中,y的最低位为1。实际上就是把x用一个奇数左移k位来表示。这时,x的   二进制         表示最右边有k个0,从右往左第k+1位为1。当对x取反时,最右边的k位0变成1,第k+1位变为0;再加1,最右边的k位就又变成了0,第   k+1位因为进         位的关系变成了1。左边的位因为没有进位,正好和x原来对应的位上的值相反。二者按位与,得到:第k+1位上为1,左边右边都为   0。结果为2^k,即         x中包含的2的最大次方的因子。
        总结一下:x&(-x),当x为0时结果为0;x为奇数时,结果为1;x为偶数时,结果为x中2的最大次方的因子。 比如x=32,其中2的最大次方因子  为                 2^5,故x&(-x)结果为32;当x=28,其中2的最大次方因子为4,故x & (-x)结果为4。当x=24,其中2的最大次方因子为8,故 x&(-x)结果为  8。

下面进行解释:

以i=6为例(注意:a_x表示数字a是x进制表示形式):

(i)_10 = (0110)_2

(i-1)_10=(0101)_2

i xor (i-1) =(0011)_2

i and (i xor (i-1))  =(0010)_2

2^k = 2

C[6] = C[6-2+1]+…+A[6]=A[5]+A[6]

数组C的具体含义如下图所示:

当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。

树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。


下面是别人总结的题目和代码(借鉴一下):

hdu 1541 Stars

题意:略。

思路:

树状数组经典入门题。

ans[i]: the amount of stars of the level i;

sum[i]: 横坐标为x的点,满足的the amount of the stars;

注意的地方:

(1)题目所给的点已经排好序了。

(2)由于x可能取0,而lowbit(0)=0,故add(0,1)会死循环。这就是为什么我一开始TLE的原因。所以将所有的 x++.

const int MAX1 = 15555, MAX2 = 32222;
int ans[MAX1], sum[MAX2], n,x,y;

int lowbit(int x){
    return x & (-x);
}

int getsum(int pos){
    int ret = 0;
    while(pos > 0){
        ret += sum[pos];
        pos -= lowbit(pos);
    }
    return ret;
}

void add(int pos, int num){
    while(pos < MAX2){
        sum[pos] += num;
        pos += lowbit(pos);
    }
}

int main()
{
    while(scanf("%d", &n) != EOF){
        memset(sum, 0, sizeof(sum));
        memset(ans, 0, sizeof(ans));
        FOR(i,1,n){
            scanf("%d%d", &x, &y);
            x++;   //注意加1,不然会在add(0,1)处死循环
            ans[getsum(x)]++;
            add(x, 1);
        }
        FOR(i,0,n-1)
            printf("%dn", ans[i]);
    }
	return 0;
}



poj 2182 Lost Cows 
题意:有一个序列a:1,2,…,N(2 <= N <= 8,000). 现该序列为乱序,已知第i个数前面的有a[i]个小于它的数。求出该序列的排列方式。
思路:由后向前推。易知最后一个数的真实值为a[N]+1。将a[N]+1在序列中删去,更新a[i],那么第N-1个数的真实值为a[N-1]+1。由此类推。
由于数据范围较小,用两层for循环的简单方法就可以解决。
这里给出树状数组的解法:


const int MAX = 8010;
int a[MAX], n, cnt[MAX], ans[MAX];

int lowbit(int x){
    return x & (-x);
}
void add(int pos, int val){
    while(pos <= n){
        cnt[pos] += val;
        pos += lowbit(pos);
    }
}
int sum(int pos){
    int res = 0;
    while(pos > 0){
        res += cnt[pos];
        pos -= lowbit(pos);
    }
    return res;
}

//二分找到第一个等于x的位置
int binary_search(int x){
    int low = 1, high = n, mid;
    while(low <= high){
        mid = (low + high) >> 1;
        int k = sum(mid);
        if(k >= x) high = mid-1;
        else low = mid+1;
    }
    while(sum(mid) < x) mid++;
    return mid;
}

int main()
{
    while(scanf("%d", &n) != EOF){
        a[1] = 0;
        FOR(i,2,n) scanf("%d", &a[i]);
        memset(cnt, 0, sizeof(cnt));
        FOR(i,1,n) add(i,1);
        for(int i = n; i >= 1; i--){
            ans[i] = binary_search(a[i]+1);
            add(ans[i], -1);
        }
        FOR(i,1,n) printf("%d\n", ans[i]);
    }
	return 0;
}



 poj 2481 Cows

题意:两个区间:[Si, Ei] and [Sj, Ej].(0 <= S < E <= 105). 若 Si <= Sj and Ej <= Ei and Ei – Si > Ej – Sj, 则第i个区间覆盖第j个区间。给定N个区间(1 <= N <= 10^5),分别求出对于第i个区间,共有多少个区间能将它覆盖。

思路:初看好像挺复杂的。其实可以把区间[S, E]看成点(S, E),这样题目就转化为hdu 1541 Stars。只是这里是求该点左上方的点的个数。

虽然如此,我还是WA了不少,有一些细节没注意到。给点排序时是先按y由大到小排序,再按x由小到大排序。而不能先按x排序。比如n=3, [1,5], [1,4], [3,5]的例子。另外还要注意对相同点的处理。


const int MAX = 100010;

struct Node{
    int x, y, id, ans;
}seq[MAX];
int sum[MAX], n;

int cmp1(const void *n1, const void *n2){
    int res = ((Node*)n2)->y - ((Node*)n1)->y;
    if(res == 0) return ((Node*)n1)->x - ((Node*)n2)->x;
    else return res;
}
int cmp2(const void *n1, const void *n2){
    return ((Node*)n1)->id - ((Node*)n2)->id;
}

int lowbit(int x){
    return x & (-x);
}
void add(int pos, int val){
    while(pos < MAX){         //我这里总是习惯性的写成n,浪费了很多时间。其实是横坐标x的最大范围。
        sum[pos]+=val;
        pos+=lowbit(pos);
    }
}
int getsum(int pos){
    int res = 0;
    while(pos>0){
        res+=sum[pos];
        pos-=lowbit(pos);
    }
    return res;
}

int main()
{
    while(scanf("%d", &n) && n){
        FOR(i,1,n){
            scanf("%d%d", &seq[i].x, &seq[i].y);
            seq[i].x++, seq[i].y++;
            seq[i].id = i;
        }
        qsort(seq+1, n, sizeof(Node), cmp1);
        memset(sum, 0, sizeof(sum));
        seq[1].ans = 0;
        add(seq[1].x, 1);
        int fa = 1;
        FOR(i,2,n){
            if(seq[i].x == seq[fa].x && seq[i].y == seq[fa].y){
                seq[i].ans = seq[fa].ans;
            }else{
                fa = i;
                seq[i].ans = getsum(seq[i].x);
            }

            add(seq[i].x, 1);
        }
        qsort(seq+1, n, sizeof(Node), cmp2);
        printf("%d", seq[1].ans);
        FOR(i,2,n) printf(" %d", seq[i].ans);
        printf("\n");
    }
	return 0;
}


poj 2155 Matrix
二维树状数组经典题
题意:给一个N*N的矩阵,里面的值不是0,就是1。初始时每一个格子的值为0。
现对该矩阵有两种操作:(共T次)
1.C x1 y1 x2 y2:将左上角为(x1, y1),右下角为(x2, y2)这个范围的子矩阵里的值全部取反。
2.Q x y:查询矩阵中第i行,第j列的值。
(2 <= N <= 1000, 1 <= T <= 50000)
思路:参见国家集训队论文:武森《浅谈信息学竞赛中的“0”和“1”》
1. 根据这个题目中介绍的这个矩阵中的数的特点不是 1 就是 0,这样我们只需记录每个格子改变过几次,即可判断这个格子的数字。
2. 先考虑一维的情况:
若要修改[x,y]区间的值,其实可以先只修改 x 和 y+1 这两个点的值(将这两个点的值加1)。查询k点的值时,其修改次数即为 sum(cnt[1] + … + cnt[k])。
3. 二维的情况:
道理同一维。要修改范围[x1, y1, x2, y2],只需修改这四个点:(x1,y1), (x1,y2+1), (x2+1,y1), (x2+1,y2+1)。查询点(x,y)的值时,其修改次数为 sum(cnt[1, 1, x, y])。
4. 而区间求和,便可用树状数组来实现。

const int MAX = 1010;
int n, cnt[MAX][MAX];

int lowbit(int x){
    return x & (-x);
}
void add(int x, int y, int val){
    for(int i = x; i <= n; i += lowbit(i))
        for(int j = y; j <= n; j += lowbit(j))
            cnt[i][j] += val;
}
int sum(int x, int y){
    int res = 0;
    for(int i = x; i > 0; i -= lowbit(i))
        for(int j = y; j > 0; j -= lowbit(j))
            res += cnt[i][j];
    return res;
}

int main()
{
    int t, m, x1, y1, x2, y2;
    char op[10];
    cin >> t;
    while(t--){
        scanf("%d%d", &n, &m);
        memset(cnt, 0, sizeof(cnt));
        while(m--){
            scanf("%s%d%d", op, &x1, &y1);
            if(op[0] == 'C'){
                scanf("%d%d", &x2, &y2);
                add(x1, y1, 1);
                add(x1, y2+1, 1);
                add(x2+1, y1, 1);
                add(x2+1, y2+1, 1);
            }else{
                printf("%d\n", sum(x1,y1) % 2);
            }
        }
        printf("\n");
    }
}

树状数组到这里就结束了。

祝大家分区赛加油,也祝我努力去打好酱油!!!!!!

你可能感兴趣的:(ACM算法总结,Algorithm(艺术))