2022-03-04 每日打卡:树状数组

2022-03-04 每日打卡:树状数组

写在前面

“这些事儿在熟练之后,也许就像喝口水一样平淡,但却能给初学者带来巨大的快乐,我一直觉得,能否始终保持如初学者般的热情、专注,决定了在做某件事时能走多远,能做多好。” 该系列文章由python编写,所刷题目共三个来源:之前没做出来的 ;Leetcode中等,困难难度题目; 周赛题目;某个专题的经典题目,所有代码已AC。每日1-3道,随缘剖析,希望风雨无阻,作为勉励自己坚持刷题的记录。

简介

我的理解是树状数组维持的是与【前缀】相关的数据。这些数据可以用于【静态】和大多数的区间情况。使用简单。其结点维护信息的数量是由其结点转化成二进制之后最左边的1的个数之后的零的个数决定的。
但树状数组并不是单纯的前缀,而是以树的结构存储前缀,譬如c2,c4结点存储的才是真正意义上的前缀。规律是:

c [ i ] = A [ i − 2 k + 1 ] + A [ i − 2 k + 2 ] + . . . + A [ i ] c[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i] c[i]=A[i2k+1]+A[i2k+2]+...+A[i]

其中k为i的二进制中从最低位到高位连续零的长度,例如i = 8(1000)时候,k = 3,可自行验证,这也是c[i]所管辖的范围。
2022-03-04 每日打卡:树状数组_第1张图片

常用操作

低位(lowbit)运算

计算非负整数 n 在二进制表示下 【最低位的 1 及其后边所有的 0】 构成的数值,比如 l o w b i t ( 6 ) = 2 lowbit(6) = 2 lowbit(6)=2 。公式为 l o w b i t ( n ) = n & ( ∼ n + 1 ) = n & ( − n ) lowbit(n) = n\&(∼n+1) = n\&(−n) lowbit(n)=n&(n+1)=n&(n)
简单解释一下,取反后加一的实际操作就是把【最低位1之前的数全部取反了】,也是补码的表示方式!负数的补码就是自身取反加一嘛。
这个 l o w b i t ( n ) lowbit(n) lowbit(n)是一种很简洁的从十进制中提取了二进制的思想,应用广泛。

前缀(query)运算

我们以计算11的前缀和为例,11的二进制是1011,期望将其拆成1000+0010+0001,而树状数组中,有 p r e [ 11 ] = c [ 11 ] + c [ 10 ] + c [ 8 ] pre[11] = c[11]+c[10]+c[8] pre[11]=c[11]+c[10]+c[8],更直观的,我们可以沿着11一直向左上角找,直到剩0。
```
// 计算i的前缀和

	while (i > 0){
	    res += c[i];
	    i -= lowbit(i);
	}
	```

更新(update)运算

对于当前更新的i,一定有某个比它大的区间包含它,而我们要做的就是找到这个【刚刚补齐的二进制数】,比如c[9]更新后,包含其的上一层为c[10]也就是c[9+1]。直到更新到最大的区间。
```
// 将第i个结点加x,n是BIT的大小(下标从1开始)

    while (i <= n){
        c[i] += x;
        i += lowbit(i);
    }
	```

经典题目

剑指 Offer 51. 数组中的逆序对

2022-03-04 每日打卡:树状数组_第2张图片

  • 二分法的方法:
class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        temp = []
        res = 0
        for t in reversed(nums):
            # bisect是python内置模块,用于有序序列的插入和查找。返回的值是应该插入的位置。
            curr = bisect_left(temp, t)
            res += curr
            temp[curr:curr] = [t]
        return res
  • 树状数组:
class BIT:
    def __init__(self, n):
        self.n = n
        self.tree = [0] * (n + 1)

    @staticmethod
    def lowbit(x):
        return x & (-x)
    
    def query(self, x):
        ret = 0
        while x > 0:
            ret += self.tree[x]
            x -= BIT.lowbit(x)
        return ret

    def update(self, x):
        while x <= self.n:
            self.tree[x] += 1
            x += BIT.lowbit(x)

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        n = len(nums)
        # 离散化:一般有两种方式。利用字典映射 或 改变绝对位置成相对位置。这里是用的后者。
        tmp = sorted(nums)
        for i in range(n):
            nums[i] = bisect.bisect_left(tmp, nums[i]) + 1
        # 树状数组统计逆序对
        bit = BIT(n)
        ans = 0
        # 把后面的放进来,计算前缀
        for i in range(n - 1, -1, -1):
            ans += bit.query(nums[i] - 1)
            bit.update(nums[i])
        return ans

2179. 统计数组中好三元组数目

2022-03-04 每日打卡:树状数组_第3张图片

class Solution:
    def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        # 按照nums1中的出现顺序,构成f,记录了当前位置数值在nums2的出现位置!
        # 这个题和数值大小本身无关,仅与下标位置有关
        pos2 = dict((nums2[i], i) for i in range(n))
        f = [pos2[nums1[i]] for i in range(n)]
        
        def add(x):
            while x <= n:
                t[x] += 1
                x += (x & (-x))
        
        def query(x):
            res = 0
            while x:
                res += t[x]
                x -= (x & (-x))
            return res
        
        left, right = [0] * n, [0] * n
        
        # 计算左侧小于 f[i] 的元素个数
        t = [0] * (n + 1)
        for i in range(n):
            left[i] = query(f[i])
            add(f[i] + 1)
        
        # 计算右侧大于 f[i] 的元素个数
        t = [0] * (n + 1)
        for i in range(n-1, -1, -1):
            right[i] = n - 1 - i - query(f[i] + 1)
            add(f[i] + 1)
            
        res = 0
        for i in range(n):
            res += left[i] * right[i]
            
        return res



VS线段树

线段树维护的是区间的信息,其父亲节点维护子节点的信息,个数是由覆盖的范围大小决定的,到了叶子结点才是具体的某一个值。
2022-03-04 每日打卡:树状数组_第4张图片

你可能感兴趣的:(algorithm,leetcode,算法,职场和发展)