当前位置: 首页 > news >正文

从位运算角度重新理解树状数组

从位运算角度重新理解树状数组:为什么传统解释不够深刻?

引言:问题的提出

当我们学习树状数组(Binary Indexed Tree,也称 Fenwick Tree)时,大多数教程都会告诉我们:

  • 树状数组是一种"树形结构"
  • 通过 i += lowbit(i) 可以"向上找父节点"
  • 通过 i -= lowbit(i) 可以"向下找子节点"
  • 每个节点管理一个特定长度的区间

这些解释看起来合理,但却没有回答一个核心问题:

为什么只能跳到这些特定的节点?为什么不是其他的跳跃方式?

更深入地思考,既然树状数组的核心操作都是基于 lowbit(x) = x & (-x) 这个位运算,那我们是否应该从位运算的角度来理解它的本质?

预备知识:lowbit 的位运算含义

在深入树状数组之前,我们需要先理解一个关键的位运算概念:lowbit

什么是 lowbit?

lowbit(x) 表示 x 的二进制表示中最右边的 1 及其右边所有 0 组成的数值

更准确地说:

  • lowbit(x) = x & (-x)
  • 它提取出 x 二进制表示中最低位的 1 对应的 2 的幂次

lowbit 的直观理解

x = 12 = 1100 (二进制)
最右边的1在位置2 (从右往左,从0开始计数)
lowbit(12) = 2² = 4x = 6 = 0110 (二进制)
最右边的1在位置1
lowbit(6) = 2¹ = 2x = 8 = 1000 (二进制)
最右边的1在位置3
lowbit(8) = 2³ = 8x = 5 = 0101 (二进制)
最右边的1在位置0
lowbit(5) = 2⁰ = 1

lowbit 的位运算实现原理

为什么 x & (-x) 能得到 lowbit?

这涉及到负数的补码表示:

计算 -x 的步骤:
1. 取 x 的二进制表示
2. 按位取反(得到反码)
3. 加 1(得到补码)例如 x = 6:
x = 6:     00000110
按位取反:   11111001
加1得-x:   11111010x & (-x):  00000110& 11111010= 00000010 = 2

关键洞察:在补码运算中,从右往左找到第一个 1,这个 1 及其右边的 0 在取反加1后保持不变,而左边的位全部被翻转。因此 x & (-x) 只保留了最右边的 1。

树状数组的基本实现

tree[i] 的含义

现在我们可以准确定义树状数组中 tree[i] 的含义:

tree[i] 管理从位置 i - lowbit(i) + 1 到位置 i 的区间和

换句话说,tree[i] 管理的是:从 i 开始,向前 lowbit(i) 这么长的区间

例如:
tree[1]: 管理 [1, 1],长度 = lowbit(1) = 1
tree[2]: 管理 [1, 2],长度 = lowbit(2) = 2  
tree[3]: 管理 [3, 3],长度 = lowbit(3) = 1
tree[4]: 管理 [1, 4],长度 = lowbit(4) = 4
tree[5]: 管理 [5, 5],长度 = lowbit(5) = 1
tree[6]: 管理 [5, 6],长度 = lowbit(6) = 2
tree[7]: 管理 [7, 7],长度 = lowbit(7) = 1
tree[8]: 管理 [1, 8],长度 = lowbit(8) = 8

Query 操作的区间分解

现在我们来看 Query 操作是如何工作的:

func (bit *BIT) Query(i int) int {sum := 0for i > 0 {sum += bit.tree[i]i -= lowbit(i)}return sum
}

Query 的本质是将 [1, i] 区间按照 tree 的管理区间进行分解

让我们看一个具体例子,Query(6) 的分解过程:

Query(6) 要计算 [1, 6] 的区间和第一步:i = 6
- tree[6] 管理 [5, 6]
- 剩余区间:[1, 4] = [1, 6-lowbit(6)] = [1, 6-2]
- i = 6 - 2 = 4第二步:i = 4  
- tree[4] 管理 [1, 4]
- 剩余区间:[1, 0] = 空
- i = 4 - 4 = 0,结束完整分解:[1, 6] = [5, 6] ∪ [1, 4]
对应代码:sum = tree[6] + tree[4]

再看一个例子,Query(7) 的分解过程:

Query(7) 要计算 [1, 7] 的区间和第一步:i = 7
- tree[7] 管理 [7, 7]  
- 剩余区间:[1, 6]
- i = 7 - 1 = 6第二步:i = 6
- tree[6] 管理 [5, 6]
- 剩余区间:[1, 4]  
- i = 6 - 2 = 4第三步:i = 4
- tree[4] 管理 [1, 4]
- 剩余区间:空
- i = 4 - 4 = 0,结束完整分解:[1, 7] = [7, 7] ∪ [5, 6] ∪ [1, 4]
对应代码:sum = tree[7] + tree[6] + tree[4]

