当前位置: 首页 > news >正文

Python-树状数组算法入门

树状数组是算法中一种十分重要的数据结构!通过这篇文章你能够快速的get到树状数组的精髓,即使是第一次接触树状数组也能让你完全弄懂

目录

管辖区间

怎么让计算机计算 lowbit?

区间初始化:

更新区间

计算前缀和:

计算任意区间和:


先来举个例子:我们想知道  a[1...7]的前缀和,怎么做?

一种做法是:a[1]  +a[2] + a[3] + a[4] .... a[7],需要求 7个数的和。

但是如果已知三个数 A,B,C,,A = a[1...4] 的总和,B = [5...6] 的总和和C = a[7..7](其实就是a[7] 自己)。你会怎么算?你一定会回答:A + B + C,只需要求  3个数的和。

这就是树状数组能快速求解信息的原因:我们总能将一段前缀 [1..n] 拆成 不多于 \log n 段区间,使得这 \log n 段区间的信息是 已知的

于是,我们只需合并这  \log n  段区间的信息,就可以得到答案。相比于原来直接合并  个信息,效率有了很大的提高。不难发现信息必须满足结合律,否则就不能像上面这样合并了。

那么如何划分a[1.. n]呢? 咱们直接先说结论吧  (图片出自灵神)

管辖区间

那么问题来了,c[x](x \ge 1) 管辖的区间到底往左延伸多少?也就是说,区间长度是多少?

树状数组中,规定 c[x] 管辖的区间长度为 2^{k},其中:

  1. 设二进制最低位为第 0 位,则 k 恰好为 x 二进制表示中,最低位的 1 所在的二进制位数;
  2. 2^k(c[x] 的管辖区间长度)恰好为 x 二进制表示中,最低位的 1 以及后面所有 0 组成的数。举个例子,c_{88} 管辖的是哪个区间?

因为 88_{(10)}=01011000_{(2)},其二进制最低位的 1 以及后面的 0 组成的二进制是 1000,即 8,所以c_{88}管辖 8 个 a 数组中的元素。

因此,c_{88}代表 a[81 \ldots 88]的区间信息。

我们记 x 二进制最低位 1 以及后面的 0 组成的数为 \operatorname{lowbit}(x),那么 c[x] 管辖的区间就是 [x-\operatorname{lowbit}(x)+1, x]。 为什么要加1呢这个理解的十分简单你看c_{88}

这里注意:\boldsymbol{\operatorname{lowbit}}指的不是最低位 1 所在的位数 \boldsymbol{k},而是这个 1 和后面所有 0 组成的 \boldsymbol{2^k}

怎么让计算机计算 lowbit?

(如果是人那么一看就知道了,那么如何让计算机理解呢?)

根据位运算知识,可以得到 lowbit(x) = x & -x。这是一种简单的实现方法

如果对位运算不是十分敏感的可能不知道为什么是这样算的,我们可以假设x是 7那么相应的lowbit(7) 为1 对于负数的二进制就是求法:

  1.         先求绝对值的二进制。对于-7来说7的二进制是111
  2.         再求第一步的补码那么就是000
  3.         在将第二步得到的值 加一得到 001,那么001就是-7的二进制,当然前面缺位补0

那么言归正传 7 & -7 就是111 & 001 那么lowbit就是001 十进制就是1表示当前lowbit(7)值为1,并且区间只有一个值

def lowbit(self, x):
    return x & -x

补充:对于计算机是这样理解的,那么对于我们还有一种更容易的理解方式,参考灵神题解

区间初始化:

    def __init__(self, nums: List[int]):
        n = len(nums)
        tree = [0] * (n + 1)
        for i, x in enumerate(nums, 1):
            tree[i] += x 
            nxt = i + (i & -i) #下一个关键区间的右端点,这也就说明当前区间在下一个区间内,那么就要操
            #这里的 i & -i 就是lowbit
            if nxt <= n:
                tree[nxt] += tree[i] 
        self.nums = nums 
        self.tree = tree 

更新区间

假设下标 x 发生了更新,那么所有包含 x 的关键区间都会被更新。

例如下标 5 更新了,那么关键区间 [5,5],[5,6],[1,8],[1,16] 都需要更新,这三个关键区间的右端点依次为 5,6,8,16。

如果在 5-6,6-8,8-16 之间连边(其它位置也同理),我们可以得到一个什么样的结构?

如下图,这些关键区间可以形成如下树形结构(区间元素和保存在区间右端点处)。


 

注意到:

5+lowbit(5)=5+1=6

6+lowbit(6)=6+2=8

8+lowbit(8)=8+8=16

猜想:如果 x 是一个被更新的关键区间的右端点,那么下一个被更新的关键区间的右端点为 x+lowbit(x)。

我们需要证明两点:

  1. 右端点为 x 的关键区间,被右端点为 x+lowbit(x) 的关键区间包含。
  2. 右端点在 [x+1,x+lowbit(x)−1] 内的关键区间,与右端点为 x 的关键区间没有任何交集。

1) 的证明

2 )的证明

以上两点成立,就可以保证 x+lowbit(x) 是「下一个」被更新的关键区间的右端点了。

