树状数组初见与理解

欢迎进入博客浏览
>>我的博客<<
更好的排版,更好的阅读体验

树状数组初见与理解

我在第一次接触树状数组的时候,学习使用的博客是: Xenny-树状数组详解
博客本身已经将树状数组讲解的很好了,不过在二叉树抽象成树状数组的过程部分没有说的特别详细,我结合了一些自己的理解,认为这步抽象的过程还是需要仔细考虑一下的,毕竟是树状数组的原理,方便日后理解时候,便写了这篇博客。

树状数组初见,如有任何理解或文章方面的错误,希望大佬指正交流

a. 什么是树状数组?

树状数组,简单的说就是用数组的形式模拟数的结构。树状数组代码写起来简单,用起来方便,在不少问题上都可以用树状数组代替线段树,简直是居家必备小能手(误)

b. 树状数组可以解决的问题?

在这里,我们讨论的问题主要有,树状数组的原理与实现,树状数组的应用,包括:

  • 区间查询
  • 区间更新,单点查询
  • 区间更新,区间查询
  • 利用树状数组求逆元

当我们对树状数组有一个大概的认知之后,我们来详细了解一下树状数组。

1. 树状数组是什么样子?

我们已经说了,树状数组是用来模拟树的一种方法,那么模拟的是什么样子的数呢?答案很显然是二叉树,在我们学习二叉树的时候,也经常使用数组来实现二叉树,我们现在所提到的树状数组有什么不同呢?他的树结构是这样的:

这是一种非常常见的二叉树,每个节点都可以存储他对应子节点的和,但可以很直观的看到,他并不能满足我们要实现的数组形式,那么我们就需要对这棵树进行一点点修改:

对于每个子节点(1~8),我们摸出了对应竖轴上重复的节点,只保留了最高节点,这样就将刚刚那样的一棵树,从x轴的方向上看,变成了一维模型。现在我们只要找出保留节点与之前节点的关系,就可以完全的用树状数组来实现树形结构了。

观察图形,我们发现,每个节点仍然存储他所有子节点(图中最底层灰色节点)的和,但表示成数列的1~8号节点不再与子节点一一对应,而是包括了许多原二叉树结点的值,通过图中信息,我们肯可以得到:

Node[1] = a[1]Node[5] = a[1]
Node[2] = a[1] + a[2]Node[6] = a[5] + a[6]
Node[3] = a[3]Node[7] = a[7]
Node[4] = a[1] + a[2] + a[3] + a[4]Node[8] = a[1] + a[2] + … + a[8]

可以发现节点和的表示形式与二进制的表示形式息息相关,观察发现:

  • 2的二进制表示为:0010

  • 4的二进制表示为:0100

  • 5的二进制表示为:0101

  • 6的二进制表示为:0110

观察每个数字末尾的0的个数,即可发现末尾0的个数就表示这对应数字的节点在树上的高度(从最底层子节点为0开始计算)

而每个节点表示的数字和,可以发现:

  • 2的二进制表示为:0010,表示2个数字的和,1~2。

  • 4的二进制表示为:0100,表示4个数字的和,1~4。

  • 5的二进制表示为:0101,表示1个数字的和,5。

  • 6的二进制表示为:0110,表示2个数字的和,5~6。

很明显的发现,表示数字的和也与末尾连续的0的个数有关。原因很简单,因为这棵树本身是一颗二叉树,当你忽略了组成树的部分节点时,二叉树逢二进一的原则还是不会改变的,所以不论是高度还是宽度(表示数字的和)都是可以计算的。
N o d e [ x ] = a [ x ] + a [ x − 1 ] + ⋯ + a [ x − 2 k − 1 ] 共 2 k 项 , 其 中 k 为 数 字 x 末 尾 0 的 数 量 Node[x] = a[x]+a[x-1]+\cdots+a[x-2^k-1] \\共2^k项,其中k为数字x末尾0的数量 Node[x]=a[x]+a[x1]++a[x2k1]2kkx0

2. Lowbit

