Python-树状数组算法入门

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

目录

管辖区间

[怎么让计算机计算 lowbit?](#怎么让计算机计算 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] 拆成 不多于 段区间 ,使得这 段区间的信息是 已知的

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

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

管辖区间

那么问题来了, 管辖的区间到底往左延伸多少?也就是说,区间长度是多少?

树状数组中,规定 管辖的区间长度为 ,其中:

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

因为 ,其二进制最低位的 1 以及后面的 0 组成的二进制是 1000,即 8,所以管辖 8 个 a 数组中的元素。

因此,代表 的区间信息。

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

这里注意:指的不是最低位 1 所在的位数 ,而是这个 1 和后面所有 0 组成的

怎么让计算机计算 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,并且区间只有一个值

python 复制代码
def lowbit(self, x):
    return x & -x

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

区间初始化:

python 复制代码
    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 之间连边(其它位置也同理),我们可以得到一个什么样的结构?

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

注意到:

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

我们需要证明两点:

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

1) 的证明

2 )的证明

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

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

python 复制代码
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的前缀和有更快的方法那就是访问树状数组的结构

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

计算任意区间和:

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

python 复制代码
def sumRange(self, left: int, right: int) -> int:
    return self.prefixsum(right + 1) - self.prefixsum(left)

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

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

python 复制代码
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)

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

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

相关推荐
m0_504135302 小时前
代码随想录算法训练营第六十一天 | floyd算法
算法
xin007hoyo6 小时前
算法笔记.染色法判断二分图
数据结构·笔记·算法
এ᭄画画的北北8 小时前
力扣-234.回文链表
算法·leetcode·链表
八股文领域大手子8 小时前
深入理解缓存淘汰策略:LRU 与 LFU 算法详解及 Java 实现
java·数据库·算法·缓存·mybatis·哈希算法
__lost9 小时前
C++ 解决一个简单的图论问题 —— 最小生成树(以 Prim 算法为例)
算法·图论·最小生成树·prim算法
wuqingshun31415910 小时前
蓝桥杯 11. 打印大X
数据结构·算法·职场和发展·蓝桥杯·深度优先
Blossom.11811 小时前
量子网络:构建未来通信的超高速“高速公路”
网络·opencv·算法·安全·机器学习·密码学·量子计算
A林玖11 小时前
【机器学习】朴素贝叶斯
人工智能·算法·机器学习
六边形战士DONK11 小时前
神经网络基础[损失函数,bp算法,梯度下降算法 ]
人工智能·神经网络·算法
wuqingshun31415911 小时前
蓝桥杯 2. 确定字符串是否是另一个的排列
数据结构·c++·算法·职场和发展·蓝桥杯