树状数组 java_树状数组:萌新的个人理解(1)

归航return:树状数组:萌新的个人理解(0)​zhuanlan.zhihu.com15ec876b445883da657f17518a8ab77a.png

回顾

在上一部分中,我们回顾了经典的前缀和问题的思路,包括在最平凡的前缀和思想和使用平方根作为分块大小的思想。使用最平凡的前缀和思想,查询前缀和的时间复杂度是

equation?tex=O%5Cleft%281%5Cright%29 ,修改某个位置的数的时间复杂度是

equation?tex=O%5Cleft%28n%5Cright%29 。如果使用数组大小的平方根作为分块大小,那么查询前缀和的时间复杂度是

equation?tex=O%5Cleft%28%5Csqrt%7Bn%7D%5Cright%29 ,修改某个位置的数的时间复杂度是

equation?tex=O%5Cleft%28%5Csqrt%7Bn%7D%5Cright%29 。但是事实上,有一种针对此类单点查询和修改更快的数据结构,这就是:树状数组。

图源:OI Wiki(即上方超链接)

引理可以使用以下函数获取一个整数的二进制表示中的最低的 1 对应的位置:

int lowBit(int x){

if (x == INT_MIN){

return INT_MIN;

}

return x & (-x);

}//C++,if you use Java, you should use Integer.MIN_VALUE instead.

证明如下:如果输入的整数是 INT_MIN,它的二进制表示是 0x80000000,正好最高位就是一个 1,因此返回本身就是最低的 1 对应的位置;如果输入的整数是 0,它的二进制表示是 0x00000000,一个 0 都没有,返回结果也正好是 0,是自洽的。

在其他的情况下:注意到:-x 这个运算在二进制上的本质是首先将原始数据逐个取反,最后加上 1。因此,如果这个整数的二进制表示结尾是 1,那么我们可以想像,反转之后再加上 1,只有最后一位的那个数字才是 1,其他的结果都被反转了,因此这个时候正好就应该返回的是 1。假设这个数字的二进制表示有若干个 0 结尾,例如:10000,那么反转之后再加上 1,正好可以进位,使得最后一个 1 对应的位置还是 1,而这个位置前面的相与的性质和前一种情况类似,因此也只有这个位置上的 1 保留下来。

综上所述,这个函数是正确的。

2.lowBit 函数的基本性质:

equation?tex=lowBit%5Cleft%282%5E%5Clambda+x%5Cright%29+%3D+2%5E%5Clambda+lowBit%5Cleft%28x%5Cright%29 。(不考虑整数溢出问题)

只需要简单地使用位运算左移的思想就可以理解这个结果。

3. 任何一个下标范围闭区间

equation?tex=%5Cleft%5Ba%2Cb%5Cright%5D 上的数组和可以拆分成

equation?tex=%5Cleft%5Ba%2C+m%5Cright%5D

equation?tex=%5Cleft%5Bm%2B1%2C+b%5Cright%5D 上的和。

equation?tex=%5Csum_%7Bi%3Da%7D%5Eb+arr%5Cleft%5Bi%5Cright%5D+%3D+%5Csum_%7Bi%3Da%7D%5Em+arr%5Cleft%5Bi%5Cright%5D+%2B+%5Csum_%7Bi+%3D+%7Bm%2B1%7D%7D%5Ebarr%5Cleft%5Bi%5Cright%5D%5Ctag%7B0%7D+

这个只需要使用简单的求和公式即可验证,这是显然的。

基本思想

首先,暂时约定数组的下标从 1 开始,然后在最后代码实现的时候,我们再将其还原到从 0 开始。

任何一个正整数都可以借助其二进制表示,表示成若干个二的非负整数次幂之和。根据前缀和的基本思路,只要我们能求出这个数组中从开始的位置(也就是 a[1] )到给定位置(也就是 a[n])的和,那么其他的情况也就都解决了。假设

equation?tex=n 的二进制表示是这样的:

equation?tex=n+%3D+%5Csum_%7Bi%3D1%7D%5Ej+2%5E%7Ba_i%7D%5Ctag%7B1%7D

WLOG,规定上述的

equation?tex=a_i 都是降序排列的。例如,当

