IndexTree是一种巧妙的数组结构,主要用于处理数组的单点更新和区间前缀和查询。它在解决动态前缀和问题方面非常优雅,代码简洁,并且在扩展到多维时比线段树更容易实现。
1、算法概述
-
IndexTree(树状数组) : IndexTree是一种巧妙的数组结构,主要用于处理数组的单点更新和区间前缀和查询。
-
它在解决动态前缀和问题方面非常优雅,代码简洁,并且在扩展到多维时比线段树更容易实现。
-
IndexTree的数组下标是从1开始的,所以本章讨论的所有下标都是从1开始,如果是处理从0开始的下标,可以在调用IndexTree的方法时,将下标+1转为从1开始即可。
-
区间前缀和查询的一般办法:
-
在一般数组中,如果要求数组的区间和,一般都是创建一个辅助的前缀和数组prefixSum,每个位置i放的数值是从0到i的所有值的和。
-
这样,prefixSum[i] - prefixSum[j-1] 就是从j到i的区间和。
-
这种方法在处理静态数组时很好用,但是如果原来的数组某个位置i的值发生了变化,那么prefixSum数组从i位置开始后面的所有值就都要进行更新。
-
所以,如果数组的某个位置的值会频繁发生变化,而需要频繁查询区间和,使用传统前缀和数组的方法就不太适用了,这个时候就需要用到IndexTree。
-
IndexTree的核心原理:
-
IndexTree 的核心思想仍然是构建一个辅助数组 tree(通常从下标1开始使用,下标0弃用),和传统前缀和不同的是,某个位置i不是存前面所有值的和,
-
而是存储一段特定区间的累加和。这个区间的划分规则由下标 i的二进制表示决定。通过这样的方法,在更新原始数组某个位置的时候,就不是更新i位置以后的所有值,
-
而是根据特定的计算方法,跳着更新对应位置的值即可。所以这个tree数组对于某个位置i的管辖范围就很有讲究了。
-
1、tree的二进制区间管辖范围:
- tree[i]管辖的区间是从 (i - (i & -i) + 1)到 i的累加和,其中 i & -i表示 i的二进制表示中最低位1所在的数。
- 换句话说,从 i的二进制形式中去掉最右边的1后再加上1,得到的数就是管辖范围的起始下标,i本身是结束下标。
- 例如,i = 12(二进制 1100),去掉最右边的1(在从右数第3位)得到 1000(8),再加1得 1001(9)。所以 tree[12]管辖 arr[9]到 arr[12]的累加和。
- 下表展示了前8个位置的管辖范围:
| i(十进制) | i(二进制) | 管辖的 arr区间范围 | 说明 |
|---|---|---|---|
| 1 | 0001 | arr[1] | 管辖自身 |
| 2 | 0010 | arr[1]~ arr[2] | 管辖 arr[1]~ arr[2] |
| 3 | 0011 | arr[3] | 管辖自身 |
| 4 | 0100 | arr[1]~ arr[4] | 管辖 arr[1]~ arr[4] |
| 5 | 0101 | arr[5] | 管辖自身 |
| 6 | 0110 | arr[5]~ arr[6] | 管辖 arr[5]~ arr[6] |
| 7 | 0111 | arr[7] | 管辖自身 |
| 8 | 1000 | arr[1]~ arr[8] | 管辖 arr[1]~ arr[8] |
-
2、i & -i 的作用:
- i & -i 表示 i的二进制表示中最低位1所在的数,用于提取一个整数二进制表示中最右边的1及其后面的0所代表的数值。
- 例如,如果i为200,其二进制和计算的结果如下:
- i : 0011001000
- i & -i: 0000001000
-
3、前缀和的查询(sum):O(logN)
- 通过对tree的处理,计算原始数组 arr中从位置1到位置 index的前缀和就成了如下过程:
- 初始化总和为0。从 index开始,不断将 tree[index]的值累加到总和中,然后执行 index = index - (index & -index),
- 即不断去掉二进制表示中最右边的1,直到 index为0。
-
4、单点增加操作(add,如果是更新,可以求出新的值和旧的值之间的差d,这样就将更新装换成了增加操作):O(logN)
- 从 index开始,将 tree[index]的值增加 d,然后执行 index = index + (index & -index),
- 即不断向当前索引加上最低位的1,直到 index超过数组长度 N。这样可以更新所有管辖了被修改位置 index的 tree数组中的区间。
-
5、区间查询:
- 得到从 L到 R的区间和,可以利用前缀和查询:sum(L, R) = sum® - sum(L-1)。
-
6、拓展到多维:
- 只需要扩展多维坐标即可,每一个维度的坐标都对应一个一维的IndexTree。
-
时间复杂度:O(logN),空间复杂度:O(N)
2、一维IndexTree的实现
java
/**
* 1维IndexTree
*/
public static class IndexTree1D {
private int[] tree;
private int N;
public IndexTree1D(int N) {
this.N = N;
// 0位置不用,所以长度要N+1
tree = new int[N + 1];
}
/**
* 查询从1到index的前缀和
*/
public int sum(int index) {
int ans = 0;
while (index > 0) {
ans += tree[index];
index -= index & -index;
}
return ans;
}
/**
* 查询从 L到 R的区间和
*/
public int sumRange(int L, int R) {
return sum(R) - sum(L - 1);
}
/**
* 在 index 位置增加d
*/
public void add(int index, int d) {
if (index <= 0 || index > N) {
return;
}
while (index <= N) {
tree[index] += d;
index += index & -index;
}
}
}
整体代码和测试如下:
java
/**
* IndexTree(树状数组) : IndexTree是一种巧妙的数组结构,主要用于处理数组的单点更新和区间前缀和查询。
* 它在解决动态前缀和问题方面非常优雅,代码简洁,并且在扩展到多维时比线段树更容易实现。
* IndexTree的数组下标是从1开始的,所以本章讨论的所有下标都是从1开始,如果是处理从0开始的下标,可以在调用IndexTree的方法时,将下标+1转为从1开始即可。
* <br>
* 区间前缀和查询的一般办法:
* 在一般数组中,如果要求数组的区间和,一般都是创建一个辅助的前缀和数组prefixSum,每个位置i放的数值是从0到i的所有值的和。
* 这样,prefixSum[i] - prefixSum[j-1] 就是从j到i的区间和。
* 这种方法在处理静态数组时很好用,但是如果原来的数组某个位置i的值发生了变化,那么prefixSum数组从i位置开始后面的所有值就都要进行更新。
* 所以,如果数组的某个位置的值会频繁发生变化,而需要频繁查询区间和,使用传统前缀和数组的方法就不太适用了,这个时候就需要用到IndexTree。
* <br>
* IndexTree的核心原理:
* IndexTree 的核心思想仍然是构建一个辅助数组 tree(通常从下标1开始使用,下标0弃用),和传统前缀和不同的是,某个位置i不是存前面所有值的和,
* 而是存储一段特定区间的累加和。这个区间的划分规则由下标 i的二进制表示决定。通过这样的方法,在更新原始数组某个位置的时候,就不是更新i位置以后的所有值,
* 而是根据特定的计算方法,跳着更新对应位置的值即可。所以这个tree数组对于某个位置i的管辖范围就很有讲究了。
* 1、tree的二进制区间管辖范围:
* tree[i]管辖的区间是从 (i - (i & -i) + 1)到 i的累加和,其中 i & -i表示 i的二进制表示中最低位1所在的数。
* 换句话说,从 i的二进制形式中去掉最右边的1后再加上1,得到的数就是管辖范围的起始下标,i本身是结束下标。
* 例如,i = 12(二进制 1100),去掉最右边的1(在从右数第3位)得到 1000(8),再加1得 1001(9)。所以 tree[12]管辖 arr[9]到 arr[12]的累加和。
* 下表展示了前8个位置的管辖范围:
* | i(十进制) | i(二进制) | 管辖的 arr区间范围 | 说明 |
* | --------- | ------- | ---------------- | ------------ |
* | 1 | 0001 | arr[1] | 管辖自身 |
* | 2 | 0010 | arr[1]~ arr[2] | 管辖 arr[1]~ arr[2] |
* | 3 | 0011 | arr[3] | 管辖自身 |
* | 4 | 0100 | arr[1]~ arr[4] | 管辖 arr[1]~ arr[4] |
* | 5 | 0101 | arr[5] | 管辖自身 |
* | 6 | 0110 | arr[5]~ arr[6] | 管辖 arr[5]~ arr[6] |
* | 7 | 0111 | arr[7] | 管辖自身 |
* | 8 | 1000 | arr[1]~ arr[8] | 管辖 arr[1]~ arr[8] |
* 2、i & -i 的作用:
* i & -i 表示 i的二进制表示中最低位1所在的数,用于提取一个整数二进制表示中最右边的1及其后面的0所代表的数值。
* 例如,如果i为200,其二进制和计算的结果如下:
* i : 0011001000
* i & -i : 0000001000
* 3、前缀和的查询(sum):O(logN)
* 通过对tree的处理,计算原始数组 arr中从位置1到位置 index的前缀和就成了如下过程:
* 初始化总和为0。从 index开始,不断将 tree[index]的值累加到总和中,然后执行 index = index - (index & -index),
* 即不断去掉二进制表示中最右边的1,直到 index为0。
* 4、单点增加操作(add,如果是更新,可以求出新的值和旧的值之间的差d,这样就将更新装换成了增加操作):O(logN)
* 从 index开始,将 tree[index]的值增加 d,然后执行 index = index + (index & -index),
* 即不断向当前索引加上最低位的1,直到 index超过数组长度 N。这样可以更新所有管辖了被修改位置 index的 tree数组中的区间。
* 5、区间查询:
* 得到从 L到 R的区间和,可以利用前缀和查询:sum(L, R) = sum(R) - sum(L-1)。
* 6、拓展到多维:
* 只需要扩展多维坐标即可,每一个维度的坐标都对应一个一维的IndexTree。
* <br>
* 时间复杂度:O(logN),空间复杂度:O(N)
*
*/
public class IndexTree {
/**
* 1维IndexTree
*/
public static class IndexTree1D {
private int[] tree;
private int N;
public IndexTree1D(int N) {
this.N = N;
// 0位置不用,所以长度要N+1
tree = new int[N + 1];
}
/**
* 查询从1到index的前缀和
*/
public int sum(int index) {
int ans = 0;
while (index > 0) {
ans += tree[index];
index -= index & -index;
}
return ans;
}
/**
* 查询从 L到 R的区间和
*/
public int sumRange(int L, int R) {
return sum(R) - sum(L - 1);
}
/**
* 在 index 位置增加d
*/
public void add(int index, int d) {
if (index <= 0 || index > N) {
return;
}
while (index <= N) {
tree[index] += d;
index += index & -index;
}
}
}
/**
* 暴力方法写的比较器
*/
public static class Comparator {
private int[] nums;
private int N;
public Comparator(int size) {
N = size + 1;
nums = new int[N + 1];
}
public int sum(int index) {
int ret = 0;
for (int i = 1; i <= index; i++) {
ret += nums[i];
}
return ret;
}
public int sumRange(int L, int R) {
int ret = 0;
for (int i = L; i <= R; i++) {
ret += nums[i];
}
return ret;
}
public void add(int index, int d) {
nums[index] += d;
}
}
public static void main(String[] args) {
int N = 1000;
int V = 100;
int testTime = 20000000;
IndexTree1D tree = new IndexTree1D(N);
Comparator test = new Comparator(N);
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
int index = (int) (Math.random() * N) + 1;
if (Math.random() <= 0.5) {
int add = (int) (Math.random() * V);
tree.add(index, add);
test.add(index, add);
} else {
if (tree.sum(index) != test.sum(index)) {
System.out.println("sum 错误!");
break;
}
int L = (int) (Math.random() * index);
if (tree.sumRange(L, index) != test.sumRange(L, index)) {
System.out.println("sumRange 错误!");
System.out.printf("L = %d, R = %d\n", L, index);
break;
}
}
}
System.out.println("测试结束");
}
}
3、二维IndexTree的实现
- 2维IndexTree
- 测试链接:https://leetcode.cn/problems/range-sum-query-2d-mutable
- 提交时直接提交 NumMatrix类即可
java
/**
* 思路:
* 2维IndexTree就是在横轴和纵轴两个方向上各自维护一个indexTree即可,
* 因为这道题要求的是要有个update方法,一般的indexTree是提供的add方法,
* 所以在调用update的时候,要将update的值先算出增量,然后用add的逻辑来操作indexTree,
* 所以本题目中要维护一个某个位置当前值的数组nums,用来记录原始的值,这样才能算出增量d
*/
public class NumMatrix {
// 2维的tree数组,横轴和纵轴各自维护一个indexTree
private int[][] tree;
// 2维的nums数组,用来记录原始的值
private int[][] nums;
// 横轴的长度
private int N;
// 纵轴的长度
private int M;
public NumMatrix(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) {
return;
}
N = matrix.length;
M = matrix[0].length;
// 下标从1开始
tree = new int[N + 1][M + 1];
// 记录原始数组
nums = new int[N][M];
// 初始化,因为原来是0,所以直接每个位置调用update方法即可
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
update(i, j, matrix[i][j]);
}
}
}
/**
* 更新矩阵中某个位置的值
*/
public void update(int row, int col, int val) {
if (row < 0 || row >= N || col < 0 || col >= M) {
return;
}
// 先算出增量d
int d = val - nums[row][col];
// 更新原始数组
nums[row][col] = val;
// 在横轴和纵轴两个indexTree上都要更新
for (int i = row + 1; i <= N; i += i & (-i)) {
for (int j = col + 1; j <= M; j += j & (-j)) {
tree[i][j] += d;
}
}
}
/**
* 获得从(0,0)到(row,col)的累加和
*/
public int sum(int row, int col) {
if (row < 0 || row >= N || col < 0 || col >= M) {
return 0;
}
int sum = 0;
for (int i = row + 1; i > 0; i -= i & (-i)) {
for (int j = col + 1; j > 0; j -= j & (-j)) {
sum += tree[i][j];
}
}
return sum;
}
public int sumRegion(int row1, int col1, int row2, int col2) {
if (N == 0 || M == 0) {
return 0;
}
// 在- sum(row1 - 1, col2) - sum(row2, col1 - 1)时减了两次sum(row1 - 1, col1 - 1),所以要将一次加回去
return sum(row2, col2) + sum(row1 - 1, col1 - 1) - sum(row1 - 1, col2) - sum(row2, col1 - 1);
}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