由于任意相邻被更新的关键区间之间,没有其余关键区间包含 x,所以我们可以找到所有包含 x 的关键区间,具体做法如下。

def update(self, index: int, val: int) -> None:#将nums[index]更新为val
    delta = val - self.nums[index] #val是目标值,相当于把当前值增加了delta
    self.nums[index] = val #把当前值更新了
    i = index + 1 #为什么index要加1呢,因为index是下标0 --n-1 为了和1--n的c的下标对应肯定加1
    while i < len(self.tree):
        self.tree[i] += delta 
        i += i & -i #手搓lowbit 

计算前缀和:

在树状数组中计算1-n的前缀和有更快的方法那就是访问树状数组的结构

def prefixsum(self, i: int) -> int: #计算前缀和是为了计算区间,每一个区间都可以用前缀和表示
    s = 0
    while i:
        s += self.tree[i]
        i  = i - (i & -i) #相当于i - lowbit(),其实就是前缀和跳到上一个关键区间的右端点了
          #相较于传统计算前缀和更快,这里的prefixsum是计算1-i的前缀和

计算任意区间和:

因为本质上所有子区间都可以写成两个区间和的差

def sumRange(self, left: int, right: int) -> int:
    return self.prefixsum(right + 1) - self.prefixsum(left)

关于相关题目,博主会尽快整理发表。

下面是全部实现代码和解释:

class NumArray:
    __slots__ = 'nums', 'tree'
    #__slots__是一个特殊的内置类属性,它可以用于定义类的属性名称的集合。一旦在类中定义了__slots__属
    #性,Python将限制该类的实例只能拥有__slots__中定义的属性。这有助于减少每个实例的内存消耗,
    #提高属性访问速度,同时也可以防止意外添加新属性。

    #最右端为i的长为lowbit(i)的关键区间是 [i - lowbit(i) + 1 , i]
    def __init__(self, nums: List[int]):
        n = len(nums)
        tree = [0] * (n + 1)
        for i, x in enumerate(nums, 1):
            tree[i] += x 
            nxt = i + (i & -i) #下一个关键区间的右端点,这也就说明当前区间在下一个区间内,那么就要操
            #这里的 i & -i 就是lowbit
            if nxt <= n:
                tree[nxt] += tree[i] 
        self.nums = nums 
        self.tree = tree 

    def update(self, index: int, val: int) -> None:#将nums[index]更新为val
        delta = val - self.nums[index] #val是目标值,相当于把当前值增加了delta
        self.nums[index] = val #把当前值更新了
        i = index + 1 #为什么index要加1呢,因为index是下标0 --n-1 为了和1--n的c的下标对应肯定加1
        while i < len(self.tree):
            self.tree[i] += delta 
            i += i & -i #手搓lowbit 

    # #需要证明两点:
    # 1.右端点为x的关键区间,被右端点为x + lowbit(x)的关键区间包含
    # 2.右端点为[x + 1, x + lowbit(x) - 1]内的关键区间,与右端点为x的关键区间没有任何交集

    def prefixsum(self, i: int) -> int: #计算前缀和是为了计算区间,每一个区间都可以用前缀和表示
        s = 0
        while i:
            s += self.tree[i]
            i  = i - (i & -i) #相当于i - lowbit(),其实就是前缀和跳到上一个关键区间的右端点了
            #相较于传统计算前缀和更快,这里的prefixsum是计算1-i的前缀和
        return s 

    def sumRange(self, left: int, right: int) -> int:
        return self.prefixsum(right + 1) - self.prefixsum(left)

本博客参考灵神题解和树状数组

希望这篇文章能帮到你,感谢点赞收藏!

相关文章:

  • Linux中基础开发工具详细介绍
  • 16.AVL树实现
  • 关于 NoC 中数据安全传输的设计与实现的详细介绍
  • C++ 容器库概述:序列容器、关联容器与无序关联容器的原理、性能与应用
  • Docker Compose 使用笔记
  • QT 学习一 paintEvent,QPainter ,QImage
  • 智慧城市运行管理服务平台建设方案
  • STM32串口通信
  • ‘java‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。
  • 【网络】什么是 IHL(Internet Header Length,首部长度)TTL(Time To Live,生存时间)?
  • 【编解码技术】什么是编码复杂度?
  • SpringMVC(三)响应处理
  • 构建智能汽车地图标准体系:自动驾驶技术的基石
  • 一文讲清楚CUDA与PyTorch、GPU之间的关系
  • 基于Python的selenium入门超详细教程(第1章)--WebDriver API篇
  • 【Linux-传输层协议TCP】TCP协议段格式+确认应答+超时重传+连接管理机制(三次握手、四次挥手、理解TIME_WAIT + CLOSE_WAIT)
  • 结构型——适配器模式
  • 二维数组常见应用场景以及示例
  • [Ai 力扣题单] 数组基本操作篇 27/704/344/386
  • Linux系统性能调优
  • 资讯网站模板带会员投稿功能/长春seo排名外包
  • 怎么制作一个网站及小程序/完美日记网络营销策划书
  • 企业网站cms源码/网络营销做的比较好的企业
  • 互联网网站建设哪家好/搜索引擎收录查询工具
  • 建设网站证书/怎么让自己上百度
  • 上传自己做的网站后台怎么办/电工培训学校