树桩数组

一、使用场景


 

频繁修改场景下用于求前缀和 前缀积等(区间和可以通过前缀和计算而来)

查询和修改的时间复杂度都是O(logN)

 

二、原理


 

如求前缀和

(树桩数组只是存一段区间的统计值,如求前缀和就存这段区间的和;如果求出现次数就存这段区间的数出现的次数)

一个原始数组A 对应一个树桩数组C

 

 

 

C[1]=A[1]  (区间范围 :包含前1个数,包含本身)

C[2]=A1+A2 (区间范围 :包含前2个数)

C[3]=A3  (区间范围 :包含前1个数)

C[4]=A1+A2+A3+A4 (区间范围 :包含前4个数)

C[n] 肯定是包含A[n]的(假设数组从1开始)

 

线段树采用二分的思想,每个节点表示的范围不断二分

而树桩数组每个元素表示的区间范围(从当前位置往前算)是由 此数的二进制数计算而来

 

规则如下:第x个数表示的区间范围=2^k (k=二进制(x)末尾0的个数)

如第1个数: 二进制为 00000001 ,末尾0的个数为0,则区间范围=2^0=1

第2个数:二进制为00000010, 末尾0的个数为1,则区间范围=2^1=2

第3个数:二进制为00000011,末尾0的个数为1,则区间范围=2^0=1

第4个数:二进制为00000100,末尾0的个数为2,则区间范围=2^2=4

 

可以发现:

如果奇数的话 区间范围=1

 

而任何一个正整数都可以采用二进制表示如6=00000110=2^2+2^1=4+2 ;

则前6个数的区间值可以分解为前4个数的区间值 + 前2个数的区间值

 

这个规则牛人已经为我们可以

树桩数组第n个数C[n]表示的区间范围=2^k (k=二进制(n)末尾0的个数) = n& (-n)

 

可以定义函数lowbit(n) 表示区间范围

private int lowbit(int n) {
    return n & -n;
}

 

三、单点更新


修改某个节点 则对应的树桩数组受影响的元素都需要改变

如A[6]增加了1 ,则C[6]肯定也需要+1

下一个受影响区间只要包含第6个数,则一样需要更新,下一个受影响的是C[8],下下一个C[16]

规则:A[n]变换 需要变化的树桩数组节点如下

          C[n] C[n + logbit(n)] .. C[m] C[m+logbit(m)] .....

 

代码如下

void updata(int i,int k){    //在i位置加上k
     while(i <= n){
         c[i] += k;
        i += lowbit(i);
     }
 }

  

 

四、前缀和 积


 

求前n个数的和

preSum(n) = C[n] + C[n-lowbit(n)] + ...+C[m] + C[m-lowbit(m)] + ....+ C[1]

如preSum(6) = C[6] + C[4]

C[6] : 区间范围5-6的值

C[4]:区间范围1-4的值

加起来正好=前6个数的区间值

代码如下

int getsum(int i){        //求A[1 - i]的和
     int res = 0;
     while(i > 0){
         res += c[i];
         i -= lowbit(i);
     }
     return res;
}

  


 

 

五、矩阵和


 

二维树桩数组(三维 多维)

如下求区域的和 = preSum(5,6) - preSum(2,6) - preSum(5,3) + preSum(2,3)

 

 相关代码 2层循环

// 查询

int sum(int i, int j){

    int res = 0;

    for(int x = i; x; x -= lowBit(x))

        for(int y = j; y; y -= lowBit(y))

            res += C[x][y];

    return res;

}

 

// 修改

void change(int i, int j, int delta){

    for(int x = i; x < MAX_N; x += lowBit(x)) {

        for (int y = j; y < MAX_N; y += lowBit(y)) {

            C[x][y] += delta;

        }

    }

}

  

 

六、应用(树桩数组求逆序对)


 

求 1 3 8 5 2 数组有多少个逆序对

直接拿值最为数组下标定义A[8] 对应的树桩数组C[8]

循环遍历每个数m 对应的A[n]就add 1, 树桩数组也对应更新

则前n个数对应与当前数m相比的逆序对=总的数n - preSum(m)

伪代码

for(int i=1;i<=n;i++) {

		add(m);  

		ans+=i-sum(m); 

}

  

此时如果数组非常大 ,则需要先做离散化

如原数组为 1 1000 20000 4 9 有点数值比较大 直接以最大值作为数组下标不合适

可以把数据离散到固定的区间内 保持相对大小不变即可

一种可行的方法:

1、对原数组排序 并记录原来每个数组对应index

2、把排序好的数 直接用 1 2 3 .. 顺序替代,再按原index的位置顺序放入

 

原数组为 1 1000 20000 4 9-->则离散化之后的数组为 1 4 5 2 3

 

posted @ 2020-04-10 23:16  蓝天随笔  阅读(363)  评论(0编辑  收藏  举报