Binary Indexed Tree

refs:

裸题之灵神题解:
<https://leetcode.cn/problems/range-sum-query-mutable/solutions/2524481/dai-ni-fa-ming-shu-zhuang-shu-zu-fu-shu-lyfll>

灵神的视频讲解:
<https://www.bilibili.com/video/BV14r421W7oR>

1. 用来解决什么问题

区间和

这个问题原先的做法是维护前缀和,查找O(1),但问题是前缀和一旦确定,数组就不能修改了,改了之后前缀和还得花O(n)的时间去改。而树状数组可以做到查找和修改都是O(logn)的,超过前缀和的查找O(1),修改O(n)。

2. 思想

2.1. 分块

先介绍一个分块的前缀和的思想,比如数组:

[ 1 1 2 3 5 6]

对他进行长度为B=2的分块:

[ 1 1 | 2 3 | 5 6 ]

然后维护每个分块的区间和。那么如果查询区间[3:6]的和,就是后两个分块,这样查询就是O(n/B)。(你可能会说如果不是整数个区间呢?那多出来的部分单算,比如[2:6],索引2上的1单独算就行了,复杂度O(n/B+B),由于B大概率比n小,那就是O(n/B))

修改的话,维护对应分块的前缀和,O(B)。

这样查找O(n/B),修改O(B)。那么怎么均衡以下,这个东西很明显是耐克函数啊,取B = sqrt(n)即可。

那有没有更优的呢?比sqrt(n)更好的,是logn,前者是幂级别的,后面是指数级别递减的。树状数组就可以做到这一点。

2.2. 树状数组

树状数组指把一个数组按照树形规则划分为多个子数组。在修改时,只需要修改相关的关键区间(通过一个顶级的位运算trick),在查询时,只需要计算logn个关键区间的和即可。

怎么划分树状数组

我们先来说怎么划分树状数组。首先要明确的是,树状数组的索引从1开始,也就是[1:n]

对于[1:n]中间的任意一个索引i,划分[1:i]的规则如下:

  1. 先把i转为二进制串,例如13D=1101B

  2. 从右往左(从小到大),对于二进制串的每一位,如果是1,那么计算当前二进制串的值。例如1101的从右往左的第3位,为1,那么0100=4D。然后划分出一个长度为4的区间。

    对于13来说,划分如下:

    1101B
    
    0001 = 1D [13,13]
    
    0000 不是1 跳过
    
    0100 = 4D [9,12]
    
    1000 = 8D [1,8]
    

    这样就把[1,13]划分成了三个区间,[1,8] [9,12] [13,13]。大家可以看上面那张图,看看是不是c[13]单指一个13,c[12]指[9,12],c[8]指[1,8]

而且这种划分有个性质,对于索引i,拆分所有的满足1≤j≤i的[1,j],将恰好得到i个不同的区间。这个想法类似于DP。例如你拆[1,13],那么递归到底部必然拆了[1,1] ⇒ [1,1],[1,2] ⇒ [2,2],[1,3] ⇒ [1,2] + [3,3]…

怎么修改树状数组

举个例子啊,如果你修改索引为10的元素,那么哪些相关的区间会变动呢?

查看上图,你会发现,比10小的都不会受影响,因为一定会有关键区间“管着”前面的元素。但是后面的关键区间也不是都得修改。比如11,有c[11]管着。

需要修改的是10,12,16。12,16都是祖先节点。那么怎么找到12和16呢?

我们来找找规律:

10D = 01010B
12D = 01100B
16D = 10000B

那么01010B和01100B差了2D,恰好是01010B的最低位的1的值,也就是00010B。

这有个专门的说法,叫做lowbit,lowbit(i)为i的二进制下最低位1的值。i为正整数

而01100B和10000B差了4D,恰好是01100B的最低位的1的值,也就是00100B。

这是个树状数组很重要的性质,就是后向的相关的关键区间就是当前区间加上lowbit的值,一直迭代到数组越界为止。

怎么查询树状数组

由于我们是按照二进制串对数组进行的划分,因此对于一个区间,划分的数量是log2n的。比如13,划分了3个,就是下取整log2(13)。所以我们至多查询logn个区间。

那么前向相关区间都在哪呢?还是lowbit,修改看后向加lowbit,那查询看前向,不就是减去lowbit吗?

比如你要查[1,7]的,你直接一看7的lowbit,1,那就算上c[7]=[7,7],然后跳到7-1=6去。你再一看6的lowbit,2,那就算上c[6]=[5,6],然后跳到6-2=4去。再一看4的lowbit,4,那就算上c[4] = [1,4],然后跳到4-4=0去,到此跳出了树状数组,查询结束,返回和。

那么任意区间也是很简单的,整体的减左边的即可。比如你查[3,7],那就是[1,7]-[1,3]就可以了。两次logn,还是logn。

3. 搞点题⑧

1. LC 307 区域和检索-数组可修改

树状数组裸题。就是板子。

对于初始化,我们要先初始化所有的关键区间,就得调update。那么之前说了,修改树状数组查询lowbit,要用一个很nb的位运算trick。比如12D = 1100B,lowbit=4。咋查?

设i=1100B

~i = 0011B
~i+1 = 0100B

i & (~i+1) = 1100 & 0100 = 0100

也就是说每个数和它的取反+1与一下就是lowbit。

那么取反+1是什么?补码嘛,那就是-i啊。所以就是i&(-i),就能算出来lowbit的值。

public class NumArray {
    private int[] nums; // 保存各数字
    private int[] tree; // 树状数组保存前缀和

    public NumArray(int[] nums) {
        int n = nums.length;
        this.nums = new int[n]; // 使 update 中算出的 delta = nums[i]
        tree = new int[n + 1];
        for (int i = 0; i < n; i++) {
            update(i, nums[i]);
        }
    }