当我们发现可每个节点的表示规律之后,下一个重要的点就是,如何快速的找到末尾零串?各路神仙发明了一种方法,叫做lowbit

lowbit的实现很简单:

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

简单的一行函数就可以求得特定数字x最后的数字串。为什么呢?lowbit巧妙地利用了补码的性质,可以发现的是,在大多数编程语言中,数字以补码的形式存储,一个数字对应的相反数,可以通过二进制各位取反后加1得到。

那么对于奇数,末尾为1,取反为0,加1为1,除了末尾外的所有位置均未因为加1发生改变,补码与原码仍是互为相反数,那么结果就是1。

对于偶数来书,末尾为长度不等的0串,以10010100为例,末尾的100取反后变成011,加一后变回100,剩余位置与奇数一样,并没有因为加1发生改变。

所以,通过补码与原码的一次且操作,我们就可以的到数字末尾的串,既节点x表示的数字范围。

3. 计算求和

当我们知道了每个点表示的范围之后,怎么利用刚刚的一些列规律求特定的一个范围的数字和呢?

我们以26为例,26的二进制表示为11010,我们将11010按步骤变换成以下几个过程:

11010 --> 11000 --> 10000

再写出这几部分对应节点的数字标识和:

  • 11010:十进制26,表示两个数字,25,26的和。
  • 11000:十进制24,表示8个数字,17~24的和。
  • 10000:十进制16,表示16个数字,1~16的和。

很显然,这三个数字加到一起就可以表示126所有数字的和。那么规律就出现了,当我们求124的和时,既可以写成:

// 得到1~i的所有值,其中a[i]为树状数组。
int getSum(int i) {
    int sum = 0;
    while(i>0) {
        sum += a[i];
        i -= lowbit(i);
    }
    return sum;
}

4. 数组的构建与单点更新。

离我们构造出完整的树状数组只差一步了,那就是如何对a[i]的值进行修改。

由于在树状数组中,a[i]的值是简介的存储在Node节点中的,观察图片可发现,图中a[3]的值不仅仅存在与Node[3]中:

因此,当我们想要修改a[3]的值时,我们只要顺藤摸瓜,从3开始,找到所有包括a[3]的节点,并将a[3]增加的值加进去就好了。跟二进制仅为的方法一样,我们同样可以推出,只要从指定节点i出发,一步一步的加入末尾串表示的值,就可以找到所有包括i的节点:

// 在a[i]的位置上,加上数值x,其中a[i]为树状数组。
int update(int i, int x) {
    while(i<=n) {
        a[i] += x;
        i += lowbit(i);
    }
}

5. 树状数组模板题:

洛谷 P3374

题目描述就是树状数组,直接贴代码:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define REP(i, lim) for(int i=0;i<lim;++i)
#define REPP(i, lim) for(int i=1;i<=lim;++i)
#define DEC(i, lim) for(int i=lim;i>=1;--i)
#define FOR(i,l,r)  for(int i=l;i<r;++i)
#define deBug cout<<"==================================="<<endl;
#define clr(s) memset(s, 0, sizeof(s))
#define lowclr(s) memset(s, -1, sizeof(s))
const int MAXN = 1000055;
const int inf = 0x3f3f3f3f;
const double eps = 1e-8;

int n, m;
ll a[MAXN];

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

int update(int i, ll x) {
    while(i<=n) {
        a[i] += x;
        i += lowbit(i);
    }
}
ll getSum(int i) {
    ll sum = 0;
    while(i>0) {
        sum += a[i];
        i -= lowbit(i);
    }
    return sum;
}

int main()
{
//    freopen("in.txt", "r", stdin);
//    freopen("out.txt", "w", stdout);

    scanf("%d%d", &n, &m);

    int val;
    REPP(i, n) {
        scanf("%d", &val);
        update(i, val);
    }

    int t, x, y;
    REPP(i, m) {
        scanf("%d%d%d", &t, &x, &y);
        if(t==1) update(x, y);
        else{
            ll ans = getSum(y) - getSum(x-1);
            printf("%lld\n", ans);
        }
    }

    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值