关键观察i -= lowbit(i) 恰好实现了最优的区间分解,每次分解都是无重叠、无遗漏的。

核心理论:贡献关系的位运算构造

核心问题:当更新位置 b 时,哪些 tree[a] 需要被更新?

贡献关系的位运算定义

从位运算角度看,a 包含 b 的条件可以这样理解:

tree[a] 管理区间 (a-lowbit(a), a],从正向构造的角度分析:

核心思想:将 a 的 lowbit(a) 位清零,然后在 lowbit(a) 位之后的位置进行任意组合,但不能全为0。

位运算构造过程

  1. 从 a 开始,保持 lowbit(a) 位之前的所有位不变
  2. 将 lowbit(a) 位清零
  3. lowbit(a) 位之后的位可以任意变化,但至少要有一个位为1(这样构造出区间 (a-lowbit(a), a))
  4. 最后加上 a 本身,得到完整的管理区间 (a-lowbit(a), a]

具体例子

  • a = 4 (100₂),lowbit(4) = 4 (100₂)

  • tree[4] 管理区间 [1,4],即 (000₂, 100₂]

  • 构造过程:从 100₂ 开始,将 lowbit 位(2²位)清零得到 000₂,然后在 lowbit 位之后的位(2⁰位和2¹位)进行变化

  • 在 000₂ 基础上,2⁰位和2¹位可以任意组合,但不能是 000₂ 本身

  • 因此构造出 (0, 4) 中的数:001₂(1), 010₂(2), 011₂(3),再加上 a 本身 100₂(4)

  • a = 6 (110₂),lowbit(6) = 2 (010₂)

  • tree[6] 管理区间 [5,6],6-lowbit(6) = 6-2 = 4,所以区间是 (4, 6]

  • 构造过程:从 110₂ 开始,将 lowbit 位(第2位)清零得到 100₂,然后最右边的位可以变化

  • 在 100₂ 基础上,最右位可以是0或1,但不能是 100₂ 本身

  • 因此构造出 (4, 6) 中的数:101₂(5),再加上 a 本身 110₂(6)

判断规律:b 属于 tree[a] 管理范围,当且仅当:

  • (b & ~(lowbit(a)-1)) == (a & ~(lowbit(a)-1)) (高位部分相同)
  • b & (lowbit(a)-1) != 0b == a (低位部分不全为0,或者就是a本身)

从 b 构造贡献目标 a 的方法

逆向构造:从 b 找到它能贡献的 a

现在反过来,对于给定的 b,我们要找到所有它能贡献的 a。

关键观察:如果 b 对 a 有贡献,那么:

  1. 前缀一致性:lowbit(a) 之前的前缀,a 和 b 必须完全一致
  2. 位置关系:lowbit(a) 必须 > lowbit(b)(因为 a > b 且 a 包含 b)
  3. lowbit(a) 位的状态:这一位在 b 中必须是 0(否则构造的 a 会 < b)

构造过程

  1. 枚举候选位置:从 lowbit(b) 位开始向左枚举,寻找 b 中为 0 的位作为 lowbit(a) 的候选位置

  2. 验证可行性:对于每个候选位置 k:

    • 如果 b 的第 k 位是 0 ✓ 可以构造
    • 如果 b 的第 k 位是 1 ✗ 不能构造(会导致 a < b)
  3. 构造 a

    • 保留 b 在第 k 位之前的所有前缀
    • 将第 k 位设置为 1(作为 a 的 lowbit)
    • 第 k 位之后全部设为 0

为什么第 k 位是 1 时不能构造?

如果 b 的第 k 位是 1,构造出的 a 会是:

  • a 的前缀 = b 的前缀(第 k 位之前)
  • a 的第 k 位 = 1
  • a 的第 k 位之后 = 全 0

但 b 除了第 k 位是 1,还有 lowbit(b) 位也是 1,所以 b > a,违反了贡献关系的要求。

简化的构造方法

  1. 从 b 的 lowbit 位开始,向左枚举 0 位
  2. 将找到的 0 位设置成 1,作为 a 的 lowbit
  3. 保留 b 在 a 的 lowbit 位之前的前缀
  4. lowbit 之后的位全部为 0

具体例子:b = 3 的贡献构造

