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]的规则如下:
-
先把i转为二进制串,例如13D=1101B
-
从右往左(从小到大),对于二进制串的每一位,如果是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;
}
}
喜欢我手搓树状数组嘛。