二维树状数组基础详解

<在此感谢原创作者>

当要频繁的对数组元素进行修改,同时又要频繁的查询数组内任一区间元素之和的时候,可以考虑使用树状数组.

通常对一维数组最直接的算法可以在O(1)时间内完成一次修改,但是需要O(n)时间来进行一次查询.而树状数组的修改和查询均可在O(log(n))的时间内完成.

一、回顾一维树状数组

假设一维数组为Ai,则与它对应的树状数组Ci是这样定义的:

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 
......

(1)C[t]展开以后有多少项?由下面公式计算:

int lowbit(int t){//计算c[t]展开的项数   
   return t&(-t);   
  }

C[t]展开的项数就是lowbit(t),C[t]就是从A[t]开始往左连续求lowbit(t)个数的和.

(2)修改

比如修改了A3,必须修改C3,C4,C8,C16,C32,C64… 
当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,对于节点i,父节点下标 p=i+lowbit(i)

//给A[i]加上 x后,更新一系列C[j]   
update(int i,int x){    
 while(i<=n){   
    c[i]=c[i]+x;    
    i=i+lowbit(i);    
     }    
}

(3)求数列A[ ]的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。

  如:
  Sun(1)=C[1]=A[1];
      Sun(2)=C[2]=A[1]+A[2];
      Sun(3)=C[3]+C[2]=A[1]+A[2]+A[3];
      Sun(4)=C[4]=A[1]+A[2]+A[3]+A[4];
      Sun(5)=C[5]+C[4];
      Sun(6)=C[6]+C[4];
      Sun(7)=C[7]+C[6]+C[4];
      Sun(8)=C[8];
      ......


int Sum(int n) //求前n项的和.   
{    
    int sum=0;    
    while(n>0)    
    {    
         sum+=C[n];    
         n=n-lowbit(n);    
    }        
    return sum;    
}  

lowbit(1)=1       lowbit(2)=2       lowbit(3)=1       lowbit(4)=4  
lowbit(5)=1       lowbit(6)=2       lowbit(7)=1       lowbit(8)=8  
lowbit(9)=1       lowbit(10)=2      lowbit(11)=1      lowbit(12)=4  
lowbit(13)=1      lowbit(14)=2      lowbit(15)=1      lowbit(16)=16  
lowbit(17)=1      lowbit(18)=2      lowbit(19)=1      lowbit(20)=4  
lowbit(21)=1      lowbit(22)=2      lowbit(23)=1      lowbit(24)=8  
lowbit(25)=1      lowbit(26)=2      lowbit(27)=1      lowbit(28)=4  
lowbit(29)=1      lowbit(30)=2      lowbit(31)=1      lowbit(32)=32  
lowbit(33)=1      lowbit(34)=2      lowbit(35)=1      lowbit(36)=4  
lowbit(37)=1      lowbit(38)=2      lowbit(39)=1      lowbit(40)=8  
lowbit(41)=1      lowbit(42)=2      lowbit(43)=1      lowbit(44)=4  
lowbit(45)=1      lowbit(46)=2      lowbit(47)=1      lowbit(48)=16  
lowbit(49)=1      lowbit(50)=2      lowbit(51)=1      lowbit(52)=4  
lowbit(53)=1      lowbit(54)=2      lowbit(55)=1      lowbit(56)=8  
lowbit(57)=1      lowbit(58)=2      lowbit(59)=1      lowbit(60)=4  
lowbit(61)=1      lowbit(62)=2      lowbit(63)=1      lowbit(64)=64  

二、树状数组可以扩充到二维。

问题:一个由数字构成的大矩阵,能进行两种操作 
1) 对矩阵里的某个数加上一个整数(可正可负) 
2) 查询某个子矩阵里所有数字的和,要求对每次查询,输出结果。

一维树状数组很容易扩展到二维,在二维情况下:数组A[][]的树状数组定义为:

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

例:举个例子来看看C[][]的组成。 
设原始二维数组为:

A[][]={
    {a11,a12,a13,a14,a15,a16,a17,a18,a19}, 
    {a21,a22,a23,a24,a25,a26,a27,a28,a29}, 
    {a31,a32,a33,a34,a35,a36,a37,a38,a39}, 
}; 

那么它对应的二维树状数组C[][]呢?

记:

  B[2]={a21,a21+a22,a23,a21+a22+a23+a24,a25,a25+a26,...} 这是第二行的一维树状数组 
  B[3]={a31,a31+a32,a33,a31+a32+a33+a34,a35,a35+a36,...} 这是第三行的一维树状数组 
  B[4]={a41,a41+a42,a43,a41+a42+a43+a44,a45,a45+a46,...} 这是第四行的一维树状数组 

那么:

C[1][1]=a11,C[1][2]=a11+a12,C[1][3]=a13,C[1][4]=a11+a12+a13+a14,c[1][5]=a15,C[1][6]=a15+a16,... 
   这是A[][]第一行的一维树状数组 