equation?tex=n%3D18 的时候,因为

equation?tex=18%3D2%5E4%2B2%5E1 ,因此

equation?tex=a_1%3D4

equation?tex=a_2%3D1 。那么在上述基础上,我们就可以将计算 a[1] 到 a[n] 的和的过程,拆分成

equation?tex=j 个长度均为 2 的幂的子区间的和的求和问题。也就是说:

equation?tex=f%5Cleft%281%2C+n%5Cright%29+%3D+f%5Cleft%281%2C+2%5E%7Ba_1%7D%5Cright%29+%2B+f%5Cleft%282%5E%7Ba_1%7D%2B1%2C+2%5E%7Ba_1%7D%2B2%5E%7Ba_2%7D%5Cright%29+%2B+...+%2B+f%5Cleft%281%2B%5Csum_%7Bi%3D1%7D%5E%7Bj-1%7D2%5E%7Ba_i%7D%2C+n%5Cright%29%5Ctag%7B2%7D

最后一个地方是 n 是因为这个地方正好就的确是

equation?tex=%5Csum_%7Bi%3D1%7D%5Ej+2%5E%7Ba_i%7D 的结果。很容易知道,上述公式

equation?tex=%5Cleft%281%5Cright%29 中,我们最多要计算

equation?tex=O%5Cleft%28%5Clog+n%5Cright%29 个求和的表达式。以

equation?tex=n+%3D+9 为例,那么就有:

equation?tex=f%5Cleft%281%2C+9%5Cright%29+%3D+f%5Cleft%281%2C+8%5Cright%29+%2B+f%5Cleft%289%2C+9%5Cright%29

由于我们规定

equation?tex=a_i 是降序排列的,因此我们发现,上述每个区间的右端点的 lowBit 函数,正好就是这个区间内包含的整数的个数(也就是右端点减去左端点然后加上 1,这个 1 千万不能忘记,这属于典型的 off-by-one-error)。因此,如果我们维护一个额外数组 assist(下标仍然从 1 开始),规定 assist[i] 代表的是 [i-lowBit(i)+1, i] 上区间的和,那么我们便可以使用循环迭代的方法,得到 [1, i] 范围内的前缀和。下面是 C++ 风格的伪代码——因为数组下标并不是从 1 开始。

int getSum(const vector& arr, int idx){

int ans = 0;

while (idx > 0){

ans += arr[idx];

idx -= lowBit(idx);

}

return ans;

}//C++ style pseudocode, cause the index in real std::vector starts from 0.

这就完成了求和的过程,时间复杂度是

equation?tex=O%5Cleft%28%5Clog+n%5Cright%29

对于动态维护前缀和的问题,我们不仅关心的是求和问题,更关心的是单点修改问题,假设修改的下标是 x,其中下标仍然暂时规定从 1 开始,修改的差值为 diff。那么,哪些下标对应的求和在上述范围内呢?我们已经指出,下标为 y 的 assist 数组,对应的是原始数组中:[y-lowBit(y)+1, y]范围内的求和,那么这里实际上就需要解一个不等式:

equation?tex=%5Cleft%5C%7B%5Cbegin%7Baligned%7D+%26y%5Cgeq+x%5C%5C+%26y-lowBit%5Cleft%28y%5Cright%29%2B1%5Cleq+x%5C%5C+%5Cend%7Baligned%7D%5Cright.%5Ctag%7B3%7D

显然,

equation?tex=y+%3D+x 是一个解,因为只要

equation?tex=y 是一个正整数,就必然有

equation?tex=lowBit%28y%29+%5Cgeq+1 。定义迭代:

equation?tex=f%5Cleft%28x%5Cright%29+%3D+x%2BlowBit%5Cleft%28x%5Cright%29 ,规定

equation?tex=g%5Cleft%28x%5Cright%29+%3D+x-lowBit%5Cleft%28x%5Cright%29

我们来证明:

equation?tex=g%5Cleft%28f%5Cleft%28x%5Cright%29%5Cright%29+%5Cleq+g%5Cleft%28x%5Cright%29 。由上面的 lowBit 函数的基本性质可以知道,WLOG,我们只需考虑当

