想象你住在一栋 8 层的公寓里,物业人员常常要统计“前 7 层一共住了多少人”。最笨的办法是一层一层跑去数,数 7 层要跑 7 趟;如果这栋楼有 1 万层,就要跑 1 万趟——这就是普通的数组累加。聪明的物业设计了一套“楼层管家”制度:每一层安排一位管家,每位管家只负责自己名下的一小段楼层,比如 6 楼的管家负责记录 5~6 层的人数,4 楼的管家负责记录 1~4 层的人数。这样物业要查“前 7 层总人数”时,只要分别问 7 楼管家(7 层自己)、6 楼管家(5~6 层)、4 楼管家(1~4 层),三个人三次就报完数了,再也不用一层层跑!这个“分层负责、按块汇总”的智慧,就是树状数组(Binary Indexed Tree,简称 BIT)的核心思想。今天我们就用这栋“管家公寓”的故事,把 BIT 的原理、查询、更新和各种应用讲透。
对 AI 从业者而言,树状数组是处理“动态前缀统计”问题的利器——从推荐系统的实时 CTR 累计、排行榜动态排名,到流式文本的词频统计,它都能以 O(log n) 的优雅代价完成增量式数据聚合。
可视化演示
树状数组(Binary Indexed Tree)是支持单点修改和前缀和查询的高效数据结构,通过lowbit运... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
树状数组结构:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
BIT基础结构
物业经理宣布实施“楼层管家”制度的那天,整栋楼的住户都松了一口气。规则很简单:每一层安排一位管家,第 i 层管家负责记录“某一段连续楼层”的住户变化情况。神奇的是,这段楼层范围不是随意划定的,而是由楼层编号 i 的二进制性质决定的——编号为 6 的管家负责 5~6 层,编号为 4 的管家反而负责 1~4 层。看起来毫无规律,实则暗藏玄机:每位管家负责的区间恰好是 (i - lowbit(i), i] 的闭区间。
在技术上,树状数组用一个长度为 n+1 的数组 BIT 来存放这些“管家账本”。BIT[i] 表示第 i 个位置记录的区间和,而这个区间由 i 的二进制决定。为了实现“任意前缀都能由若干互不重叠的区间拼出来”,BIT 的下标必须从 1 开始——如果从 0 开始,lowbit 链会陷入死循环,这个问题我们后面会详细解释。
这套“楼层管家”制度能成立的关键在于:任意一个前缀区间 [1, x],都可以被拆成若干个完整的“管家责任区间”,而且这些区间互不重叠、刚好拼满。物业经理正是利用这一性质,把原本需要 O(n) 次累加的问题压缩到了 O(log n) 次问询。整栋楼的住户从此告别了“一层层数人数”的原始时代。
- i楼层编号,同时也是管家登记表 BIT 数组的下标。
- lowbit(i)i 的二进制最低位 1 对应的数值,可以理解为管家“分到的楼层跨度”。
- i - lowbit(i) + 1用跨度算出区间左端点,管家从这一层开始负责。
- […, i]闭区间写法,表示管家负责的左边界到右边界之间所有楼层。
lowbit运算
物业给每位管家发了一把“责任跨度密码锁”,这把锁决定了他们管辖多少层楼。密码的规则来自楼层号的二进制写法:从左往右看,二进制最低位那个 1 所代表的数值,就是这位管家的管辖跨度。比如 6 的二进制是 110,最低位的 1 代表 2,所以 6 楼管家负责 2 层(5~6 层);4 的二进制是 100,最低位的 1 代表 4,所以 4 楼管家负责 4 层(1~4 层)。
在 C/C++/Python 中,这个“最低位 1 的数值”可以用一个精妙的位运算公式直接算出来:x & (-x)。为什么负号有这种魔力?因为负数在计算机中用补码表示,-x 等价于对 x 逐位取反再加 1,这恰好会让“最低位的 1”在补码中保持不变,而它右边的 0 和左边的所有位都被取反。与运算后,只有最低位那个 1 被保留下来。
学会用 lowbit 这把“钥匙”之后,整个管家的责任体系就清晰了:管家 i 的管辖范围是以 lowbit(i) 为跨度的一段连续楼层。需要特别提醒的是,lowbit 的结果永远是 2 的幂次(1、2、4、8……),这也保证了区间拆分总是规整的。物业经理把这把密码锁命名为 lowbit——“最低位的比特”,从此每位管家都明确知道自己的辖区边界。
- x任意正整数下标,例如 6。
- -xx 的补码表示,等价于 ~x + 1。
- & 按位与两个二进制数逐位取交集,只有都为 1 的位结果才为 1。
- 运算结果得到最低位 1 对应的数值。
前缀和查询
物业经理要想知道“前 7 层一共住了多少人”,现在只需要做三件事:先问 7 楼管家要他自己那层的数字(因为 lowbit(7)=1,他只管 7 层自己);然后往左跳到 6 楼,问 6 楼管家要 5~6 层的累计数;再往左跳到 4 楼,问 4 楼管家要 1~4 层的累计数。三个数相加,前 7 层的总人数就到手了。这个“向左逐级上报”的过程,在代码里就是不断地执行 i -= lowbit(i),每跳一次收集一个 BIT[i]。
这个流程之所以总是能恰好拼满前缀,是因为每次减去 lowbit(i) 后,新的下标 i' 恰好是上一个管家区间的左边界再往左一位。数学上可以严格证明:对任意正整数 i,将 i、i-lowbit(i)、i-lowbit(i)-lowbit(i-lowbit(i))……这条链走到底,会得到一组互不重叠且无缝覆盖 [1, i] 的区间。而且链的长度绝不会超过 log₂(n)+1,因为每次 i 都会至少去掉二进制中的一个 1。
这一小段“问询-跳跃-累加”的循环,就是整个 BIT 数据结构的半边天。无论楼层总数 n 有多大,查询前 i 层的总人数都只需要走 log₂n 步左右。物业经理很满意:以前查一次要跑遍整栋楼,现在喝杯咖啡的功夫就出结果了。而且每次有住户搬入搬出,只需要在管家账本上轻轻改一笔,查询依然高效如初。
- BIT[i]当前下标 i 的管家账本,记录其责任区间的总和。
- i - lowbit(i)向左跳一步,跳到下一个待询问的管家位置。
- 递归叠加重复累加并跳跃,直到 i 变为 0。
- 终止条件i = 0 时没有管家可问,递归结束。
单点更新
这天 3 楼搬来了 2 位新住户,物业经理必须保证所有相关管家的账本都是最新的。哪些管家的账本需要更新呢?凡是“责任区间覆盖 3 楼”的管家都要改。按照管家制度,3 楼自己肯定要记一笔;4 楼管家负责 1~4 层,覆盖 3 楼,要改;8 楼管家负责 1~8 层,也覆盖 3 楼,同样要改。这串需要更新的位置恰好是 3 → 4 → 8,规律就是不断执行 i += lowbit(i)——向右上方攀升,直到超过大楼总层数 n。
这个过程与前缀和查询形成了完美的对称:查询是向左下方跳(i -= lowbit(i)),更新是向右上方跳(i += lowbit(i))。为什么会这样?因为 lowbit(i) 的意义就是“管家 i 向上级汇报的跨度”,而所有包含下标 i 的管家位置,都等于 i 加上某个不断翻倍的 lowbit 值。代码实现极短,但背后是严谨的二进制进位逻辑。
有了单点更新,树状数组就真正“活”了起来——它不再是一张静态的表格,而是一套可以随时响应变化的动态系统。对 AI 场景尤其重要的是:互联网数据是以“流”的形式不断到达的,每一次点击、每一条日志都是一个“单点增量”。BIT 能以极低代价把这种增量实时融入全局统计之中。物业经理终于可以宣布:以后搬入搬出、临时住户登记,账本永远实时准确。
- BIT[i] += δ先更新当前管家的账本,加上增量 δ。
- i += lowbit(i)向右上方跳到管辖范围更大的下一位管家。
- 循环条件只要 i 不超过 n,就继续更新。
- 路径长度每一步 lowbit 至少翻倍,故总步数为 O(log n)。
区间扩展
管家制度运行一段时间后,物业经理收到一个新的需求:不仅要知道“前 n 层总人数”,还想知道“任意一段楼层(比如 3 到 6 层)住了多少人”。这难不倒聪明的前台:先用刚才的查询方法算出“前 6 层总人数”,再算出“前 2 层总人数”,两者相减,3~6 层的精确人数就出来了。这就像把一个长绳子的总长减去起点之前那段绳子的长度,剩下的恰好就是我们想要的中间段。
更妙的是,如果需求变成“给 3~6 层统一增加 2 人”这种区间修改,单纯一个 BIT 就不够用了,这时需要引入差分数组的思想:把“区间加 v”转化为在差分数组上的两次单点操作——在位置 l 加 v,在位置 r+1 减 v。这样原本的“区间修改”就被拆解成了两个“单点更新”,BIT 的看家本领刚好用得上。若要同时支持区间修改和区间查询,则需要用两个 BIT 配合,一个维护差分值,一个维护带权差分值。
差分数组的标记公式同样重要:
物业经理意识到,这套“加标记-减标记”的玩法就像在某些楼层贴临时通知,到期限自动失效,只要沿着走廊走一遍就能看到所有生效中的通知。BIT 正是那个让“走一遍”变成“跳着走”的高速电梯。
- prefix(r)先查询前 r 层的总人数。
- prefix(l-1)再查询第 l 层之前的累计人数。
- 相减两个前缀和的差正好是 [l, r] 区间的和。
- 复杂度两次 O(log n) 查询,总代价仍是 O(log n)。
- diff[l] += v在区间起点打一个“加 v”的标记。
- diff[r+1] -= v在区间终点后一位打一个“减 v”的标记。
- 前缀和还原对 diff 做前缀和即可得到每个位置的真实增量。
经典应用
管家制度的能力边界比物业经理想象的还要宽广。第一个经典应用是逆序对计数:如果住户门牌号是按入住顺序贴的,物业想知道“有多少对住户的门牌号顺序是反的”。办法是让住户依次登记入住,第 j 位住户入住时,先查一下“目前已经入住的住户中,门牌号小于等于他的有多少位”,那么在他之前且比他大的住户数量就是 j - 已登记的小于等于他的数量,累加所有贡献就是总逆序对数。BIT 在这里充当了“实时频率统计表”。
第二个应用是动态排名:游戏排行榜中玩家的分数不断变化,BIT 可以维护“每个分数段有多少人”的频次数组,查询“当前排名第 k 的分数”时通过二分 + 前缀和快速定位。第三个应用是区间频率统计:在 NLP 的流式处理中,统计最近 N 个 token 里某个词出现了多少次,BIT 的每次单点更新与区间查询都是 O(log n),完美匹配高吞吐场景。为了处理数值范围很大的情况,通常先对原始数据做离散化,把它们映射到 1..m 的小范围排名上。
物业经理看着这栋楼从“管人数”发展到“管门牌号顺序”、“管游戏分数”、“管词频”,感慨当初这套管家制度真是万能的积木。其实本质上,所有应用都在做同一件事:把“位置”映射到 BIT 的下标上,用前缀和回答“排名范围内有多少个已登记元素”。这正是 BIT 作为“动态前缀统计器”的灵魂。
- rank[a_j]把原数组第 j 个元素离散化后的排名,缩放到 1..m。
- prefix(rank[a_j])查询已登记元素中排名不超过它的数量。
- j - prefix(rank[a_j])在第 j 个元素之前、且比它大的元素个数。
- 求和每个元素的贡献累加即为总逆序对数。
性能对比
物业经理听说隔壁小区用了一套更复杂的“大堂经理”管理系统——线段树,据说功能更全面,于是特意做了一次对比调研。结论是:在“单点修改 + 前缀/区间和查询”这一类问题上,楼层管家制度(BIT)几乎全面胜出——代码短、常数小、空间省;但如果物业管理需要“某段楼层中人数最多是多少”这类区间最值查询,管家账户的“减法抵消”逻辑就失效了,必须请出线段树这个更重量级的大堂经理。
从时空复杂度看,BIT 的构建可以用原始数组直接在线性时间内完成:初始化 BIT 数组后,对每个位置 i,将 a[i] 累加到 BIT[i] 并把值传递给 i+lowbit(i)。而线段树需要 O(4n) 的空间存储树形结构。实际运行中 BIT 的循环极短、缓存友好,常数因子远小于线段树的递归开销。
物业经理最后的决策是:常规的住户统计、排行榜、词频分析全部用 BIT;只有偶尔需要“找出某段楼层最热闹的楼层”这种特殊需求时,才临时启用线段树。这个决策准则对 AI 工程师同样适用:大部分增量式聚合任务(CTR 统计、向量相似度排名、流式频率)BIT 已游刃有余,不必杀鸡用牛刀。毕竟,能用一把小钥匙打开的门,就不要扛着一根大撬棍。
- T_BIT = O(log n)单次单点更新或前缀查询的时间复杂度。
- S_BIT = O(n)只需长度为 n+1 的数组存储。
- 对比线段树线段树时间同为 O(log n),空间为 O(4n)。
- 决策准则只做可加减的前缀统计用 BIT,需要区间最值再升级线段树。
AI 应用场景
推荐系统实时 CTR 统计
阶段:特征工程与在线服务
解决的问题:推荐系统需要实时统计“某个商品在最近 N 次曝光中的点击率”。传统做法用滑动窗口数组,每来一条新数据都要重算窗口内和,复杂度 O(N);当曝光量每秒上万时 CPU 很快被打满。用树状数组把每次曝光/点击作为单点增量写入 BIT,前缀和查询直接给出窗口内累计值,O(log N) 的消耗让实时 CTR 计算变得轻如鸿毛。
具体案例:某新闻 App 的信息流推荐中,每条新闻的“最近 1000 次曝光点击率”需要毫秒级刷新。工程师将曝光序列映射到 BIT 下标(循环复用),每次点击执行单点 +1,查询时用前缀和相减得到最近 1000 次中的点击数,再除以 1000 即得实时 CTR。该方案单机可支撑每秒 5 万次曝光。
流式文本词频统计
阶段:数据预处理与特征提取
解决的问题:NLP 流式管线中需要在滑动窗口内统计某个 token 的频率(如 TF-IDF 变体、共现统计)。用 Python 列表切片再 count 是 O(窗口长度),高吞吐语料下不可行。BIT 以 token 的离散化 ID 为下标维护频次,窗口滑动时旧 token 出窗口做单点 -1,新 token 进来做单点 +1,查询某 token 窗口频率只需一次前缀和。
具体案例:在实时翻译系统中,系统需要统计最近 5000 个源语言 token 中每个词的出现次数,用于动态调整领域词汇权重。接入 BIT 后,每次 token 滑入滑出仅需两次 O(log V) 操作(V 为词表大小),吞吐量从每秒 2000 词提升到 8 万词。
Python 演示
class Bit: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) def lowbit(self, x): return x & -x def add(self, idx, delta): while idx <= self.n: self.tree[idx] += delta idx += self.lowbit(idx) def prefix(self, idx): res = 0 while idx > 0: res += self.tree[idx] idx -= self.lowbit(idx) return res def range_sum(self, l, r): return self.prefix(r) - self.prefix(l - 1) def inversion_count(nums): sorted_vals = sorted(set(nums)) rank = {v: i + 1 for i, v in enumerate(sorted_vals)} bit = Bit(len(nums)) inv = 0 for j, x in enumerate(nums): r = rank[x] inv += j - bit.prefix(r) bit.add(r, 1) return inv if __name__ == "__main__": bit = Bit(8) data = [3, 1, 4, 1, 5, 9, 2, 6] for i, v in enumerate(data, 1): bit.add(i, v) print("前5项和:", bit.prefix(5)) print("区间[2,6]和:", bit.range_sum(2, 6)) bit.add(3, 2) print("更新后前6项和:", bit.prefix(6)) print("逆序对数:", inversion_count([5, 3, 2, 4, 1]))
tree = [0] * (n + 1)下标从 1 开始,多开一格数组存放管家账本return x & -x核心 lowbit 运算,一把钥匙算出管辖跨度idx += self.lowbit(idx)单点更新时向右上方攀升,逐级通知上级管家idx -= self.lowbit(idx)前缀查询时向左下方跳跃,逐级收集管家账本return self.prefix(r) - self.prefix(l - 1)区间和 = 两个前缀相减,抵消中间冗余inv += j - bit.prefix(r)逆序对核心公式:已扫描元素中比当前元素大的数量if __name__ == "__main__":可执行入口,直接运行即可看到全部演示输出
常见误区
误区我以为 `x & (-x)` 里的负号就是把 x 变成负数,比如 lowbit(6) = 6 & (-6),数学上 -6 是一个负数,跟 6 按位与怎么会得到正数 2?感觉特别玄乎,背下来就算了。
正解这里的负号不是数学乘法意义上的“取负”,而是计算机的补码表示。在补码中,-x 等于 ~x + 1,这个操作会把 x 的二进制最低位 1 保留下来,同时把它左边所有位取反。于是 x & (-x) 恰好只留下最低位的那个 1。拿 6 举例:6 = (110)₂,-6 的补码是 (…1010)₂,两者相与得 (010)₂ = 2。就像管家制度里的“跨度密码锁”,不是负号有魔法,而是补码恰好放大了最低位 1 的痕迹。
为何常见:大多数人第一次接触位运算时,教材都直接丢公式不解释补码,大家就用“数学负号”的直觉去理解,自然觉得玄学。
误区数组下标不都是 0 开始吗?我把 BIT 也写成从 0 开始,结果查询时 while (i >= 0) 永远减不到负数,程序直接卡死。我觉得是循环条件写错了。
正解根本原因不是循环条件,而是 BIT 的设计前提就是“下标从 1 开始”。因为 lowbit(0) = 0,如果从 0 出发执行 i -= lowbit(i),会永远停留在 0 上;同理 i += lowbit(i) 也会卡死。下标从 1 开始后,查询链 7 → 6 → 4 → 0 能在有限的 log n 步内自然终止。可以理解为:管家编号从 1 楼开始,没有“0 楼管家”,这样每次问询都在向真实楼层编号收敛。
为何常见:习惯了 Python/Java 的 0-based 数组,下意识照搬到 BIT 上,忽略了 lowbit(0) 这个特例。
误区BIT 能做区间和,那我把每个节点的值换成“区间最大值”,不就能用 BIT 做 RMQ 区间最值查询了吗?反正都是 O(log n)。
正解BIT 能工作的前提是“两个前缀可以相减抵消”。最大值操作不可逆——max(a) - max(b) 无法还原出中间区间的最大值,前缀抵消的策略就失效了。类比管家制度:如果你问“前 6 层谁家最热闹”和“前 2 层谁家最热闹”,这两个信息相减并不能推出“3~6 层谁最热闹”。所以区间最值需求下,BIT 只能支持“单点更新 + 整体最值”,真正做区间 RMQ 还是得上线段树。
为何常见:看到 BIT 的“区间和”很好用,就想推广到其他操作,没意识到“可减性”是 BIT 成立的基础。
误区我想给 3~6 层统一加 2 人,就直接调用 BIT 的 add 方法,把 3、4、5、6 这四层各加一遍,觉得这样就行,结果查询前缀和全乱了。
正解BIT 原生只擅长单点修改。要实现“区间修改”,正确姿势是引入差分数组 diff:在 diff[l] += v、diff[r+1] -= v 两个位置做单点更新。这样任意位置的真实变化量 = diff 的前缀和,依然可以用 BIT 高效查询。如果还要区间查询,就得用两个 BIT,一个维护差分本身,另一个维护“下标 × 差分”的加权值。只调用四次 add 虽然单点上也对,但完全失去了区间修改“一次标记、批量生效”的意义,还会让修改量大的场景退化回 O(区间长度)。
为何常见:把“区间修改”简单拆成多个“单点修改”,没有利用差分把区间操作压缩成 O(log n) 的技巧,等于重新发明了低效方案。
面试题
Q1. 在树状数组中,lowbit(6) 的值是多少?
- A.1
- B.2← 正确答案
- C.4
- D.6
答案:B。6 的二进制是 (110)₂,最低位的 1 对应数值 2,所以 lowbit(6) = 2。这就像 6 楼管家只负责 2 层楼(5~6 层)。A 是 lowbit(奇数) 的结果,C 是 lowbit(4) 或 lowbit(12) 的结果,D 是 6 本身而不是它的最低位 1。
Q2. BIT 单次前缀和查询的时间复杂度是多少?
- A.O(1)
- B.O(log n)← 正确答案
- C.O(n)
- D.O(n log n)
答案:B。前缀和查询通过 i -= lowbit(i) 跳跃,每次跳跃至少消去二进制中一个 1,而二进制中 1 的个数不超过 log₂n,所以总步数为 O(log n)。就像物业查前 7 层只问了 3 位管家,而不用一层层跑完 7 层。A 只有空间换时间的预处理才能做到,C 是朴素数组累加,D 是排序复杂度。
Q3. 实时游戏排行榜中,玩家分数从 800 涨到 900,用 BIT 维护分数频次时应该执行什么操作?
- A.区间修改
- B.单点更新← 正确答案
- C.前缀查询
- D.离散化
答案:B。分数从 800 变成 900,本质是“800 分段的频次减 1、900 分段的频次加 1”,这是两次单点更新。BIT 的看家本领就是单点更新 O(log n) 完成。A 是给一段区间统一加值,C 是查询操作,D 是预处理手段不是动作。
Q4. BIT 的下标为什么必须从 1 开始?
- A.为了节省一个数组元素
- B.因为 lowbit(0)=0,从 0 开始查询链会死循环← 正确答案
- C.为了配合 C 语言的指针运算
- D.因为 0 号管家负责的区间不存在
答案:B。如果下标从 0 开始,i -= lowbit(i) 中 lowbit(0)=0,查询会永远停在 0 无法终止;更新同理。下标从 1 开始,查询链如 7→6→4→0 能在 log n 步自然结束。A 不成立,反而多了开头的空位;C、D 不是根本原因。
Q5. 关于单点更新的向上攀升路径,下列说法正确的是?
- A.每一步执行 i += lowbit(i)← 正确答案
- B.路径上的 lowbit 值严格非减← 正确答案
- C.总步数为 O(log n)← 正确答案
- D.路径终点一定是 2 的幂次
答案:ABC。更新路径从 i 开始反复 i += lowbit(i),每一步的 lowbit 至少翻倍(非减),步数不超过 log₂n,所以 A、B、C 都对。D 错,例如 n=7 时更新位置 3:路径 3→4→8 到 8 就停了,而 n=10 时更新 3:3→4→8→16 超过 n 终止,终点是 16 = 2⁴,看起来像 2 的幂但实际只要 i <= n 都会继续,比如 n=100 时更新 97:97→98→100→104,终点 100 不是 2 的幂,D 说法不成立。
Q6. 用 BIT 求区间 [l, r] 的元素和,正确公式是?
- A.prefix(r) + prefix(l)
- B.prefix(r) - prefix(l-1)← 正确答案
- C.prefix(l) - prefix(r-1)
- D.prefix(r) + prefix(l-1)
答案:B。区间和 = 前 r 项之和减去前 l-1 项之和,相当于把绳子总长剪掉起点前那段。A 和 D 是相加会重复计算,C 把两个端点的角色搞反了。举个具体例子:求 [3,5] 的和,就是 prefix(5) - prefix(2),B 中 l-1 = 2 恰好对应。
Q7. 在逆序对计数的 BIT 解法中,离线离散化(rank 映射)的主要作用是什么?
- A.把数据压缩成连续正整数,使 BIT 下标紧凑← 正确答案
- B.对数据排序,方便后续二分查找
- C.去重,避免重复计数
- D.把负数变成正数
答案:A。原数组数值可能很大甚至为负(如 10⁹ 或 -5),BIT 下标必须是正整数且尽量紧凑。离散化把所有不同值映射到 1..m 的连续排名上,BIT 只需 m 个位置即可工作。B 中的排序是离散化的副产品而非目的,C 中的去重是排序时顺手做的,D 只是部分场景的附带效果。
Q8. 要实现“区间修改(一段统一加 v)+ 区间查询(求一段总和)”,BIT 的正确做法是?
- A.朴素 BIT 直接对每个点单点更新
- B.差分数组 + 单点更新即可
- C.用两个 BIT,一个维护差分,一个维护加权差分← 正确答案
- D.必须改用线段树
答案:C。仅用差分数组(方案 B)可以高效做区间修改,但查询需要再对差分数组做前缀和还原,若频繁查询复杂度会退化。正确方案是用两个 BIT:BIT1 维护差分 diff[i],BIT2 维护 diff[i] × i,这样区间查询 = (r+1)*sum(diff, r) - sum(diff*i, r) - (l)*sum(diff, l-1) + sum(diff*i, l-1),仍然 O(log n)。A 退化为 O(n log n),D 没必要——双 BIT 完全够用。
Q9. 下列哪些问题适合用树状数组高效解决?
- A.单点修改 + 区间求和← 正确答案
- B.前缀最小值查询(值会动态更新)
- C.动态排名:查询第 k 小的分数← 正确答案
- D.求静态数组每个位置左边比它小的元素个数← 正确答案
答案:ACD。A 是 BIT 最经典的应用;C 用 BIT 维护频次后二分即可找到第 k 小;D 本质是逆序对变体,逐个插入并查询前缀和即可。B 错误,min 操作不可减——前缀最小的信息无法通过两个前缀相减还原出区间最小,所以 BIT 无法直接支持前缀最值查询。
Q10. 在 BIT 上用“两个前缀最大值相减”来求区间最大值,会得到什么结果?
- A.正确结果,因为 max 是结合律的
- B.无意义的结果,因为 max 操作没有逆运算← 正确答案
- C.正确结果,但时间复杂度变高
- D.溢出错误
答案:B。区间和能被前缀差出来,是因为加法有逆运算“减法”;而最大值没有逆运算——max(a, b) - max(a) 无法还原出 b。比如前缀 [1..6] 最大值是 9,前缀 [1..2] 最大值是 3,9 - 3 = 6 并不是 [3..6] 的最大值(可能是 7)。这就像管家制度里“最热闹的楼层”无法通过两次汇报相减得出,BIT 的区间查询本质上依赖可减性,所以求区间最值需要线段树。