算法学习笔记:树状数组(一维、二维)

前置知识:

lowbit()函数:

取出一个数二进制形式下的最低位的1及其后面的0

例如:lowbit(40)=lowbit(101000)=1000=8

具体实现方式:

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

40 = 0101000

-40 = 1011000

(40) & (-40) = 1000 = 8        

其实可以略微理解一下,因为负数在计算机里面是用补码存储,取反加一,正好就是把最后一个1和后面的0保留,前面的所有位都取反了。

树状数组结构:

树状数组结构图:选自 【精选】树状数组(详细分析+应用),看不懂打死我!-CSDN博客

可以看到,数组下标其所包含的和的对应关系是:t[i]包含 i-lowbit(i)+1 ~ i 的全部和。

可以从二进制的角度理解一下,这种方案可以表示所有1-i的区间,因为把i转化为二进制后可以一段一段的把最后的一个1消除掉

例如需要表示a[1]~a[7]的和:

t[7] = t[0111] = a[7]                                        // 7-lowbit(7)+1 = 7

t[6] = t[0110] = a[5] + a[6]                              // 6-lowbit(6)+1 = 5

t[4] = t[0100] = a[1] + a[2] + a[3] + a[4]          // 4-lowbit(4)+1 = 1

a[1]~a[7]的和 = t[4] + t[6] + t[7]

t[7]在这里负责涵盖最后一个1的计值范围,也就是111~111和。

t[6]在这里负责涵盖101~110的和,也就是倒数第二个1的范围。

t[4]在这里负责涵盖001~100的和,也就是倒数第三个1的范围。

每一段都是负责把最后一个1消除掉的范围。这样就可以表示所有1~i的区间。

所以只要理解了这个原理,就能知道为什么要lowbit函数了。


一维树状数组:

单点修改、区间查询:

树状数组为t[i],维护a[i]的和

单点修改:

要实现单点修改,从本质上我们可以理解为:当需要修改a[i]的值的时候,我们要把所有包含a[i]的t[i]都修改一遍。

在前置知识树状数组结构中我们理解了t数组的包含关系,那么我们更新的算法就能轻易的写出来:

//a[index]+e
void add(int index,int e){
    a[i] += e;
    for(int i=index; i<=n; i+=lowbit(i))
        t[i] += e;
}

还是拿一个例子来说明,假设我要把a[3]+5:

t数组的更新顺序如下:

t[3] = t[0011] += 5   // t[3] = a[3]

t[4] = t[0100] += 5   // 3+lowbit(3)=4    t[4] = a[1] + a[2] + a[3] + a[4]

t[8] = t[1000] += 5   // 4+lowbit(4)=8    t[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8]

在所有的t中,只有这三个元素是包含了a[3]的,其他都不包含。

对于t[i]而言,包含他且离他最近的元素一定是t[i+lowbit(i)],且不存在一个t[k]只包含t[i]的一部分。

区间查询:

树状数组只能直接查找1-i这种形式的区间和,但是可以在这个基础上进行修改:

比如我需要j~i这个区间的和,我可以先查询sum(1 ~ j-1),再查询sum(1 ~ i),然后用sum(1 ~ i) - sum(1 ~ j-1),得出的结果就是j~i区间的和了。

可以写出求和代码:

//求a[1]~a[index]的和
int sum(int index){
    int ans = 0;
    for(int i=index; i; i-=lowbit(i))
        ans += t[i];
    return ans;
}

求和原理可以看前置知识:树状数组结构。

区间修改、单点查询:

区间修改:

如果需要用树状数组实现区间修改,我们需要引入差分的概念:

d[i] 是差分数组,对应a[i] 有如下关系:

d[i] = a[i] - a[i-1]

d[i] 的用处是实现以下关系:a[i] = \sum_{1}^{i}d[i]

例如:

a[1~8] = {1,2,3,4,5,6,7,8}

d[1~8] = {1,1,1,1,1,1,1,1}

可以很快的发现这个关系:a[i] = \sum_{1}^{i}d[i] 是成立的

这时候我们如果把d[3] + 1

d[1~8] = {1,1,2,1,1,1,1,1}

再用 a[i] = \sum_{1}^{i}d[i] 这个公式去计算a[i]可以得出:

a[1~8] = {1,2,4,5,6,7,8,9}

我们会惊喜的发现:a[3~8]都被加1了,这就是差分数组的作用,可以把一段区间同时变化,但只用修改一次。