equation?tex=x 为奇数的情况,若不然,只需要使用

equation?tex=%5Cfrac%7Bx%7D%7BlowBit%5Cleft%28x%5Cright%29%7D 代替

equation?tex=x 即可转化为这种情况。此时有:

equation?tex=f%5Cleft%28x%5Cright%29+%3D+x%2B1

equation?tex=g%5Cleft%28x%5Cright%29+%3D+x-1 ,于是我们只需证明:

equation?tex=g%5Cleft%28x%2B1%5Cright%29+%5Cleq+x-1 即可。显然,由于

equation?tex=x%2B1 是偶数,那么必然有

equation?tex=lowBit%5Cleft%28x%5Cright%29%5Cgeq+2 ,于是这就证明完毕了。 定义

equation?tex=f_0%5Cleft%28x%5Cright%29+%3D+x

equation?tex=f_i%5Cleft%28x%5Cright%29+%3D+f%5Cleft%28f_%7Bi-1%7D%5Cleft%28x%5Cright%29%5Cright%29 ,根据上述结论就可以得到:

equation?tex=%5Cbegin%7Baligned%7D+%26f_0%5Cleft%28x%5Cright%29%3C+f_1%5Cleft%28x%5Cright%29+%3C+f_2%5Cleft%28x%5Cright%29+%3C+...%5C%5C+%26g%5Cleft%28f_0%5Cleft%28x%5Cright%29%5Cright%29+%5Cgeq+g%5Cleft%28f_1%5Cleft%28x%5Cright%29%5Cright%29+%5Cgeq+g%5Cleft%28f_2%5Cleft%28x%5Cright%29%5Cright%29+%5Cgeq+...++%5Cend%7Baligned%7D%5Ctag%7B4%7D

于是这些

equation?tex=f_i%5Cleft%28x%5Cright%29 中都包含了对下标为

equation?tex=x 的求和情况,需要相应加上对 a[x] 的修改值 diff。

下面还要证明,只有这样的下标才包含了针对下标为

equation?tex=x 的修改情况。任何一个大于

equation?tex=x 的下标

equation?tex=x_0 ,且不是上述

equation?tex=f_i%5Cleft%28x%5Cright%29 中的下标,一定存在一个非负整数

equation?tex=n_0 ,使得

equation?tex=f_%7Bn_0%7D%5Cleft%28x%5Cright%29+%3C+x_0+%3C+f_%7Bn_0%2B1%7D%5Cleft%28x%5Cright%29 。此时必然有

equation?tex=f_%7Bn_0%7D%5Cleft%28x%5Cright%29 是偶数,否则这个区间内不存在

equation?tex=x_0 。我们来证明:

equation?tex=g%5Cleft%28x_0%5Cright%29+%5Cgeq+f_%7Bn_0%7D%5Cleft%28x%5Cright%29

首先:

equation?tex=lowBit%5Cleft%28x_0%5Cright%29+%3C+lowBit%5Cleft%28f_%7Bn_0%7D%5Cleft%28x%5Cright%29%5Cright%29 。若不然,这意味着

equation?tex=lowBit%5Cleft%28f_%7Bn_0%7D%5Cleft%28x%5Cright%29%5Cright%29 对应的位遭到了修改,因而这个时候一定有

equation?tex=x_0+%5Cgeq++f_%7Bn_0%7D%5Cleft%28x%5Cright%29+%2B+lowBit%5Cleft%28f_%7Bn_0%7D%5Cleft%28x%5Cright%29%5Cright%29+%3D+f_%7Bn_0%2B1%7D%5Cleft%28x%5Cright%29 ,这是不符合要求的。

那么,在这种情况下,无论如何运行,

equation?tex=lowBit%5Cleft%28f_%7Bn_0%7D%5Cleft%28x%5Cright%29%5Cright%29 和更高的二进制位,都不会发生变动,因此的确有

equation?tex=g%5Cleft%28x_0%5Cright%29+%5Cgeq+f_%7Bn_0%7D%5Cleft%28x%5Cright%29 ,也就意味着

equation?tex=g%5Cleft%28x_0%5Cright%29%2B1+%3E+f_n%5Cleft%28x%5Cright%29+%5Cgeq+f_0%5Cleft%28x%5Cright%29+%3D+x ,因此其他的下标都不符合要求。

