「树状数组」第 3 节:理解 lowbit 操作

下面我们介绍一种很酷的操作,叫做 lowbit ,它可以高效地计算 2 k 2^k 2k,即我们要证明:

l o w b i t ( i ) = 2 k {\rm lowbit}(i) = 2^k lowbit(i)=2k

其中 k k k 是将 i i i 表示成二进制以后,从右向左数,遇到 1 1 1 则停止时,数出的 0 0 0 的个数。

通过 lowbit 高效计算 2 k 2^k 2k

lowbit(i) = i & (-i)

理解这行伪代码需要一些二进制和位运算的知识作为铺垫。首先,我们知道负数的二进制表示为:相应正数的二进制表示的反码 + 1

例 8

计算 − 6 -6 6 的二进制表示。

分析: 6 6 6 的二进制表示为 0000    0110 0000\;0110 00000110,先表示成反码,即“ 0 0 0 1 1 1 1 1 1 0 0 0”,得 1111    1001 1111\;1001 11111001,再加 1 1 1,得 1111    1010 1111\;1010 11111010

例 9

i = 6 时,计算 l o w b i t ( i ) {\rm lowbit}(i) lowbit(i)

分析:

  • 由例 7 及「与」运算的定义,把它们按照数位对齐上下写好:
0000 0110
1111 1010
0000 0010
  • 上下同时为 1 1 1 才写 1 1 1,否则写 0 0 0,最后得到 0000 0010,这个二进制数表示成十进制数就是 2 2 2。建议大家多在稿纸上写几个具体的例子来计算 l o w b i t {\rm lowbit} lowbit,进而理解为什么 l o w b i t ( i ) = 2 k {\rm lowbit}(i)=2^k lowbit(i)=2k
  • 下面我给出一个我的直观解释:如果我们直接将一个整数「位取反」,再与原来的数做「与」运算,一定得到 0 0 0。巧就巧在,负数的二进制表示上,除了要求对「按位取反」以外,还要「加」 1 1 1,在「加」 1 1 1 的过程中产生的进位数即是「将 i i i 表示成二进制以后,从右向左数,遇到 1 1 1 停止时数出 0 0 0 的个数」。

那么我们知道了 l o w b i t {\rm lowbit} lowbit 以后,又有什么用呢?由于位运算是十分高效的,它能帮助我们在树状数组中高效计算「从子结点到父结点」(即对应「单点更新」操作),高效计算「前缀和由预处理数组的那些元素表示」(即对应「前缀和查询操作」)。

体会 lowbit 的作用

1、「单点更新」操作:从子结点到父结点

在这里插入图片描述

例 10

修改 A [ 3 ] A[3] A[3], 分析对数组 C C C 产生的变化。

分析:

  • 从图中我们可以看出 A [ 3 ] A[3] A[3] 的父结点以及祖先结点依次是 C [ 3 ] C[3] C[3] C [ 4 ] C[4] C[4] C [ 8 ] C[8] C[8] ,所以修改了 A [ 3 ] A[3] A[3] 以后 C [ 3 ] C[3] C[3] C [ 4 ] C[4] C[4] C [ 8 ] C[8] C[8] 的值也要修改;
  • 先看 C [ 3 ] C[3] C[3] l o w b i t ( 3 ) = 1 {\rm lowbit}(3) = 1 lowbit(3)=1 3 + l o w b i t ( 3 ) = 4 3 + {\rm lowbit}(3) = 4 3+lowbit(3)=4 就是 C [ 3 ] C[3] C[3] 的父亲结点 C [ 4 ] C[4] C[4] 的下标值;
  • 再看 C [ 4 ] C[4] C[4] l o w b i t ( 4 ) = 4 {\rm lowbit}(4) = 4 lowbit(4)=4 4 + l o w b i t ( 4 ) = 8 4 + {\rm lowbit}(4) = 8 4+lowbit(4)=8 就是 C [ 4 ] C[4] C[4] 的父亲结点 C [ 8 ] C[8] C[8] 的下标值;
  • 从图中,也可以验证:红色结点的下标值 + 右下角蓝色圆形结点的值 = 红色结点的双亲结点的下标值。

