简述树状数组

树状数组

树状数组(二元索引树 / 二元下标树 / Binary Indexed Tree, BIT / Fenwick Tree)

引入

树状数组是一种支持 单点修改区间查询 的,代码量小的数据结构。

注意


事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小,因此仍有学习价值。

有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的 区间加单点值区间加区间和 问题。


定义


对于大小为 n n n 的序列 n u m s nums nums ,最基本的树状数组以 O ( l o g n ) O(logn) O(logn) 时间复杂度同时支持如下两种操作。

  • 更新 n u m s nums nums 中单个元素的值,即单点修改
  • n u m s nums nums 任意区间的元素值之和,即区间查询

无论使用普通数组还是利用前缀和数组,对于上述两种操作,均有一种的时间复杂度为 O ( n ) O(n) O(n)。而树状数组通过维护一个与 n u m s nums nums 等大的,在逻辑上为树状结构 (一棵或多棵多叉树) t r e e [ ] tree[] tree[] ,使得两种操作的时间复杂度均为 O ( l o g n ) O(logn) O(logn)

序列操作数组前缀和树状数组
单点修改 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n) O ( l o g n ) O(logn) O(logn)
区间查询 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( l o g n ) O(logn) O(logn)

相关操作:

操作定义
单点修改 (Point Update, PU)修改 n u m s nums nums 的单个元素
单点查询 (Point Query, PQ)查询 n u m s nums nums 的单个元素
区间修改 (Range Update, RU)修改 n u m s nums nums 的某个区间(区间元素都加上同一个数)
区间查询 (Range Query, RQ) n u m s nums nums 的某个区间的区间和

操作

PURQ BIT(单改区查)

最基本的树状数组支持「单点修改 (PU)」和「区间查询 (RQ)」,即 PURQ BIT。


区间


树状数组这一数据结构,对输入数组 n u m s nums nums 划分为多个子区间,使得对任意的 [ 0 , k ] [0, k] [0,k] 前缀区间,都可以由划分结果的若干个连续的子区间构成,这些子区间的区间和相加即可得到 前缀区间和 ,对于任意区间 [ l , r ] [l,r] [l,r] ,将右界 [ 0 , r ] [0,r] [0,r] 前缀区间的区间和减去左界前一位的前缀区间 [ 0 , l − 1 ] [0,l-1] [0,l1] 的区间和,即为 [ l , r ] [l,r] [l,r] 区间和。

这就是树状数组能快速求解信息的原因:我们总能将一段前缀 [ 1 , n ] [1, n] [1,n] 拆成 不多于 l o g n logn logn 段区间,使得这 l o g n logn logn 段区间的信息是 已知的

于是,我们只需合并这 l o g n logn logn 段区间的信息,就可以得到答案。相比于原来直接 n n n 合并个信息,效率有了很大的提高。

下图展示了树状数组的工作原理:

在这里插入图片描述

最下面的八个方块代表原始数据数组 a a a 。上面参差不齐的方块(与最上面的八个方块是同一个数组)代表数组的上级—— c c c 数组。

c c c 数组就是用来储存原始数组 a a a 某段区间的和的,也就是说,这些区间的信息是已知的,我们的目标就是把查询前缀拆成这些小区间。

不难发现 c [ x ] c[x] c[x] 管辖的一定是一段右边界是 x x x 的区间总信息。无论怎么划分,一定有且只有一个以 x x x 为右界的子区间。

举例:计算 a [ 1...7 ] a[1...7] a[1...7] 的和。

过程:从 c 7 c_7 c7 开始往前跳,发现 c 7 c_7 c7 只管辖 a 7 a_7 a7 这个元素;然后找 c 6 c_6 c6 ,发现 c 6 c_6 c6 管辖的是 a [ 1...7 ] a[1...7] a[1...7] ,然后跳到 c 4 c_4 c4 ,发现 c 4 c_4 c4 管辖的是 a [ 1...4 ] a[1...4] a[1...4]这些元素,然后再试图跳到 c 0 c_0 c0 ,但事实上 c 0 c_0 c0 不存在,不跳了。

我们刚刚找到 c c c c 7 , c 6 , c 4 c_7,c_6,c_4 c7,c6,c4 ,事实上这就是 a [ 1...7 ] a[1...7] a[1...7] 拆分出的三个小区间,合并得到的答案是 c 7 + c 6 + c 4 c_7+c_6+c_4 c7+c6+c4