综上所述,我们得到了更新的伪代码如下:

void update(vector& arr, int idx, int diff){

while (idx <= arr.size()){

arr[idx] += diff;

idx += lowBit(idx);

}

}//C++ style pseudocode, cause the index in real std::vector starts from 0.

具体实现

在上述的解释中,我们证明了树状数组的实现原理,接下来让我们具体来实现这个数据结构。

C++ 代码如下:

#include using namespace std;

class BinaryIndexTree{

private:

vector prefixSumArr;

static int lowBit(int x){

if (x == INT_MIN){

return INT_MIN;

}

return x & (-x);

}

public:

BinaryIndexTree(const vector& arr){

prefixSumArr = vector(arr.size()+1);

for (int i = 0; i < arr.size(); ++i){

add(i, arr[i]);

}

}

void add(int idx, int diff){

++idx;

while (idx < prefixSumArr.size()){

prefixSumArr[idx] += diff;

idx += lowBit(idx);

}

}

int getSum(int left, int right){

return getSum(right)-getSum(left);

}

int getSum(int idx){

int ans = 0;

while (idx > 0){

ans += prefixSumArr[idx];

idx -= lowBit(idx);

}

return ans;

}

};

Java 代码如下:

import java.util.*;

class PrefixSumSupport{

public static int lowBit(int x){

return x & (-x);

}

public static long lowBit(long x){

return x & (-x);

}

}

class BinaryIndexTree {

private int[] prefixSum;

private int length;

private int[] arr;

public BinaryIndexTree(final int[] arr){

resetArr(arr);

}

public BinaryIndexTree(){

this(new int[0]);

}

public BinaryIndexTree(final ArrayList _arr){

resetArr(_arr.stream().mapToInt(i -> i).toArray());

}

public void modify(int idx, int diff){

checkBoundary(idx, length);

arr[idx++] += diff;

while (idx <= length){

prefixSum[idx] += diff;

idx += PrefixSumSupport.lowBit(idx);

}

}

public int returnSum(int begin, int end){

if (begin > end){

throw new IllegalStateException();

}

checkBoundary(begin, length+1);

checkBoundary(end, length+1);

if (begin == end){

return 0;

}

return returnSum(end)-returnSum(begin);

}

private int returnSum(int idx){

int ret = 0;

while (idx > 0){

ret += prefixSum[idx];

idx -= PrefixSumSupport.lowBit(idx);

}

return ret;

}

public void showArr(){

System.out.println(Arrays.toString(arr));

}

public void showPrefix(){

System.out.println(Arrays.toString(prefixSum));

}

public int[] getPrefixSum(){

return Arrays.copyOf(prefixSum, length+1);

}

public int[] getArr(){

return Arrays.copyOf(arr, length);

}

public void resetArr(final int[] arr){

length = arr.length;

prefixSum = new int[arr.length+1];

this.arr = new int[length];

for (int i = 0; i < length; ++i){

modify(i, arr[i]);

}

}

private void checkBoundary(int parameter, int limit){

if (parameter < 0 || parameter >= limit){

throw new ArrayIndexOutOfBoundsException(String.format("Your input is %d, which is out of the limit %d\n", parameter, limit));

}

}

@Override

public String toString(){

StringBuffer SB = new StringBuffer();

SB.append("Original array: ");

SB.append(Arrays.toString(arr));

SB.append("\n");

SB.append("Prefix sum array: ");

SB.append(Arrays.toString(prefixSum));

return SB.toString();

}

}

本质上是一样的,只不过 Java 多了更多的边界检查。接下来简要解释函数的原理,以 C++ 版本为例:

构造函数 BinaryIndexTree(const vector& arr)

按照惯例,通常前缀和数组的长度,设定为输入数组的长度 +1,能大幅简化问题的边界细节,这里也不例外,分配好空间之后,就将元素逐个逐个加入到树状数组中,这里实际上有一种时间复杂度

equation?tex=O%5Cleft%28n%5Cright%29 的方法,不过我为了让代码看起来更简单,使用的是 trivial 的时间复杂度是