下面试图解释这个现象(个人理解):

  • 3 3 3 0011 0011 0011,从右向左,遇到 0 0 0 放过,遇到 1 1 1 为止,给这个数位加 1 1 1,这个操作就相当于加上了一个 2 k 2^k 2k 的二进制数,即一个 l o w b i t {\rm lowbit} lowbit 值,有意思的事情就发生在此时,马上就发发生了进位,得到 0100 0100 0100,即 4 4 4 的二进制表示;
  • 接下来处理 0100 0100 0100,从右向左,从右向左,遇到 0 0 0 放过,遇到 1 1 1 为止,给这个数位加 1 1 1,同样地,这个操作就相当于加上了一个 2 k 2^k 2k 的二进制数,即一个 l o w b i t {\rm lowbit} lowbit 值,可以看到,马上就发发生了进位,得到 1000 1000 1000,即 8 8 8 的二进制表示;
  • 从上面的叙述中,你可以发现,我们又在做「从右边到左边数,遇到 1 1 1 之前数出 0 0 0 的个数」这件事情了,
    由此我们可以总结出规律:从已知子结点的索引 i i i ,则结点 i i i 的父结点的索引 p a r e n t {\rm parent} parent 的计算公式为:

p a r e n t ( i ) = i + l o w b i t ( i ) {\rm parent}(i) = i + {\rm lowbit}(i) parent(i)=i+lowbit(i)

还需要说明的是,这不是巧合和循环论证,这正是因为对「从右边到左边数出 0 0 0 的个数,遇到 1 1 1 停止这件事情」的定义,使用 l o w b i t {\rm lowbit} lowbit 可以快速计算这件事成立,才会有的。

分析到这里「单点更新」的代码就可以马上写出来了。

Java 代码:

/**
 * 单点更新
 *
 * @param i     原始数组索引 i
 * @param delta 变化值 = 更新以后的值 - 原始值
 */
public void update(int i, int delta) {
    
    
    // 从下到上更新,注意,预处理数组,比原始数组的 len 大 1,故 预处理索引的最大值为 len
    while (i <= len) {
    
    
        tree[i] += delta;
        i += lowbit(i);
    }
}

public static int lowbit(int x) {
    
    
    return x & (-x);
}

2、「前缀和查询」操作:计算前缀和由预处理数组的那些元素表示

还是上面那张图。

在这里插入图片描述

例 11

求出「前缀和(6)」。

  • 由图可以看出前缀和(6) = C [ 6 ] C[6] C[6] + C [ 4 ] C[4] C[4]
  • 先看 C [ 6 ] C[6] C[6] l o w b i t ( 6 ) = 2 {\rm lowbit}(6) = 2 lowbit(6)=2 6 − l o w b i t ( 6 ) = 4 6 - {\rm lowbit}(6) = 4 6lowbit(6)=4 正好是 C [ 6 ] C[6] C[6] 的上一个非叶子结点 C [ 4 ] C[4] C[4] 的下标值。这里给出我的一个直观解释,如果下标表示高度,那么上一个非叶子结点,其实就是从右边向左边画一条水平线,遇到的墙的下标。只要这个值大于 0 0 0,都能正确求出来。

例 12

求出「前缀和(5)」。

  • 再看 C [ 5 ] C[5] C[5] l o w b i t ( 5 ) = 1 {\rm lowbit}(5) = 1 lowbit(5)=1 5 − l o w b i t ( 6 ) = 4 5 - {\rm lowbit}(6) = 4 5lowbit(6)=4 正好是 C [ 5 ] C[5] C[5] 的上一个非叶子结点 C [ 4 ] C[4] C[4] 的下标值,故「前缀和(5)」 = C [ 5 ] C[5] C[5] + C [ 4 ] C[4] C[4]

例 13

