【ACM摸鱼 】简单理解树状数组

本博客源自学长前辈的教解,并参考网络资源。侵权则删,如有错误劳驾指出。问题精深仅一知半解望海涵。



闻名

树状数组:
分析名字:   他是一个类似树形的 数组
作用:     降低复杂度: 频繁对于区间查询和单点修改的操作,或者是单点查询和区间修改的操作。
时间复杂度:  修改和查询都是log(n),相对普通数组修改 o(1) 区间求和是o(n)。
区别线段树:  结构简单(线性结构和树形)代码量少,简洁,能够实现部分线段树的功能。

初见

我们想要实现快速的区间求和。如果用树形,这样的存储方式可能更直观一些:这里写图片描述

(x,y)表示的是 数组[x]到数组[y]的和。 这种存储方式叫做线段树。线段树虽然功能多,但总归是树形,难免操作繁多冗长。而树状数组用数组实现了类似树的功能,而且方式巧妙。

线段树的下标编码规则是由上而下、从左至右依次编码。左孩子和右孩子的和 是他们父亲节点的值。然而对于每个右孩子节点的值,我们总能通过其父亲节点值减去左兄弟节点值来计算。可以说 即使没有右节点,也丝毫不影响我们求解对应区间的区间和。这样既能节省这部分空间,又不会将原来的问题复杂化
这里写图片描述

于是出现了上图所示的树状数组。其中方块中的数字表示对应区间的下标范围,红色字体表示节点的下标,图中的蓝色粗线条将每个节点和其对应的下标连接。但是它的规律还是很特殊的。
这里写图片描述
>观察上表,我们可以得出如下结论:
一、节点下标为 i 时,节点中对应的最后一个元素下标为 i
二、节点下标对应的二进制数末尾有 k 个 0 ,节点中对应的元素个数为 2 ^ k
三、节点中对应的元素下标是连续的
四、树状数组的节点个数和原数据元素个数相等

相识

核心内容:lowbit —— 巧妙地运用了二进制的 规律 ,进行分层更新:

假设a数组存原始数据,v数组是更新后的树状数组 ;
上半部分的c[i]初始化为c[i]=a[i]。用以描述树状数组的产生过程、
c[i]旁边的红色数字表示的是i的二进制
树状数组
因为0的性质有些特别,为了避免错误发生我们使用的数组从下标1开始,

在简单的了解树状数组后,其实不难发现树状数组就是用来解决 前i个元素和 的这种问题。

求前n个数的和nn的二进制v数组的求值过程
(1,1)100001c1
(1,2)200010c1+c2
(1,3)300011c3
(1,4)400100c2+c3+c4

观察规律可得
求(1,7)= c[7]+c[6]+c[4]=c[0111]+c[0110]+c[0100]

由上述表达式可以发现,前7个元素和(1,7)的加数包括 c[0111], 在此基础上,每次将下标从右向左数第一位 1 抹去作为下一个加数的下标, 直到数字变为 0 结束,0111 抹去最后一位 1 得到 0110,0110 抹去最后一位 1 得到 0100,0100 抹去最后一位 1 得到 0000 结束运算。

“抹掉(二进制的)最后一位 1 ”是容易用语言描述的。
体现在代码为:i&(-i)
即:我们得到每次的减数 x =i & ( -i )

1 &(-1)补码运算 : 0001 & 1111 = 0001
2 &(-2)补码运算 : 0010 & 1110 = 0010
3 &(-3)补码运算 : 0011 & 1101 = 0001
4 &(-4)补码运算 : 0100 & 1100 = 0100
5 &(-5)补码运算 : 0101 & 1011 = 0001
6 &(-6)补码运算 : 0110 & 1010 = 0010
7 &(-7)补码运算 : 0111 & 1001 = 0001
8 &(-8)补码运算 : 1000 & 1000 = 1000

原因

于正数来说,补码和原码形式是一样的,但对于负数来说,补码便是将原码按位取反后在末位加 1 ,这就导致一个负数绝对值的补码和这个负数的补码在形式上满足:以从右向左数第一个非零位为界,在左侧,负数绝对值的补码和负数的补码各位均不相同,在右侧,负数绝对值的补码和负数的补码各位均为0,将两数做 and 运算得到的数字刚好就是我们需要求的 x ,我们知道 负数的绝对值和负数本身互为相反数,同时也说明一个正数的补码与其相反数的补码做 and 运算 得到的数字就是 x 。

分体解读

UVALive - 2191 为例

lowbit

int lb(int x)
///lowbit 用来求二进制的末尾0的个数 求的是二进制(从右往左数)最后一个1 的位置,
//并且将之前的1全都抹掉,并转为2进制
//i+lb(i)实现了: c[i+lb(i)]刚好是包含c[i]的上一层
{
    return x&-x;
}
/*
i= 1  lb(i)=1
i= 2  lb(i)=2
i= 3  lb(i)=1
i= 4  lb(i)=4
i= 5  lb(i)=1
i= 6  lb(i)=2
i= 7  lb(i)=1
i= 8  lb(i)=8
i= 9  lb(i)=1
*/

update


void add(int k, int d, int n)///对某个元素进行加法操作
                            ///元素总个数n ,第k个元素增加d
{
    C[k] += d;              ///对树的更新
    while (k+lb(k) <= n)
        C[(k+=lb(k))] += d;// 是+=哦
}

加法操作使得函数可以在 一次循环中 完成赋值和更新
通过i+lowbit(i) 得到i的上一层
所以实际上是从最底层进行向上更新

sum

int sum(int k)///求前n项和:
{
    int value = C[k];
    while (k > lb(k))
    {
        k-=lb(k);
        value += C[k];
    }
    return value;
}

int range_sum (int i,int j)///区间求和
{
    return sum(j)-sum(i);
}

示例

#include <cstring>
#include <cstdio>

///用于区间求和
int data[200002];
int C[200002];

int lb(int x)///lowbit 用来求二进制的末尾0的个数 求的是二进制最后一个1 的位置
{
    return x&-x;
}

int sum(int k)///求前n项和:
{
    int value = C[k];
    while (k > lb(k))
    {
        k-=lb(k);
        value += C[k];
    }
    return value;
}

void add(int k, int d, int n)///对某个元素进行加法操作
                            ///元素总个数n ,第k个元素增加d
{
    C[k] += d;              ///对树的更新
    while (k+lb(k) <= n)
        C[(k+=lb(k))] += d;
}
int range_sum (int i,int j)///区间求和
{
    return sum(j)-sum(i);
}

int main()
{

    int  cases = 0, n, x, y;//元素总个数n

    char buf[10];
    while (~scanf("%d",&n) && n)
    {
        memset(C, 0, sizeof(C));
        for (int i = 1; i <= n; ++ i)
        {
            scanf("%d",&data[i]);
            add(i, data[i], n);
        }
        if (cases ++) puts("");
        printf("Case %d:\n",cases);
        while (~scanf("%s",buf) && strcmp(buf, "END"))
        {
            scanf("%d%d",&x,&y);
            if (buf[0] == 'S')
            {
                add(x, y-data[x], n);
                data[x] = y;
            }
            else
            {

                printf("%d\n",sum(y)-sum(x-1));
            }
        }
    }
    return 0;
}

常用扩展

树状数组区间更新
树状数组求第k大
树状数组+离散化
树状数组+并查集
树状数组+搜索

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值