举例:计算 a [ 4...7 ] a[4...7] a[4...7] 的和。

在这里插入图片描述


区间划分


问题: c [ x ] ( x ≥ 1 ) c[x](x≥1) c[x](x1) 管辖的区间向左延伸多少?也就是说,区间长度是多少?

树状数组中,规定 c [ x ] c[x] c[x] 管辖的区间长度为 2 k 2^k 2k ,其中:

  • 设二进制最低位为第 0 0 0 位, 则 k k k 恰好为 x x x 二进制表示中,最低位的 1 1 1 所在的二进制位数;
  • 2 k 2^k 2k c [ x ] c[x] c[x] 的管辖区间长度)恰好为 x x x 二进制表示中,最低位的 1 1 1 以及后面所有 0 0 0 组成的数。

举例:

c 88 c_{88} c88 管辖的是哪个区间?

因为 8 8 ( 10 ) = 0101100 0 ( 2 ) 88_{(10)} = 01011000_{(2)} 88(10)=01011000(2),其二进制最低位的 1 以及后面的 0 组成的二进制是 1000 ,即 8 ,所以 c 88 c_{88} c88 管辖 8 个 a a a 数组的元素。因此 c 88 c_{88} c88 代表 a [ 81...88 ] a[81...88] a[81...88] 的区间信息。

我们记 x x x 二进制最低位 1 以及后面的 0 组成的数为 l o w b i t ( x ) lowbit(x) lowbit(x) ,那么 c [ x ] c[x] c[x] 管辖的区间就是 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x) + 1,x] [xlowbit(x)+1,x]

根据位运算知识,可以得到 lowbit(x) = x & -x

int lowbit(int x) {
  // x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
  // lowbit(0b01011000) == 0b00001000
  //          ~~~~^~~~
  // lowbit(0b01110010) == 0b00000010
  //          ~~~~~~^~
  return x & -x;
}

区间查询


只需将有关 l . . . r l...r l...r 的区间查询转化为 1... r 1...r 1...r 1... l − 1 1...l-1 1...l1 的前缀查询再差分。

查询 a [ 1... x ] a[1...x] a[1...x] 的过程:

  • c [ x ] c[x] c[x] 开始往前跳,有 c [ x ] c[x] c[x] 管辖 a [ x − l o w b i t ( x ) + 1... x ] a[x-lowbit(x)+1...x] a[xlowbit(x)+1...x]
  • x ← x − b o w b i t ( x ) x←x-bowbit(x) xxbowbit(x),如果 x = 0 x=0 x=0 说明已经跳到尽头了,终止循环;否则回到第一步。
  • 将跳到的 c c c 合并。

实现时,我们不一定要先把 c c c 都跳出来然后一起合并,可以边跳边合并。

int getsum(int x) {  // a[1]..a[x]的和
  int ans = 0;
  while (x > 0) {
    ans = ans + c[x];	// 边跳边合并。
    x = x - lowbit(x);
  }
  return ans;
}

单点修改


树状数组的一些基本性质:

  • 性质1:对于 x ≤ y x≤y xy ,要么有 c [ x ] c[x] c[x] c [ y ] c[y] c[y] 不交,要么有 c [ x ] c[x] c[x] 包含于 c [ y ] c[y] c[y]
  • 性质2:在 c [ x ] c[x] c[x] 真包含于 c [ x + l o w b i t ( x ) ] c[x+lowbit(x)] c[x+lowbit(x)]
  • 性质3:对于任意 x < y < x + l o w b i t ( x ) x<y<x+lowbit(x) x<y<x+lowbit(x) ,有 c [ x ] c[x] c[x] c [ y ] c[y] c[y] 不交。

只需遍历并修改管辖了 a [ x ] a[x] a[x] 的所有 c [ y ] c[y] c[y] ,因为其他的 c c c 显然没有发生变化。

管辖 a [ x ] a[x] a[x] c [ y ] c[y] c[y] 一定包含 c [ x ] c[x] c[x] (根据性质1),所以 y 在树状数组树形态上是 x 的祖先。因此我们从 x 开始不断跳父亲,直到跳得超过了原数组长度为止。