    public void update(int index, int val) {
        int delta = val - nums[index];
        nums[index] = val;
        for (int i = index + 1; i < tree.length; i += i & -i) { // i+= i & -i:增加lowbit,找到相关的关键区间
            tree[i] += delta;
        }
    }

    private int prefixSum(int i) {
        int s = 0;
        // 举个例子,如果是[1,13] 相当于求 [13,13] + [9,12] + [1,8]的区间和
        for (; i > 0; i &= i - 1) { // i -= i & -i 的另一种写法
            s += tree[i];
        }
        return s;
    }

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

这样初始化是n次logn,就是O(nlogn),也是整个树状数组的瓶颈。怎么提升呢?

其实我们可以在初始化的时候就采用update的那种迭代更新后向相关的关键区间的方式。比如说你初始化好了c[6],那么lowbit=2,这样你后向的相关区间就是6+2=8。所以你可以直接在这次迭代的时候先把c[6]累加到c[8]上去,这样就变成O(n),有点像DP的刷表。

public class NumArray {
    private int[] nums;
    private int[] tree;

    public NumArray(int[] nums) {
        int n = nums.length;
        this.nums = nums;
        tree = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            tree[i] += nums[i - 1];
            int nxt = i + (i & -i); // 下一个关键区间的右端点
            if (nxt <= n) {
                tree[nxt] += tree[i];
            }
        }
    }

    public void update(int index, int val) {
        int delta = val - nums[index];
        nums[index] = val;
        for (int i = index + 1; i < tree.length; i += i & -i) {
            tree[i] += delta;
        }
    }

    private int prefixSum(int i) {
        int s = 0;
        for (; i > 0; i &= i - 1) { // i -= i & -i 的另一种写法
            s += tree[i];
        }
        return s;
    }

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

2. LC 3072 将元素分配到两个数组中Ⅱ

周赛387T4。这题我本来是手搓了一个sortedList然后二分做的。但其实可能被极限用例构造O(n²)从而T掉,这个要看官方是否要rej了。

这题真正的做法是树状数组+名次排序+哈希表。

首先我们先把nums去重排序,然后给每个不同的数标个rank放到哈希表。这个名次是数越小名次越小。例如:

nums: 5 14 3 1 2

排序: 1 2 3 5 14

rank: 1 2 3 4 5

随后我们定义在树状数组中,nums[i]代表名次为i+1的数字出现的次数,那么比名次为i+1的数字大的数的出现次数就是sumRange(i,nums.length-1)。这个查询是O(logn)的,随后把数字放入arr1或arr2后的更新树状数组,也是O(logn)的。总共n个数,就是O(nlogn)的。树状数组初始化我用的默认初始化O(1),排序O(nlogn)。所以时间总体上O(nlogn),空间O(n)。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

class BIT{
    public final int[] nums;
    public final int[] tree;

    public BIT(int n){
        this.nums = new int[n];
        this.tree = new int[n+1];
    }

    public BIT(int[] init){
        int n = init.length;

        this.nums = init;
        this.tree = new int[n+1];

        for(int i=1;i<=n;i++){
            tree[i]+=init[i-1];

            int next = i+(i&(-i));
            if(next<=n){
                tree[next] += tree[i];
            }
        }
    }

    public void update(int index,int val){
        int diff = val-nums[index];

        nums[index] = val;
        for(int i=index+1;i<tree.length;i+=(i&(-i))){
            tree[i]+=diff;
        }
    }

    private int prefixSum(int index){
        int sum = 0;

        for(int i=index;i>0;i-=(i&(-i))){
            sum+=tree[i];
        }

        return sum;
    }

    public int sumRanges(int l,int r){
        return prefixSum(r+1)-prefixSum(l);
    }
}

class Solution {
    public int[] resultArray(int[] nums) {
        HashMap<Integer, Integer> m = new HashMap<>();

        int[] tmp = nums.clone();
        // 排序去重计算rank
        Arrays.sort(nums);
        int rank = 1;
        for(int i=0;i<nums.length;){
            int j = i;
            while(j<nums.length&&nums[j]==nums[i]){
                j++;
            }
            m.put(nums[i],rank++);
            i = j;
        }

        BIT BT1 = new BIT(rank-1);
        BIT BT2 = new BIT(rank-1);

        nums = tmp;
        int rk;
        rk = m.get(nums[0]);
        BT1.update(rk-1,BT1.nums[rk-1]+1);
        rk = m.get(nums[1]);
        BT2.update(rk-1,BT2.nums[rk-1]+1);

        ArrayList<Integer> arr1 = new ArrayList<>();
        arr1.add(nums[0]);
        ArrayList<Integer> arr2 = new ArrayList<>();
        arr2.add(nums[1]);

        int gc1,gc2;
        for(int i=2;i<nums.length;i++){
            rk = m.get(nums[i]);

            gc1 = BT1.sumRanges(rk,rank-1-1);
            gc2 = BT2.sumRanges(rk,rank-1-1);

            if(gc1>gc2 || gc1==gc2 && arr1.size()<=arr2.size()){
                arr1.add(nums[i]);
                BT1.update(rk-1,BT1.nums[rk-1]+1);
            }else{
                arr2.add(nums[i]);
                BT2.update(rk-1,BT2.nums[rk-1]+1);
            }
        }

        int[] result = new int[nums.length];
        for (int i = 0; i < arr1.size(); i++) {
            result[i] = arr1.get(i);
        }
        for (int i = 0; i < arr2.size(); i++) {
            result[i+arr1.size()] = arr2.get(i);
        }

        return result;
    }
}

喜欢我手搓树状数组嘛。

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值