equation?tex=O%5Cleft%28n%5Clog+n%5Cright%29 的方法进行构造过程。

修改函数 void add(int idx, int diff)

树状数组中,数组下标的定义被规定从 1 开始,因此这里我们需要首先做一个偏移量,令其 +1,然后才能使用伪代码里面提到的方法来修改所有波及到的下标之处。

查询求和函数 int getSum(int left, int right) 和 int getSum(int idx)

这里的查询函数,都是左闭右开区间形式的定义,其中第二个函数相当于 left = 0, right = idx 的情况。这里使用左闭右开区间,能让边界条件的处理变得十分自然。 getSum(int idx) 函数,求出来的是转换下标之后

equation?tex=%5Cleft%5B1%2C+idx%5Cright%5D 范围内所有元素的和,也就是原来数组下标中

equation?tex=%5Cleft%5B0%2C+idx-1%5Cright%5D 内的求和结果,也就是

equation?tex=%5Cleft%5B0%2C+idx%5Cright%29 的结果,不难想到,如果 idx == 0,这个时候正好就是自然地定义为结果是 0 了。那么将

equation?tex=%5Cleft%5B0%2C+right%5Cright%29

equation?tex=%5Cleft%5B0%2C+left%5Cright%29 范围内的求和结果相减,自然也就得到了

equation?tex=%5Cleft%5Bleft%2C+right%5Cright%29 范围内的求和结果。

离散化

在 LeetCode 中,如果一个题目需要用到树状数组,那么还通常需要利用离散化的思想方法,就是将数组中的元素排序,然后去重。这个动作可以简单地使用一个哈希表和一个排序函数即可完成,时间复杂度为

equation?tex=O%5Cleft%28n%5Clog+n%5Cright%29 ​。代码如下:

template

vector discretization(const vector& input){

unordered_set duplicationRemoval(input.begin(), input.end());

vector output(duplicationRemoval.begin(), duplicationRemoval.end());

sort(output.begin(), output.end());

return output;

}

针对非 C++ 语言,上述代码已经足够。但是由于 unordered_set 自带大常数,因此如果是 C++,还可以使用 C++ 标准算法库 中提供的 std::unique 函数,文档在std::unique - cppreference.com​zh.cppreference.com

这个操作的时间复杂度仍然是

equation?tex=O%28n%5Clog+n%29 ,但由于实际的常数问题(unordered_set 自带大常数,上文已经提及到了),这个算法是跑起来快了不少。一个简单的 unique 函数的 Java 实现:

class MySTLAlgorithmToJava{

public static int unique(int[] arr, int from, int to){

if (from == to){

return from;

}

int ans = from;

while (++from != to){

if (!(arr[ans] == arr[from])){

arr[++ans] = arr[from];

}

}

return ++ans;

}

}

使用 unique 函数之后,代码可以变成这样:

template

vector discretization(const vector& input){

vector output(input);

sort(output.begin(), output.end());

auto it = unique(output.begin(), output.end());

output.erase(it, output.end());

return output;

}

如果要求输入和输出数组类型不同,那么代码可以这样:

template

vector discretization(const vector& input, const outputType& typeIndicator){

vector output(input.begin(), input.end());

sort(output.begin(), output.end());

auto it = unique(output.begin(), output.end());

output.erase(it, output.end());

return output;

}

几道例题

LeetCode 307307. 区域和检索 - 数组可修改 - 力扣(LeetCode)​leetcode-cn.com

直接应用上述模版即可。Java 代码如下:

class PrefixSumSupport{...}

class BinaryIndexTree {...}

class NumArray {

BinaryIndexTree Solution;

int[] numsCopy;

public NumArray(int[] nums) {

Solution = new BinaryIndexTree(nums);

numsCopy = Arrays.copyOf(nums, nums.length);

}

public void update(int i, int val) {

int diff = val-numsCopy[i];

numsCopy[i] = val;

Solution.modify(i, diff);

}

public int sumRange(int i, int j) {

int ret = Solution.returnSum(i, j+1);

return ret;

}

}

省略号内容直接复制上述 Java 实现即可。

LeetCode 315315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)​leetcode-cn.com