b = 3 = 0011, lowbit(b) = 1 (位置0)从位置0向左枚举0位:
- 位置1: 是1,跳过
- 位置2: 是0 ✓ 可以设置成1构造a₁:
- a的lowbit在位置2,值为2² = 4
- 保留位置2之前的前缀: "00"  
- 构造: a₁ = 00100 = 4验证包含关系:
- tree[4]管理[1,4],位置3在其中 ✓
- 4 - 4 < 3 ≤ 4 → 0 < 3 ≤ 4 ✓继续枚举:
- 位置3: 是0 ✓ 可以设置成1构造a₂:
- a的lowbit在位置3,值为2³ = 8
- 保留位置3之前的前缀: "001"
- 构造: a₂ = 1000 = 8验证包含关系:
- tree[8]管理[1,8],位置3在其中 ✓
- 8 - 8 < 3 ≤ 8 → 0 < 3 ≤ 8 ✓贡献路径: 3 → 4 → 8 → 16 → 32 → ...

再看例子:b = 6 的贡献构造

b = 6 = 0110, lowbit(b) = 2 (位置1)从位置1向左枚举0位:
- 位置2: 是1,跳过
- 位置3: 是0 ✓ 可以设置成1构造a₁:
- a的lowbit在位置3,值为2³ = 8  
- 保留位置3之前的前缀: "011"
- 构造: a₁ = 1000 = 8验证包含关系:
- tree[8]管理[1,8],位置6在其中 ✓
- 8 - 8 < 6 ≤ 8 → 0 < 6 ≤ 8 ✓继续枚举:
- 位置4: 是0 ✓ 可以设置成1构造a₂:
- a的lowbit在位置4,值为2⁴ = 16
- 保留位置4之前的前缀: "0110"  
- 构造: a₂ = 10000 = 16贡献路径: 6 → 8 → 16 → 32 → ...

理论与代码的完美契合

现在我们来看最精彩的部分:我们的位运算理论如何完美地对应到树状数组的代码实现

Update 操作的位运算本质

