BUGAWAY算法小抄-树状数组(2024.03.23 更新)

什么是树状数组?

树状数组是支持单点修改区间查询的、代码量小的数据结构。

事实上,树状数组能解决的问题是线段树(一棵二叉树,每个节点表示一个区间,并存储该区间的一些相关信息。线段树可以高效地进行区间查询和区间更新操作。不是本文重点)能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小,因此仍有学习价值。

有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的区间加单点值区间加区间和问题。

举个栗子🌰,想知道a[1,...,7]的前缀和,怎么做?

一种方法就是将所有数都加起来。但如果已知三个数 A,B,C,A=a[1,...,4],B=a[5,...,6],C=a[7,...,7]。求和只需要 A + B + C 。

这就是树状数组能快速求解信息的原因:我们总能将一段前缀拆成 不多于logn 段区间,使得这 logn 段区间的信息是 已知的。于是,我们只要合并这些段区间的信息,就可以得到答案,相比于原先直接合并 n 个元素,效率有了很大提升。

img

直接上结论,不难发现,c[x]管辖的一定是[x - lowbit(x)+1, x]的区间总信息。如c[88]管辖的是[88-8+1,...,88]即[81,...,88]的区域($88_{(10)}$=$1011000_{(2)}$,因此 lowbit(88) = $1000_{(2)}$=8)。

⚠️注意:lowbit 指的不是x 的最低位1 所在的位数 k,而是这个 1 和其后面的 0 所组成的 $2^k$。

lowbit 对应代码为:

public int lowbit(int x){
  // x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
  // lowbit(0b01011000) == 0b00001000
  //          ~~~~^~~~
  // lowbit(0b01110010) == 0b00000010
  //          ~~~~~~^~
  return x & -x;
}

树状数组有两大核心功能:

  1. 单点更新 update(i, v): 把序列 i 位置的数加上一个值 v;
  2. 区间查询 query(i): 查询序列 [1⋯i] 区间的区间和,即 i 位置的前缀和;

修改和查询的时间代价都是 O(logn),其中 n 为需要维护前缀和的序列的长度。

OK,我们不扯长篇大论,以练促学,接下来我们实操一下!💪

实践

【板子题】307. 区域和检索 - 数组可修改

class NumArray {
    private int[] nums; // 基础数组
    private int[] tree; // 树状数组,功能是单点修改和区间查询。下标表示右边界。

    public void add(int index, int val) {
        // 单点修改,把序列第 index 个数增加 val
        // 为保证效率,我们只需遍历并修改管辖了 a[index]的 tree[y],其他的 tree 没有明显变化
        // 管辖 a[index]的 tree[y] 一定包含tree[index]。所以 y 在形态上是 index 的祖先
        // 因此我们从 index 开始不断往上跳父亲,直到超出数组边界
        while (index < tree.length) {
            tree[index] += val;
            index += lowbit(index);
        }
    }

    public int prefixSum(int index) {
        // 区间查询,查询前 index 个元素的前缀和
        int sum = 0;
        while (index > 0) {
            sum += tree[index];
            index -= lowbit(index);
        }
        return sum;
    }

    public NumArray(int[] nums) {
        this.tree = new int[nums.length + 1];
        this.nums = nums;
        for (int i = 0; i < nums.length; i++) {
            add(i + 1, nums[i]);
        }
    }

    public void update(int index, int val) {
        add(index + 1, val - nums[index]);
        nums[index] = val;
    }

    public int sumRange(int left, int right) {
        return prefixSum(right + 1) - prefixSum(left);
    }

    // lowbit 函数计算区间管辖的左边界
    public int lowbit(int x) {
        return x & -x;
    }
}

【离散化树状数组】315. 计算右侧小于当前元素的个数

class Solution {
    int[] a; // 原始数组
    int[] c; // 树状数组
    public List<Integer> countSmaller(int[] nums) {
        // 离散化+ 树状数组
        // 离散化的目的是因为“把原序列的值域映射到一个连续的整数区间,并保证它们的偏序关系不变。“
        List<Integer> resultList = new ArrayList<>();
        discretization(nums);
        init(nums.length);
        for(int i = nums.length -1; i >= 0; i--){
            int id = getId(nums[i]); // 离散化之后相当于id 为 nums[i]在 nums 中第 id 小的元素
            resultList.add(query(id-1)); // id-1是因为不包括 id 的个数
            update(id);
        }
        Collections.reverse(resultList);
        return resultList;
    }

    public void init(int length){
        c = new int[length];
        Arrays.fill(c,0);
    }

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

    public int query(int pos){
        // 范围查询
        int ret = 0;
        while(pos > 0){
            ret += c[pos];
            pos -= lowbit(pos);
        }
        return ret;
    }

    public void update(int pos) {
        // 单点更新
        while( pos < c.length){
            c[pos] += 1;
            pos += lowbit(pos);
        }
    }

    // 离散化操作
    public void discretization(int[] nums){
        Set<Integer> set = new HashSet<Integer>();
        for(int num: nums){
            set.add(num);
        }
        int size = set.size(); // 有 size 个不一样的数
        a = new int[size];
        int index = 0;
        for(int num: set){
            a[index++] = num;
        }
        Arrays.sort(a);
    }

    public int getId(int x){
        return Arrays.binarySearch(a,x) + 1;
    }
}

解释:

过程如下:

posted @ 2024-03-23 18:09  无发可说  阅读(14)  评论(2编辑  收藏  举报