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

本文深入解析Lowbit操作及其在树状数组中的应用,包括高效计算2^k、单点更新与前缀和查询操作,附带Java与Python代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

lowbit(i)=2k {\rm lowbit}(i) = 2^k lowbit(i)=2k

其中 kkk 是将 iii 表示成二进制以后,从右向左数,遇到 111 则停止时,数出的 000 的个数。

通过 lowbit 高效计算 2k2^k2k

lowbit(i) = i & (-i)

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

例 8

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

分析:666 的二进制表示为 0000  01100000\;011000000110,先表示成反码,即“ 000111111000”,得 1111  10011111\;100111111001,再加 111,得 1111  10101111\;101011111010

例 9

i = 6 时,计算 lowbit(i){\rm lowbit}(i)lowbit(i)

分析:

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

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

体会 lowbit 的作用

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

在这里插入图片描述

例 10

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

分析:

  • 从图中我们可以看出 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]lowbit(3)=1{\rm lowbit}(3) = 1lowbit(3)=13+lowbit(3)=43 + {\rm lowbit}(3) = 43+lowbit(3)=4 就是 C[3]C[3]C[3] 的父亲结点 C[4]C[4]C[4] 的下标值;
  • 再看 C[4]C[4]C[4]lowbit(4)=4{\rm lowbit}(4) = 4lowbit(4)=44+lowbit(4)=84 + {\rm lowbit}(4) = 84+lowbit(4)=8 就是 C[4]C[4]C[4] 的父亲结点 C[8]C[8]C[8] 的下标值;
  • 从图中,也可以验证:红色结点的下标值 + 右下角蓝色圆形结点的值 = 红色结点的双亲结点的下标值。

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

  • 333001100110011,从右向左,遇到 000 放过,遇到 111 为止,给这个数位加 111,这个操作就相当于加上了一个 2k2^k2k 的二进制数,即一个 lowbit{\rm lowbit}lowbit 值,有意思的事情就发生在此时,马上就发发生了进位,得到 010001000100,即 444 的二进制表示;
  • 接下来处理 010001000100,从右向左,从右向左,遇到 000 放过,遇到 111 为止,给这个数位加 111,同样地,这个操作就相当于加上了一个 2k2^k2k 的二进制数,即一个 lowbit{\rm lowbit}lowbit 值,可以看到,马上就发发生了进位,得到 100010001000,即 888 的二进制表示;
  • 从上面的叙述中,你可以发现,我们又在做「从右边到左边数,遇到 111 之前数出 000 的个数」这件事情了,
    由此我们可以总结出规律:从已知子结点的索引 iii ,则结点 iii 的父结点的索引 parent{\rm parent}parent 的计算公式为:

parent(i)=i+lowbit(i) {\rm parent}(i) = i + {\rm lowbit}(i) parent(i)=i+lowbit(i)

还需要说明的是,这不是巧合和循环论证,这正是因为对「从右边到左边数出 000 的个数,遇到 111 停止这件事情」的定义,使用 lowbit{\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]lowbit(6)=2{\rm lowbit}(6) = 2lowbit(6)=26−lowbit(6)=46 - {\rm lowbit}(6) = 46lowbit(6)=4 正好是 C[6]C[6]C[6] 的上一个非叶子结点 C[4]C[4]C[4] 的下标值。这里给出我的一个直观解释,如果下标表示高度,那么上一个非叶子结点,其实就是从右边向左边画一条水平线,遇到的墙的下标。只要这个值大于 000,都能正确求出来。

例 12

求出「前缀和(5)」。

  • 再看 C[5]C[5]C[5]lowbit(5)=1{\rm lowbit}(5) = 1lowbit(5)=15−lowbit(6)=45 - {\rm lowbit}(6) = 45lowbit(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]lowbit(7)=1{\rm lowbit}(7) = 1lowbit(7)=17−lowbit(7)=67 -{\rm lowbit}(7) = 67lowbit(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]lowbit(8)=8{\rm lowbit}(8) = 8lowbit(8)=88−lowbit(8)=08 - {\rm lowbit}(8) = 08lowbit(8)=0000 表示没有,从图上也可以看出从右边向左边画一条水平线,不会遇到的墙,故「前缀和(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、树状数组的初始化

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

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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值