求出「前缀和(7)」。

  • 再看 C [ 7 ] C[7] C[7] l o w b i t ( 7 ) = 1 {\rm lowbit}(7) = 1 lowbit(7)=1 7 − l o w b i t ( 7 ) = 6 7 -{\rm lowbit}(7) = 6 7lowbit(7)=6 正好是 C [ 7 ] C[7] C[7] 的上一个非叶子结点 C [ 6 ] C[6] C[6] 的下标值,再由例 9 的分析,「前缀和(7)」 = C [ 7 ] C[7] C[7] + C [ 6 ] C[6] C[6] + C [ 4 ] C[4] C[4]

例 14

求出「前缀和(8)」。

  • 再看 C [ 8 ] C[8] C[8] l o w b i t ( 8 ) = 8 {\rm lowbit}(8) = 8 lowbit(8)=8 8 − l o w b i t ( 8 ) = 0 8 - {\rm lowbit}(8) = 0 8lowbit(8)=0 0 0 0 表示没有,从图上也可以看出从右边向左边画一条水平线,不会遇到的墙,故「前缀和(8)」 = C [ 8 ] C[8] C[8]

经过以上的分析,求前缀和的代码也可以写出来了。

Java 代码:

/**
 * 查询前缀和
 *
 * @param i 前缀的最大索引,即查询区间 [0, i] 的所有元素之和
 */
public int query(int i) {
    
    
    // 从右到左查询
    int sum = 0;
    while (i > 0) {
    
    
        sum += tree[i];
        i -= lowbit(i);
    }
    return sum;
}

可以看出「单点更新」和「前缀和查询操作」的代码量其实是很少的。

3、树状数组的初始化

  • 这里要说明的是,初始化前缀和数组应该交给调用者来决定;
  • 下面是一种初始化的方式。树状数组的初始化可以通过「单点更新」来实现,因为「最最开始」的时候,数组的每个元素的值都为 0 0 0,每个都对应地加上原始数组的值,就完成了预处理数组 C C C 的创建;
  • 这里要特别注意,update 操作的第 2 2 2 个索引值是一个变化值,而不是变化以后的值。因为我们的操作是逐层上报,汇报变更值会让我们的操作更加简单,这一点请大家反复体会。

Java 代码:

public FenwickTree(int[] nums) {
    
    
    this.len = nums.length + 1;
    tree = new int[this.len + 1];
    for (int i = 1; i <= len; i++) {
    
    
        update(i, nums[i]);
    }
}

基于以上所述,树状数组的完整代码已经可以写出来了。

Java 代码:

public class FenwickTree {
    
    

    /**
     * 预处理数组
     */
    private int[] tree;
    private int len;

    public FenwickTree(int n) {
    
    
        this.len = n;
        tree = new int[n + 1];
    }

    /**
     * 单点更新
     *
     * @param i     原始数组索引 i
     * @param delta 变化值 = 更新以后的值 - 原始值
     */
    public void update(int i, int delta) {
    
    
        // 从下到上更新,注意,预处理数组,比原始数组的 len 大 1,故 预处理索引的最大值为 len
        while (i <= len) {
    
    
            tree[i] += delta;
            i += lowbit(i);
        }
    }

    /**
     * 查询前缀和
     *
     * @param i 前缀的最大索引,即查询区间 [0, i] 的所有元素之和
     */
    public int query(int i) {
    
    
        // 从右到左查询
        int sum = 0;
        while (i > 0) {
    
    
            sum += tree[i];
            i -= lowbit(i);
        }
        return sum;
    }

    public static int lowbit(int x) {
    
    
        return x & (-x);
    }
}

Python 代码:

class FenwickTree:
    def __init__(self, n):
        self.size = n
        self.tree = [0 for _ in range(n + 1)]

    def __lowbit(self, index):
        return index & (-index)

    # 单点更新:从下到上,最多到 size,可以取等
    def update(self, index, delta):
        while index <= self.size:
            self.tree[index] += delta
            index += self.__lowbit(index)

    # 区间查询:从上到下,最少到 1,可以取等
    def query(self, index):
        res = 0
        while index > 0:
            res += self.tree[index]
            index -= self.__lowbit(index)
        return res

猜你喜欢

转载自blog.csdn.net/lw_power/article/details/106965287