【算法笔记】树状数组IndexTree

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的实现

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);
        }

    }

后记

个人学习总结笔记,不能保证非常详细,轻喷

相关推荐
sonadorje2 小时前
ECC公钥生成过程
算法·安全
声声codeGrandMaster2 小时前
线性回归实战下与深度学习概念
深度学习·算法·线性回归
Paddy哥2 小时前
java 经典循环依赖解决
java
sin_hielo2 小时前
leetcode 2092(排序+bfs)
算法·leetcode·宽度优先
2 小时前
TIDB——PD(placement Driver)
java·数据库·分布式·tidb·
TG:@yunlaoda360 云老大2 小时前
配置华为云国际站代理商OBS跨区域复制时,如何编辑委托信任策略?
java·前端·华为云
计算机毕设指导62 小时前
基于微信小程序的鸟博士系统【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·maven
C雨后彩虹2 小时前
斗地主之顺子
java·数据结构·算法·华为·面试
CC.GG2 小时前
【C++】AVL树
java·开发语言·c++