但是如果我不想把a[3~8]都加1怎么办呢?我只想把a[3-5]加1。

我们只要把a[6-8]再加上-1不就可以了吗?

根据之前的原理:我们只要把d[6] - 1就可以实现了!

d[1~8] = {1,1,2,1,1,0,1,1}

推出a:

a[1~8] = {1,2,4,5,6,6,7,8}

至此,我们已经明白了差分的原理:a[i] = \sum_{1}^{i}d[i]

这与我们之前:单点修改、区间查询 的功能不是不谋而合吗?

我们只要维护d数组的前缀和(前缀和就是指d[1] + d[2] + ... + d[i])就能得到a[i],而且,如果我们想要修改某一段a[i~j] 只要维护d数组中的两个点就可以了(d[i] 和 d[j+1])。

我们可以给出区间修改的算法:

void add(int index,int e){
    for(int i=index; i<=n; i+=lowbit(i))
        d[i] += e;
}

//a[i~j] + e
void change(int i,int j,int e){
    add(i,e);
    add(j+1,-e);
}

这样维护d数组后就可以实现d的前缀和就是a的值。

单点查询:

根据之前的理解,单点查询 a[i] = \sum_{1}^{i}d[i]

算法如下:

//查询a[index]的值
int find(int index){
    int ans = 0;
    for(int i=index; i;i-=lowbit(i))
        ans += d[i];
    return ans; 
}

区间修改,区间查询:

区间修改:

因为需要实现区间查询,那么之前的区间修改方案肯定就不适用了,我们只能利用差分的思想,另辟蹊径:

接下来是一串数学推导:

在 区间修改,单点查询 中我们有:a[i] = \sum_{1}^{i}d[i]

那么:\sum_{1}^{i}a[i] = \sum_{1}^{i}\sum_{1}^{i}d[i]

推出:

\sum_{1}^{i}a[i] =i*\sum_{1}^{i}d[i] - \sum_{1}^{i}(i-1)*d[i]

所以我们需要维护两个前缀和:

M=\sum_{1}^{i}d[i]

N=\sum_{1}^{i}(i-1)d[i]

\sum_{1}^{i}a[i] = i*M-N

所以根据以上内容,综合上一节,我们可以给出区间修改的算法:

void add(int index,int e){
    for(int i=index; i<=n; i+=lowbit(i)){
        d[i][0] += e;                //d[i]
        d[i][1] += (index-1)*e;      //(i-1)*d[i]
    }
}

//a[i~j] + e
void change(int i,int j,int e){
    add(i,e);
    add(j+1,-e);
}

区间查询:

根据之前的公式,我们可以得出区间查询的方案:

 M=\sum_{1}^{i}d[i]

N=\sum_{1}^{i}(i-1)d[i]

\sum_{1}^{i}a[i] = i*M-N

//查询d[index][num]的前缀和
int find(int index, int num){
    int ans = 0;
    for(int i=index; i;i-=lowbit(i))
        ans += d[i][num];
    return ans; 
}

//查询a[i]的前缀和
int sum(int i){
    return (i)*find(i,0) - find(i,1)    //i*M-N
}

二维树状数组:

 当一维树状数组拓展到二维时:

t[i][j] = 

a[1][1] + a[1][2] + a[1][3] + ··· +a[1][j] +

a[2][1] + a[2][2] + a[2][3] + ··· +a[2][j] +

a[3][1] + a[3][2] + a[3][3] + ··· +a[3][j] +

···

a[i][1] + a[i][2] + a[i][3] + ··· +a[i][j] 

可以发现t[i]从一个变成个了一个树状数组,可以理解为t是一个大的树状数组,其中每个元素t[i]也是一个树状数组,对于小树状数组t[i],其中的元素t[i][j]是一个整数。

单点修改、区间查询:

单点修改:

可以从一维进行类比推理,一样是只修改受影响的元素

具体介绍在代码注释中体现:

//a[i][j] + e
void add(int index_i,int index_j,int e){
    for(int i=index_i; i<=n; i+=lowbit(i)){      //枚举受影响的树状数组t[i]
        for(int j=index_j; j<=m; j+=lowbit(j)){  //枚举修改树状数组t[i]中受影响的元素
            t[i][j] += e;
        }
    }
}

区间查询:

//求a[1][1]~a[index_i][index_j]的和
int sum(int index_i,int index_j){
    int ans = 0;
    for(int i=index_i; i; i-=lowbit(i))
        for(int j=index_j; j; j-=lowbit(j))
            ans += t[i][j];
    return ans;
}

二维下如果要求某个特定的区间,和一维相比更加复杂一些:

例如我们需要求(a,b) 到 (i,j) 这个区间的和,但我们只有从(1,1)开始到任意点的和:记作sum(i,j)。

这时我们可以得出(减1是因为边界也是要算进和里面的):

(a,b) 到 (i,j) 这个区间的和 = sum(i,j)-sum(a-1,j)-sum(i,b-1)+sum(a-1,b-1)

为什么要加上sum(a-1,b-1)呢?因为我们在-sum(a-1,j)-sum(i,b-1)的时候减去了两个sum(a-1,b-1),要补回来一个。

所以,求任意区间和的算法为:

//求(x1,y1)~(x2,y2)的区间和
int find(int x1,int y1,int x2,int y2){
    return sum(x2,y2) - sum(x1-1,y2) - sum(x2,y1-1) + sum(x1-1,y1-1);
}

区间修改、单点查询:

区间修改:

区间修改还是逃不出差分的思想,但是二维下的差分和一维相比略有区别:

如果要增加a[i][j] ~ a[x][y]的值那么对d数组的操作为:

d[i][j] + e

d[x][y+1] - e

d[x+1][y] - e

d[x+1][y+1] + e

可以结合之前的知识思考一下,还是很好理解的。

可以得出算法为:

void add(int index_i,int index_j,int e){
    for(int i=index_i; i<=n; i+=lowbit(i))
        for(int j=index_j; j<=m; j+=lowbit(j))
            d[i][j] += e;
}

//a[x1][y1]~a[x2][y2] + e
void change(int x1,int y1,int x2,int y2,int e){
    add(x1,y1,e);
    add(x2+1,y2,-e);
    add(x2,y2+1,-e);
    add(x2+1,y2+1,e);
}

单点查询:

//查询a[x][y]的值
int find(int x,int y){
    int ans = 0;
    for(int i=x; i; i-=lowbit(i))
        for(int j=y; j; j-=lowbit(j))
            ans += d[i][j];
    return ans; 
}

区间修改、区间查询:

区间修改:

a[i][j] = \sum_{1}^{i}\sum_{1}^{j}d[i][j]

\sum_{1}^{i}\sum_{1}^{j}a[i][j] = \sum_{1}^{i}\sum_{1}^{j}\sum_{1}^{i}\sum_{1}^{j}d[i][j]

所以我们需要维护四个前缀和:

M = \sum_{1}^{i}\sum_{1}^{j}d[i][j]

N = \sum_{1}^{i}\sum_{1}^{j}(j-1)d[i][j]

P = \sum_{1}^{i}\sum_{1}^{j}(i-1)d[i][j]

Q = \sum_{1}^{i}\sum_{1}^{j}(i-1)(j-1)d[i][j]

\sum_{1}^{i}\sum_{1}^{j}a[i][j] = i*j*M - i*N -j*P + Q

void add(int index_i,int index_j,int e){
    for(int i=index_i; i<=n; i+=lowbit(i)){
        for(int j=index_j; j<=n; j+=lowbit(j)){
            d[i][j][0] += e;                              //d[i]
            d[i][j][1] += (index_j-1)*e;                  //(j-1)*d[i]
            d[i][j][2] += (index_i-1)*e;                  //(i-1)*d[i]
            d[i][j][3] += (index_i-1)*(index_j-1)*e;      //(i-1)*(j-1)*d[i]
        }
    }
}

//a[x1][y1]~a[x2][y2] + e
void change(int x1,int y1,int x2,int y2,int e){
    add(x1,y1,e);
    add(x2+1,y2,-e);
    add(x2,y2+1,-e);
    add(x2+1,y2+1,e);
}

区间查询:

//查询d[index_i][index_j][num]的前缀和
int find(int index_i,inde index_j,int num){
    int ans = 0;
    for(int i=index_i; i; i-=lowbit(i))
        for(int j=index_j; j; j-=lowbit(j))
            ans += d[i][j][num];
    return ans; 
}

//查询a[i][j]的前缀和
int sum(int i,int j){
    //i*j*M - i*N -j*P + Q
    return (i*j)*find(i,j,0) - i*find(i,j,1) - j*find(i,j,2) + find(i,j,3)    
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值