逆序数问题。C++ 代码如下:

class BinaryIndexTree{...};

class Solution {

public:

vector countSmaller(vector& nums) {

if (nums.size() == 0){

return {};

}

vector discretization = discrete(nums);

vector ans(nums.size(), 0);

BinaryIndexTree Helper(vector(discretization.size()));

for (int i = nums.size()-1; i >= 0; --i){

int thisIdx = lower_bound(discretization.begin(), discretization.end(), nums[i])-discretization.begin();

ans[i] = Helper.getSum(thisIdx);

//because we want to get count of existence less than current number,//so thisIdx is literally correct, if the problem is no more than,//line 12 should be replaced by calling upper_bound function in Helper.add(thisIdx, 1);

}

return ans;

}

private:

vector discrete(const vector& input){

unordered_set duplicationRemoval;

for (const auto & x : input){

duplicationRemoval.insert(x);

}

vector output;

output.reserve(duplicationRemoval.size());

for (const auto & x : duplicationRemoval){

output.emplace_back(x);

}

sort(output.begin(), output.end());

return output;

}

};

LeetCode 327327. 区间和的个数 - 力扣(LeetCode)​leetcode-cn.com

这里因为需要知道的是给定区间范围内的前缀和结果求和,而且是闭区间上的结果,因此为了转换成左闭右开区间,求和的下界我们应该使用 lower_bound 函数,而上界应当使用 upper_bound 函数求解。另外,这个题目如果直接使用 int 类型,会溢出,改成 long 才行。

class BinaryIndexTree{

private:

vector prefixSumArr;

static int lowBit(int x){

if (x == INT_MIN){

return INT_MIN;

}

return x & (-x);

}

public:

BinaryIndexTree(const vector& arr){

prefixSumArr = vector(arr.size()+1);

for (int i = 0; i < arr.size(); ++i){

add(i, arr[i]);

}

}

void add(int idx, int diff){

++idx;

while (idx < prefixSumArr.size()){

prefixSumArr[idx] += diff;

idx += lowBit(idx);

}

}

long getSum(int left, int right){

return getSum(right)-getSum(left);

}

long getSum(int idx){

long ans = 0;

while (idx > 0){

ans += prefixSumArr[idx];

idx -= lowBit(idx);

}

return ans;

}

};

class Solution {

public:

int countRangeSum(vector& nums, int lower, int upper) {

if (nums.size() == 0){

return 0;

}

vector prefixSum = prefixSumGeneration(nums);

vector discreted = discretization(prefixSum);

BinaryIndexTree Helper(vector(discreted.size()));

int ans = 0;

for (int i = 0; i < prefixSum.size(); ++i){

long curSum = prefixSum[i];

int curIdx = lower_bound(discreted.begin(), discreted.end(), curSum)-discreted.begin();

int begin = lower_bound(discreted.begin(), discreted.end(), curSum-upper)-discreted.begin();

int end = upper_bound(discreted.begin(), discreted.end(), curSum-lower)-discreted.begin();

ans += Helper.getSum(begin, end);

Helper.add(curIdx, 1);

}

return ans;

}

private:

vector prefixSumGeneration(const vector& input){

vector output(input.size()+1);

for (int i = 0; i < input.size(); ++i){

output[i+1] = output[i]+input[i];

}

return output;

}

template

vector discretization(const vector& input){

unordered_set duplicationRemoval;

for (const auto & x : input){

duplicationRemoval.insert(x);

}

vector output;

output.reserve(duplicationRemoval.size());

for (const auto & x : duplicationRemoval){

output.emplace_back(x);

}

sort(output.begin(), output.end());

return output;

}

};

LeetCode 493493. 翻转对 - 力扣(LeetCode)​leetcode-cn.com

同样需要注意 int 溢出问题。

class BinaryIndexTree{...

};

