📌  相关文章
📜  二进制索引树:范围更新和点查询(1)

📅  最后修改于: 2023-12-03 14:49:02.492000             🧑  作者: Mango

二进制索引树:范围更新和点查询

二进制索引树,也称为树状数组,是一种用于支持快速范围查询(Range Query)和单点更新(Point Update)的数据结构。它常常用于解决各种算法竞赛中的问题,比如求逆序对、区间和等。

定义

二进制索引树是一个数组,它的下标从1开始,数组中的每个元素代表一段区间内元素的和。这里以求区间和为例,假设我们有一个长度为N的数组arr,其下标从1到N,二进制索引树的定义如下:

int bit[N]; // bit[i]表示arr[1]到arr[i]的区间和
前缀和与差分

为了更好地理解二进制索引树的实现,我们先介绍两个概念:前缀和(Prefix Sum)和差分(Difference)。

前缀和是指数组中每个元素前面所有元素的和,我们用S[i]表示前缀和,则:

S[i] = arr[1] + arr[2] + ... + arr[i]

对于一个区间[L,R]的和,我们可以用前缀和数组S求得:

sum[L,R] = S[R] - S[L-1]

将上式变形得到:

S[R] = sum[1,R] = arr[1] + arr[2] + ... + arr[R]
S[L-1] = sum[1,L-1] = arr[1] + arr[2] + ... + arr[L-1]
sum[L,R] = S[R] - S[L-1] = arr[1] + arr[2] + ... + arr[R] - arr[1] - arr[2] - ... - arr[L-1] = arr[L] + arr[L+1] + ... + arr[R]

所以,我们可以先求出前缀和数组S,然后根据S计算任意区间的和。

差分是前缀和的逆运算,它将一个数组变形为另一个数组,使得后者的前缀和等于前者的差分。

假设我们有一个长度为N的数组arr,其下标从1到N,那么差分数组d[i]的定义如下:

d[i] = arr[i] - arr[i-1], (i>1)
d[1] = arr[1]

我们可以通过差分数组d求得原数组arr的前缀和数组S:

S[0] = 0;
for (int i = 1; i <= N; i++) {
    S[i] = S[i-1] + d[i];
}
更新操作

二进制索引树支持单点更新操作,也就是将原数组中的一个元素改变后,重新计算前缀和数组S。为了方便起见,我们假设要更新的元素下标为i,且将元素增加了v。

我们可以通过以下代码实现单点更新:

void update(int i, int v) {
    while (i <= N) {
        bit[i] += v;
        i += i & -i;
    }
}

update函数采用了循环方式,每次将当前位置的值增加v,然后将i向前移动i的二进制表示中最后一个非零位。为了更好地理解这段代码,我们需要了解以下两个概念:

  1. 二进制表示中最后一个非零位:假设i的二进制表示为1101010,那么i的二进制表示中最后一个非零位是2(其二进制为10)。
  2. i & -i:i的二进制表示中最后一个非零位及其之后的所有位构成的数值。

以i = 6为例,i的二进制为110,二进制中最后一个非零位是2,所以i & -i的值为2。如果i的二进制中没有非零位,那么i & -i的值为1(即i = 0时)。

在update函数中,我们依次更新bit[i],bit[i + (i&-i)],bit[i + (i&-i)*2],直到i+N。假设i的二进制表示为b1b2b3...bn,那么做完最后一次更新后,i会变成i + 2^(n-1)。

例如,在i = 6时,我们需要更新bit[6]、bit[8]和bit[16],得到:

update(6, 3); // 将bit[6]加3
update(8, 3); // 将bit[8]加3
update(16, 3); // 将bit[16]加3
查询操作

二进制索引树支持范围查询操作,也就是查询一个区间[L,R]的和。我们可以通过以下代码实现范围查询:

int query(int i) {
    int res = 0;
    while (i > 0) {
        res += bit[i];
        i -= i & -i;
    }
    return res;
}

query函数采用了循环方式,每次将当前位置的值累加到res中,然后将i向前移动i的二进制表示中最后一个非零位。最后返回res即为[L,R]的和。

例如,在i = 8时,我们需要查询[1,8]的和,得到:

int sum = query(8); // sum的值为bit[1]+bit[4]+bit[8]
范围更新

除了单点更新,二进制索引树还支持范围更新操作,也就是将一个区间[L,R]的所有元素加上v。我们可以利用差分数组实现范围更新,在差分数组d中,我们将[d[L],d[R+1])的元素都增加v,然后重新计算前缀和数组S。具体来说,我们可以通过以下代码实现范围更新:

void update_range(int L, int R, int v) {
    update(L, v);
    update(R + 1, -v);
}

update_range函数实际上是通过单点更新来实现范围更新的。首先将L处的元素加上v,然后将R+1处的元素减去v,这样d[L]会增加v,d[R+1]会减少v。最后,我们可以通过前缀和数组S求得更新后的原数组,例如:

update_range(2, 4, 2); // 将arr[2]、arr[3]、arr[4]分别加2
计算复杂度

在二进制索引树中,update操作和query操作的时间复杂度均为O(logN),其中N为数组长度。因此,二进制索引树在处理区间查询以及单点修改时表现出很好的效率,它比线段树更加简单,且实现容易,是一种优秀的数据结构。

总结

本文介绍了二进制索引树及其常见应用,包括单点修改、范围修改和区间查询等。通过本文的学习,相信大家已经对二进制索引树有了更深入的了解,希望本文能对大家学习和使用二进制索引树提供一些帮助。