n n n 表示 a a a 的大小,单点修改 a [ x ] a[x] a[x] 的过程:

  • 初始化令 x " = x x^" = x x"=x
  • 修改 c [ x " ] c[x"] c[x"]
  • x " ← x " + l o w b i t ( x " ) x" ← x" + lowbit(x^") x"x"+lowbit(x") ,如果 x " > n x" > n x">n 说明已经跳到尽头了,终止循环;否则回到第二步。

下面以维护区间和,单点加为例给出实现。

void add(int x, int k) {
  while (x <= n) {  // 不能越界
    c[x] = c[x] + k;
    x = x + lowbit(x);
  }
}

**区间信息和单点修改的种类,共同决定 c [ x " ] c[x"] c[x"] 的修改方式。**下面给几个例子:

  • c [ x " ] c[x"] c[x"] 维护区间和,修改种类是将 a [ x ] a[x] a[x] 加上 p p p ,则修改方式则是将所有 c [ x " ] c[x"] c[x"] 也加上 p p p
  • c [ x " ] c[x"] c[x"] 维护区间积,修改种类是将 a [ x ] a[x] a[x] 乘上 p p p ,则修改方式则是将所有 c [ x " ] c[x"] c[x"] 也乘上 p p p

然而,单点修改的自由性使得修改的种类和维护的信息不一定是同种运算。

例如: c [ x " ] c[x"] c[x"] 维护区间和,修改种类是将 a [ x ] a[x] a[x] 赋值为 p p p ,可以考虑转化为将 a [ x " ] a[x"] a[x"] 加上 p − a [ x ] p-a[x] pa[x] 。如果是将 a [ x ] a[x] a[x] 乘上 p p p ,就考虑转化 a [ x ] a[x] a[x] 加上 a [ x ] × p − a [ x ] a[x]×p-a[x] a[x]×pa[x]


建树

Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn)

可以直接转化为 n n n 次单点修改

比如给定序列 a = ( 5 , 1 , 4 ) a=(5,1,4) a=(5,1,4) 要求建树,直接看作对 a [ 0 ] a[0] a[0] 加 5 ,对 a [ 1 ] a[1] a[1] 加 1 ,对 a [ 4 ] a[4] a[4] 加 4。

Tricks Θ ( n ) \Theta(n) Θ(n)

方法一:

每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。

// Θ(n) 建树
void init() {
  for (int i = 1; i <= n; ++i) {
    t[i] += a[i];
    int j = i + lowbit(i);
    if (j <= n) t[j] += t[i];
  }
}

方法二:

前面讲的 c [ i ] c[i] c[i] 表示的区间是 [ i − l o w b i t ( i ) + 1 , i ] [i-lowbit(i)+1,i] [ilowbit(i)+1,i],那么我们可以先预处理一个 s u m sum sum 前缀和数组,在计算 c c c 数组。

// Θ(n) 建树
void init() {
  for (int i = 1; i <= n; ++i) {
    t[i] = sum[i] - sum[i - lowbit(i)];
  }
}

模板示例:

// 单点修改区间查询
class PURQBIT {
    int[] nums, tree; // nums 为输入数组,tree 为对应 nums 的区间和树状数组
    int n; // nums大小
    public PURQBIT(int[] nums){
        this.nums = nums;
        this.n = nums.length;
        this.tree = new int[n];
        for(int i = 0; i < n; i++){
            add(i, nums[i]);
        }
    }
    // 单点修改: 令 nums[k] = x
    public void update (int k, int x){
        add(k, x - nums[k]);
        nums[k] = x; // 更新 nums[k] 为 x
    }
    // 单点修改: 令 nums[k] += x
    public void add(int k, int x){
        for(int i = k + 1; i <= n; i += lowbit(i)){
            tree[i - 1] += x; // 包含第 k 项的区间都加上 x
        }
    }
    // 区间查询 (区间求和): 求 nums[l] 到 nums[r] 之和
    public int sum(int l, int r){
        return preSum(r) - preSum(l - 1);
    }
    // 求前缀和: 求 nums[0] 到 nums[k] 的区间和 (前 k+1 项和)
    private int preSum(int k){
        int ans = 0;
        for(int i = k + 1; i > 0; i -= lowbit(i)){
            ans += tree[i - 1];
        }
        return ans;
    }
    private int lowbit(int i){
        return i & -i;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值