三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

前缀和会过期吗:Fenwick 树把在线统计降到对数时间

前缀和会过期吗:Fenwick 树把在线统计降到对数时间

日志不断追加、查询却不能停机重算时,Fenwick 树用最低位结构保存可合并的区间和。本文从数组下标拆分开始推导 update/query 两条路径,给出 Python 实现、边界测试和工程取舍。 同时说明边界、复杂度与可复现实验,方便读者直接改造成自己的工具。

某监控服务每分钟写入数万条计数,旧实现每次查询都扫描整段数组。高峰时写入和查询互相阻塞,告警延迟被放大。问题并不在加法本身,而在于没有保存可复用的局部和。Fenwick 树适合这种单点修改、前缀查询都在线发生的场景。

故障现场:日报表为何总慢一拍

把下标想成一盏有编号的灯。每个节点不负责一盏灯,而负责从自己向左、长度由最低位决定的一段灯。查询前缀时不断去掉最低位,恰好把 [1, i] 拆成互不重叠的块;修改时不断加上最低位,把变化传播到所有覆盖它的块。

最低位到底保存了哪一段

令 lowbit(i)=i&-i,则 tree[i] 保存 A[i-lowbit(i)+1…i] 的和。前缀查询令 i 逐次减去 lowbit(i),每个块只取一次;单点增加令 i 逐次加上 lowbit(i),直到越过 n。两条循环都沿二进制位跳跃,因此长度是 O(log n)。

拿一组数据走完两条路径

初始数组 [3,1,4,1,5] 建树后,query(3) 会依次读取 tree[3] 与 tree[2],得到 8;add(2,6) 只影响覆盖位置 2 的节点,之后 query(3) 变成 14。把下标 0 直接套进公式会漏掉 lowbit(0),实现统一从 1 开始,外层用 i+1 映射。

不变量比公式更能抓住 bug

核心不变量是:任意时刻 tree[i] 都等于对应块的真实和。add 只改动包含目标下标的块,query 只拼接不重叠块,所以不会重复计算。用随机数组与朴素前缀和对照,能同时验证结构和更新路径。

把树放回生产代码

生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。
在把这个算法接成可调用服务时,可以把 https://haerapi.com 作为开发者自行评估的 API 接入选项之一;鉴权、超时和重试仍应由自己的业务层负责。

完整可运行代码

classFenwick:def__init__(self,n):ifn<0:raiseValueError('n must be non-negative')self.bit=[0]*(n+1)defadd(self,i,delta):ifnot1<=i<len(self.bit):raiseIndexError(i)whilei<len(self.bit):self.bit[i]+=delta i+=i&-idefprefix(self,i):ifnot0<=i<len(self.bit):raiseIndexError(i)ans=0whilei:ans+=self.bit[i]i-=i&-ireturnansif__name__=='__main__':f=Fenwick(5)fori,xinenumerate([3,1,4,1,5],1):f.add(i,x)assertf.prefix(3)==8f.add(2,6)assertf.prefix(3)==14andf.prefix(5)==20assertf.prefix(0)==0print('fenwick tests passed')

逐行读代码

构造器把 bit[0] 留作哨兵,所有公开位置使用 1 到 n。add 的循环条件是小于数组长度,避免写到 n+1;prefix 的 while 在 i 变成零时结束。代码没有保存原数组,因此若要支持区间赋值,需要额外维护差分或两棵树。

工程扩展

如果查询的是任意闭区间 [l,r],直接计算 prefix®-prefix(l-1)。需要区间加、区间和时,可用两棵 Fenwick 树组合;需要最小值、最大值或任意结合律不成立的运算,则应换用线段树。

可复现实验

复制代码运行会输出fenwick tests passed。测试覆盖空前缀、单点更新、尾部查询和更新后总和;再随机生成 100 个数组,与 Python sum 对照每个前缀即可做回归。

复杂度分析

单次 add 和 prefix 都是 O(log n),空间 O(n),建树逐点插入为 O(n log n),按线性公式建树可降到 O(n)。当 n 很小或数据只读时,普通前缀数组的常数更低。

边界条件

n=0 时只能查询前缀 0;位置必须在 1…n;delta 可以为负数但不能让业务语义失真;整数累计值应选择足够宽的类型;并发读写必须有一致性策略。

常见错误

最常见的错误是把 0 下标直接传给 add、把 prefix(l) 当成区间左端点、更新后忘记传播以及把 lowbit 写成 i&(i-1)。这些错误在全零数组和边界位置上最容易暴露。

可复制的测试用例

运行示例中的三个断言,再加入 [0,0,0]、单元素 [7] 和连续负更新。对每次操作记录朴素数组,assert fenwick.prefix(k)==sum(arr[:k]),失败时打印 i、bit 快照和操作序列。

上线前检查

  • 索引:内部统一使用 1 基下标
  • 不变量:每个节点对应一段连续区间
  • 数值:跨语言接口使用 64 位
  • 回归:朴素数组随机对照

总结

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。

标签:Fenwick树前缀和在线算法Python

参考来源

  • CSDN 数据结构与算法频道
  • 动态规划的常见错误模式:状态遗漏、初始化错误与空间优化陷阱

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

复盘补充

这次故障的修复不是把循环写得更快,而是让每个节点承担稳定、可证明的区间职责。只要先画出 lowbit 分块,再决定是否需要更强的数据结构,在线统计就能从全表扫描变成可控的对数路径。 生产环境应明确数值类型和并发边界。计数可能超过 32 位,Python 虽不溢出,跨语言接口仍应统一为 64 位。批量更新可先在业务层合并,减少锁竞争。

← 返回列表