func (bit *BIT) Update(i, delta int) {for i <= bit.n {bit.tree[i] += deltai += lowbit(i)  // 关键操作}
}

i += lowbit(i) 恰好枚举了我们理论中的贡献路径!

让我们分析 i += lowbit(i) 的位运算过程:

设 i = ...abc10...0 (最后有k个连续的0)
lowbit(i) = 2^k  
i + lowbit(i) = ...ab(c+1)00...0操作效果:
1. 找到i的最右边的1 (当前lowbit位)
2. 将其进位到下一个0位
3. 进位过程自动保留了前缀
4. 进位后自动清零了新lowbit之后的位

这恰好是我们理论中的构造过程!

验证 b = 3 的例子

i = 3 = 0011, lowbit(3) = 1
i + lowbit(i) = 3 + 1 = 4 = 0100位运算分析:
- 最右边的1在位置0
- 进位到下一个0位(位置2) 
- 自动保留前缀"00"
- 自动清零位置2之后的位
- 结果恰好是我们构造的a₁ = 4 ✓

验证 b = 6 的例子

i = 6 = 0110, lowbit(6) = 2
i + lowbit(i) = 6 + 2 = 8 = 1000位运算分析:
- 最右边的1在位置1
- 进位到下一个0位(位置3)
- 自动保留前缀"011" 
- 自动清零位置3之后的位
- 结果恰好是我们构造的a₁ = 8 ✓

Query 操作的位运算验证

基于前面的区间分解理论,i -= lowbit(i) 的位运算过程可以这样理解:

位运算分析

设 i = ...abc1000...0 (末尾有k个0)
lowbit(i) = 2^k
i - lowbit(i) = ...ab(c-1)111...1 (将最右边的1变为0,其右边全变为1)这个操作恰好去掉了tree[i]管理的区间,留下了需要继续分解的部分。

Update 操作中 x += lowbit(x) 的构造原理

基于我们的贡献关系理论,x + lowbit(x) 能实现构造的原理:

3 = 0011 + 0001 = 0100 = 4位运算分析:
- 最右边的1在位置0
- 加上lowbit相当于进位到下一个0位(位置2)
- 自动清零位置2之后的位 → 00
- 保留位置2之前的前缀 → "00"
- 结果: 0100 = 4

神奇之处x + lowbit(x) 自动找到了第一个可以"设置成1"的0位,并完成了我们理论中的构造过程!

对比验证代码

package mainimport "fmt"// ========== 位运算枚举实现(基于我们的理论) ==========
type BitEnumBIT struct {tree []intn    int
}func NewBitEnumBIT(n int) *BitEnumBIT {return &BitEnumBIT{tree: make([]int, n+1),n:    n,}
}func (bit *BitEnumBIT) lowbit(x int) int {return x & (-x)
}// 基于位运算枚举理论的Update实现
func (bit *BitEnumBIT) Update(pos, delta int) {// 从pos开始,枚举所有它能贡献的位置for pos <= bit.n {bit.tree[pos] += delta// 使用我们的理论:找到下一个可以设置为1的0位pos = bit.constructNextContributor(pos)if pos > bit.n {break}}
}// 根据我们的位运算理论构造下一个贡献目标
func (bit *BitEnumBIT) constructNextContributor(b int) int {// 从b的lowbit位开始向左枚举0位lowbitB := bit.lowbit(b)// 寻找b中第一个为0的位(从lowbit位开始向左)for k := lowbitB << 1; k <= (1 << 20); k <<= 1 {if (b & k) == 0 { // 找到0位// 构造a:保留k位之前的前缀,设置k位为1,k位之后清零prefix := b & (^(k - 1)) // 保留k位之前的前缀a := prefix | k          // 设置k位为1return a}}return bit.n + 1 // 超出范围
}// 基于位运算区间分解理论的Query实现
func (bit *BitEnumBIT) Query(i int) int {sum := 0for i > 0 {sum += bit.tree[i]i -= bit.lowbit(i) // 位运算区间分解}return sum
}// ========== 传统暴力实现(用于对比验证) ==========
type BruteForce struct {arr []intn   int
}func NewBruteForce(n int) *BruteForce {return &BruteForce{arr: make([]int, n+1),n:   n,}
}func (bf *BruteForce) Update(i, delta int) {bf.arr[i] += delta
}func (bf *BruteForce) Query(i int) int {sum := 0for j := 1; j <= i; j++ {sum += bf.arr[j]}return sum
}// ========== 验证函数 ==========
func main() {fmt.Println("=== 位运算枚举实现 vs 传统暴力方法对比验证 ===\n")bit := NewBitEnumBIT(10)bf := NewBruteForce(10)// 测试操作序列operations := []struct {op    stringpos   intvalue int}{{"update", 1, 5},{"update", 3, 7},{"query", 3, 0}, // 查询前3个位置的和{"update", 5, 2},{"query", 5, 0}, // 查询前5个位置的和{"update", 2, 3},{"query", 4, 0}, // 查询前4个位置的和}for i, op := range operations {if op.op == "update" {fmt.Printf("操作%d: Update(%d, %d)\n", i+1, op.pos, op.value)bit.Update(op.pos, op.value)bf.Update(op.pos, op.value)} else {result1 := bit.Query(op.pos)result2 := bf.Query(op.pos)fmt.Printf("操作%d: Query(%d) -> 位运算枚举: %d, 暴力: %d",i+1, op.pos, result1, result2)if result1 == result2 {fmt.Printf(" ✓\n")} else {fmt.Printf(" ✗ 不一致!\n")}}}fmt.Println("\n验证完成:位运算枚举实现与传统方法结果完全一致,证明我们的位运算理论正确!")
}
http://www.dtcms.com/a/388711.html

相关文章:

  • 从零开始构建Kubernetes Operator:一个完整的深度学习训练任务管理方案
  • 关于CAS的ABA问题的原因以及解决?
  • C语言(长期更新)第16讲:字符和字符串函数
  • c过渡c++应知应会(2)
  • 分析下kernel6.6中如何获取下一次的cpu频率
  • 22.4 单卡训练T5-Large!DeepSpeed ZeRO-2让12GB显存hold住770M参数模型
  • 《Linux 常用 C 函数参考手册》更新 2.0 版本啦!适合 C 语言开发者、Linux 系统程序员、嵌入式开发者使用
  • str.maketrans() 方法
  • 漫谈:C语言 C++ 声明和定义的区别是什么
  • Java企业级开发中的对象类型深度解析:PO、Entity、BO、DTO、VO、POJO 使用场景、功能介绍、是否必须、总结对比
  • 从弱 AI 到通用人工智能(AGI):核心技术壁垒与人类社会的适配挑战
  • 数据序列化语言---YAML
  • Dify: Step2 Dify模型配置 Dify, Docker,ollama是什么关系
  • SSH连接排故排查
  • 【DMA】DMA架构解析
  • STM32HAL库-移植mbedtls开源库示例(一)
  • MAP的具体实现
  • 排序不等式的推广,对于任意两个数列的推广
  • 9.7.3 损失函数
  • Java Web开发的基石:深入理解Servlet与JSP​
  • pyOCD发布V0.39版本(2025-09-17)
  • kernel侧CPU是怎样判断共享的?
  • pcl案例六 基于配准的无序抓取
  • 动态库和静态库的链接加载
  • 离线安装docker镜像
  • MySql索引性能优化
  • 【实战指南】WAF日志分析系统的生产部署:性能调优与最佳实践
  • OKZOO联合非小号TKW3,海上ALPHA WEB3派对启航
  • Java工程代码架构度量:从DSM到构建工具的深度实践
  • 车联网网络安全