class Solution {

public:

int reversePairs(vector& nums) {

if (nums.size() <= 1){

return 0;

}

vectordiscreted = discretization(nums);

int ans = 0;

BinaryIndexTree Helper(vector(nums.size()));

for (int i = 0; i < nums.size(); ++i){

int thisIdx = lower_bound(discreted.begin(), discreted.end(), 1L*nums[i])-discreted.begin();

int targetIdx = upper_bound(discreted.begin(), discreted.end(), 2L*nums[i])-discreted.begin();

ans += Helper.getSum(targetIdx, discreted.size());

Helper.add(thisIdx, 1);

}

return ans;

}

private:

vector discretization(const vector& input){

unordered_set removalDuplicate;

for (auto x : input){

removalDuplicate.insert(x);

}

vector output;

for (int x : removalDuplicate){

output.emplace_back(x);

}

sort(output.begin(), output.end());

return output;

}

};

总之,在给定区间范围内的二分求范围,如果左边是闭区间,那么就用 lower_bound 函数,否则用 upper_bound 函数作为左闭右开区间的下界。如果右边是闭区间,就用 upper_bound 函数作为左闭右开区间的上界,否则就是 lower_bound ,这样可以保证我们可以方便地调用库函数,我用 Java 刷题的时候也自己写了一个类似的二分查找库,原封不动地实现了 STL 这几个二分函数,也同样支持泛型数组,自定义比较器的使用。

其他语言的实现:

Javascript:windliang:二叉索引树(树状数组)的原理​zhuanlan.zhihu.com83ccb77d2707559b829e0299e4128a86.png

拓展

我们在查询求和的过程中,将下标拆分成若干个二进制的表示,使用的是 lowBit 函数,那么很容易对偶地猜测,highBit——也就是返回二进制最高位的 1 的函数,是否能对偶地实现同样的功能呢?经过我的思考,发现不是很可行,因为这么做之后,会让修改过程的时间复杂度变得不可接受,具体证明留做习题。

EOF。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
树状数组(Fenwick Tree)是一种用于快速维护数组前缀和的数据结构。它可以在 $O(\log n)$ 的时间内完成单点修改和前缀查询操作,比线段树更加简洁高效。 下面是 Java 实现的树状数组详解: 首先,在 Java 中我们需要使用数组来表示树状数组,如下: ``` int[] tree; ``` 接着,我们需要实现两个基本操作:单点修改和前缀查询。 单点修改的实现如下: ``` void update(int index, int value) { while (index < tree.length) { tree[index] += value; index += index & -index; } } ``` 该函数的参数 `index` 表示要修改的位置,`value` 表示修改的值。在函数内部,我们使用了一个 `while` 循环不断向上更新树状数组中相应的节点,直到到达根节点为止。具体来说,我们首先将 `tree[index]` 加上 `value`,然后将 `index` 加上其最后一位为 1 的二进制数,这样就可以更新其父节点了。例如,当 `index` 为 6 时,其二进制表示为 110,最后一位为 2^1,加上后变为 111,即 7,这样就可以更新节点 7 了。 前缀查询的实现如下: ``` int query(int index) { int sum = 0; while (index > 0) { sum += tree[index]; index -= index & -index; } return sum; } ``` 该函数的参数 `index` 表示要查询的前缀的结束位置,即查询 $[1, index]$ 的和。在函数内部,我们同样使用了一个 `while` 循环不断向前查询树状数组中相应的节点,直到到达 0 为止。具体来说,我们首先将 `sum` 加上 `tree[index]`,然后将 `index` 减去其最后一位为 1 的二进制数,这样就可以查询其前一个节点了。例如,当 `index` 为 6 时,其二进制表示为 110,最后一位为 2^1,减去后变为 100,即 4,这样就可以查询节点 4 的值了。 最后,我们还需要初始化树状数组,将其全部置为 0。初始化的实现如下: ``` void init(int[] nums) { tree = new int[nums.length + 1]; for (int i = 1; i <= nums.length; i++) { update(i, nums[i - 1]); } } ``` 该函数的参数 `nums` 表示初始数组的值。在函数内部,我们首先创建一个长度为 `nums.length + 1` 的数组 `tree`,然后逐个将 `nums` 中的元素插入到树状数组中。具体来说,我们调用 `update(i, nums[i - 1])` 来将 `nums[i - 1]` 插入到树状数组的第 `i` 个位置。 到此为止,我们就完成了树状数组的实现。可以看到,树状数组的代码比线段树要简洁很多,而且效率也更高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值