C[2][1]=a11+a21,C[2][2]=a11+a12+a21+a22,C[2][3]=a13+a23,C[2][4]=a11+a12+a13+a14+a21+a22+a23+a24, 
C[2][5]=a15+a25,C[2][6]=a15+a16+a25+a26,... 
   这是A[][]数组第一行与第二行相加后的树状数组 

C[3][1]=a31,C[3][2]=a31+a32,C[3][3]=a33,C[3][4]=a31+a32+a33+a34,C[3][5]=a35,C[3][6]=a35+a36,... 
   这是A[][]第三行的一维树状数组 

C[4][1]=a11+a21+a31+a41,C[4][2]=a11+a12+a21+a22+a31+a32+a41+a42,C[4][3]=a13+a23+a33+a43,... 
    这是A[][]数组第一行+第二行+第三行+第四行后的树状数组

搞清楚了二维树状数组C[][]的规律了吗? 仔细研究一下,会发现:

(1)在二维情况下,如果修改了A[i][j]=delta,则对应的二维树状数组更新函数为:

void add(int x,int y,int v)
{
    for(int i=x;i<=n;i += lowbit(i))      //n为数组大小
        for(int j=y;j<=n;j += lowbit(j))
            c[i][j] += v;
}

(2)在二维情况下,求子矩阵元素之和∑ a[i]j的函数为

int sum(int x,int y)
{
    int ans=0;
    for(int i=x;i>0;i -= lowbit(i))
        for(int j=y;j>0;j -= lowbit(j))
            ans+=c[i][j];
    return ans;
}


比如:
    sum(1,1)=C[1][1];  sum(1,2)=C[1][2]; sum(1,3)=C[1][3]+C[1][2];...
    sum(2,1)=C[2][1];  sum(2,2)=C[2][2]; sum(2,3)=C[2][3]+C[2][2];...
    sum(3,1)=C[3][1]+C[2][1]; sum(3,2)=C[3][2]+C[2][2]

以任意两点为左上和右下两个端点的子矩阵和

int ask(int x1,int y1,int x2,int y2)
{
    return sum(x2,y2)+sum(x1-1,y1-1)-sum(x1-1,y2)-sum(x2,y1-1);
}

和以为树状数组一样,我们依然借助sum去求。 
但是,我们看到,这个公式似乎很长,别急,别晕,听我解释一边即可明白。 
首先声明,我们保证x2>=x1,y2>=y1 
下面让我们先来看一个图: 

红色的矩形是我们要求的。 
我们这里为了和计算机里二维数组的保持一致,我们把x坐标视为纵坐标。 
首先sum(x2,y2)很显然是整个大矩形, 
sum(x1-1,y2)和sum(x2,y1-1)则是绿色和黄色的两个矩阵(不含红色边),很明显这是我们不要的,所以我们用大的矩阵减去这两个小矩阵。 
但是,减完以后我们会发现蓝色阴影部分的矩阵被减了两次,很明显减多了,所以我们还需要加上sum(x1-1,y1-1) 
这样就成了我给的公式。
 

参考博客:

https://blog.csdn.net/zzti_xiaowei/article/details/81053094

https://blog.csdn.net/cggwz/article/details/78420102

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用malloc函数申请二维动态数组需要注意以下几点: 1. 二维数组在内存中的存储是连续的一片区域,因此需要先分配一维数组的空间,再为每个一维数组分配空间。 2. 为了方便起见,可以先通过一维数组的形式定义二维数组的行和列。 下面是一个示例程序,演示如何使用malloc函数申请二维字符串数组: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { int i, j, rows, cols; char **str_array; printf("Enter the number of rows: "); scanf("%d", &rows); printf("Enter the number of columns: "); scanf("%d", &cols); // 分配一维数组的空间 str_array = (char **) malloc(rows * sizeof(char*)); // 分配每个一维数组的空间 for(i = 0; i < rows; i++) { str_array[i] = (char *) malloc(cols * sizeof(char)); } // 输入字符串数组 printf("Enter the strings:\n"); for(i = 0; i < rows; i++) { scanf("%s", str_array[i]); } // 输出字符串数组 printf("The strings are:\n"); for(i = 0; i < rows; i++) { printf("%s\n", str_array[i]); } // 释放空间 for(i = 0; i < rows; i++) { free(str_array[i]); } free(str_array); return 0; } ``` 这个程序首先要求用户输入二维数组的行和列,然后使用malloc函数分配一维数组和每个一维数组的空间。接下来,程序允许用户输入字符串数组,并输出结果。最后,程序释放动态分配的内存。 需要注意的是,在释放空间时,需要先释放每个一维数组的空间,再释放一维数组的空间。这是因为每个一维数组的空间是通过malloc函数分配的,因此必须先释放它们的空间,否则可能会导致内存泄漏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值