zoukankan      html  css  js  c++  java
  • 树状数组

    转载于:http://fqq11679.blog.hexun.com/21722866_d.html

    树状数组

                                      武钢三中   吴豪
    【引言】
              在解题过程中,我们有时需要维护一个数组的前缀和S[i]=A[1]+A[2]+...+A[i]。
              但是不难发现,如果我们修改了任意一个A[i],S[i]、S[i+1]...S[n]都会发生变化。
              可以说,每次修改A[i]后,调整前缀和S[]在最坏情况下会需要O(n)的时间。
              当n非常大时,程序会运行得非常缓慢。
              因此,这里我们引入“树状数组”,它的修改与求和都是O(logn)的,效率非常高。
    【理论】

              为了对树状数组有个形 象的认识,我们先看下面这张图。


      如图所示,红色矩形表示的数组C[]就是树状数组。
              这里,C[i]表示A[i-2^k+1]到A[i]的和,而k则是i在二进制时末尾0的个数,

    #include<iostream>
    #include<bitset>
    using namespace std;
    inline int lowbit(int t)
    {
    	return t&(-t); 
    }
    int main()
    {
    	for(int i=0;i<=10;i++)
    	{
    		bitset<4> da(i);
    		cout<<i<<" "<<da<<" "<<lowbit(i)<<endl;
    	}
    	return 0;
    }


              或者说是i用2的幂方和表示时的最小指数。
             ( 当然,利用位运算,我们可以直接计算出2^k=i&(i^(i-1)) )也就是i&(-i)
              同时,我们也不难发现,这个k就是该节点在树中的高度,因而这个树的高度不会超过logn。
              所以,当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,
              这个操作的复杂度在最坏情况下就是树的高度即O(logn)。  
              另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。
              不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,
              因此,求和操作的复杂度也是O(logn)。
              接着,我们考察这两种操作下标变化的规律:
              首先看修改操作:
              已知下标i,求其父节点的下标。
              我们可以考虑对树从逻辑上转化:

    如图,我们将子树向右对称翻折,虚拟出一些空白结点(图中白色),将原树转化成完全二叉树。
             有图可知,对于节点i,其父节点的下标与翻折出的空白节点下标相同。
             因而父节点下标 p=i+2^k  (2^k是i用2的幂方和展开式中的最小幂,即i为根节点子树的规模)
             即  p = i + i&(i^(i-1)) 。
             接着对于求和操作:
             因为每棵子树覆盖的范围都是2的幂,所以我们要求子树i的前一棵树,只需让i减去2的最小幂即可。
             即  p = i - i&(i^(i-1)) 。
             至此,我们已经比较详细的分析了树状数组的复杂度和原理。
             在最后,我们将给出一些树状数组的实现代码,希望读者能够仔细体会其中的细节。
    【代码】
      求最小幂2^k:

    int Lowbit(int t) 
    { 
        return t & ( t ^ ( t - 1 ) ); 
    } 

    求前n项和:
    int Sum(int end) 
    { 
        int sum = 0; 
        while(end > 0) 
        { 
            sum += in[end]; 
            end -= Lowbit(end); 
        } 
        return sum; 
    } 
    对某个元素进行加法操作: 
    void plus(int pos , int num) 
    { 
        while(pos <= n) 
        { 
              in[pos] += num; 
              pos += Lowbit(pos); 
        } 
    } 



    树状数组是一种非常优雅的数据结构.
     
    当要频繁的对数组元素进行修改,同时又要频繁的查询数组内任一区间元素之和的时候,可以考虑使用树状数组.
     
    最直接的算法可以在O(1)时间内完成一次修改,但是需要O(n)时间来进行一次查询.
    而树状数组的修改和查询均可在O(log(n))的时间内完成.
     
    假设a[1...N]为原数组,定义c[1...N]为对应的树状数组:
    c[i] = a[i - 2^k + 1] + a[i - 2^k + 2] + ... + a[i]
    其中k为i的二进制表示末尾0的个数,所以2^k即为i的二进制表示的最后一个1的权值.
    所以2^k可以表示为n&(n^(n-1))或更简单的n&(-n):
     
    int lowbit(int n)
    {return n& (-n);}
     
    对a[n]进行修改后,需要相应的修改c数组中的p1, p2, p3...等一系列元素
    其中p1 = n,
           pi+1 = pi + lowbit(pi)
    所以修改原数组中的第n个元素可以实现为:
    void Modify(int n, int delta)
    {
        while(n <= N)
        { c[n] += delta; n += lowbit(n);}
    }
     
    当要查询a[1],a[2]...a[n]的元素之和时,需要累加c数组中的q1, q2, q3...等一系列元素
    其中q1  = n,
           qi+1 = qi - lowbit(qi)
    所以计算a[1] + a[2] + .. a[n]可以实现为:
    int Sum(int n)
    {
         int result = 0;
         while(n != 0)
         { result += c[n]; n -= lowbit(n); }
        return result;
    }





    树状数组可以扩充到二维。在二维情况下:

    C[x][y] = ∑ a[i][j], 其中,
    x-lowbit(x) + 1 <= i <= x,
    y-lowbit(y) + 1 <= j <= y.

    在二维情况下,对应的更新和查询函数为:
    void Modify(int x, int y, int delta)
    {
        for(int i = x; i <= N; i += lowbit(i))
           for(int j = y; j <= N; j += lowbit(i))
              C[x][y] += delta;
    }

    int Sum(int i, int j)
    {
        int result = 0;
        for(int x = i; x > 0; x -= lowerbit(x))
        {
            for(int y = j; y > 0; y -= lowerbit(y))
            {
                result += C[x][y];
            }
        }

        return result;
    }




    树状数组可以解决更新线段区间,查询某个点的问题。
    在这种情况下,更新线段区间和查询点的时间复杂度仍然为O(logn).

    设A[1,N]为我们要处理的数组。
    另外设置一个数组B[1,N],使得B[1] + B[2] + .. B[i] = A[i], 其中1 <= i <= N.
    即B[i] = A[i] - A[i-1]

    当要查询A[i]的时候,我们只需在数组B上使用一般的树状数组操作查询区间[1,i]即可
    当我们要给区间A[a~b]加上delta时,只需要在数组B上对B[a]进行一般树状数组的更新delta的操作,同时对数组B上对B[b+1]进行 - delta操作。

    这种使用方式同样可以扩展到二维。
    二维情况下,
    B[i][j] = A[i][j] - A[i-1][j] - A[i][j-1]
    当要查询A[i][j]的时候,只需在数组B上查询B[0][0]到B[i][j]的和即可。
    当要给矩形A[x1][y1],A[x2][y2]加上delta的时候,只需要在数组B上对B[x1][y1], [x2+1][y2+1]进行一般树状数组的更新delta的操作,同时对B[x1][y2+1], B[x2+1][y1]进行一般树状数组的更新-delta的操作即可。

  • 相关阅读:
    又是一年叶落时(二)
    动态规划 之 区间DP练习
    [hdu2255] 奔小康赚大钱
    [洛谷P1967] 货车运输
    [UVA1494] Qin Shi Huang's National Road System
    斜率优化总结
    latex一些有用的写法
    [YTU]_2384 ( 矩形类中运算符重载【C++】)
    [YTU]_2442( C++习题 矩阵求和--重载运算符)
    [YTU]_2640( 编程题:运算符重载---矩阵求和)
  • 原文地址:https://www.cnblogs.com/lgh1992314/p/5835048.html
Copyright © 2011-2022 走看看