树状数组是一种高效的数据结构,常用于处理区间求和与单点修改问题。它通过将数据分段存储,并利用二进制的特性,实现了快速查询和更新。树状数组的核心思想是将区间划分为多个小段,每段长度为2的幂次,从而在查询和修改时只需操作相关段,大幅提升效率。其应用场景广泛,包括区间求和、逆序对计数、动态排名等。与线段树相比,树状数组更轻量,适合处理“查一段、改一天”的问题,但在复杂区间操作上稍显不足。通过树状数组,用户可以像使用“魔法账本”一样,快速完成数据的统计与更新。
一、树状数组是什么?——“魔法账本”
比喻:树状数组就像一个会魔法的账本,能快速帮你算出一段时间内的总收入,还能随时修改某一天的收入。
现实问题
假如你每天记账,想知道第1天到第n天的总收入,或者某一天的收入变了,怎么高效处理?
- 普通做法:每次都把1到n天的收入加一遍,慢!
- 树状数组:只用查几页账本,立刻算出结果,快!
二、树状数组的实现原理——“分段记账,巧妙合并”
1. 分段记账
- 你把账本分成很多小段,每一段都记着一部分天数的总收入。
- 某一天的收入变了,只需要更新相关的小段。
- 查询某几天的总收入,只需要查查相关的小段,把它们加起来。
2. 巧妙合并
- 每一段的长度不是随便分的,而是2的幂次(1、2、4、8……)。
- 这样可以用二进制巧妙地合并和拆分。
画面感:
像你有一摞账本,每本账本记着1天、2天、4天、8天……的收入。
查账时,你只需要翻几本账本就能算出总收入。
三、树状数组的结构——“二进制魔法”
- 树状数组其实是一个一维数组,但每个位置都代表着一段区间的和。
- 这些区间的划分方式和二进制有关。
举例:
假设有8天的收入,树状数组的下标和区间如下:
下标 | 代表的区间 |
---|---|
1 | 1 |
2 | 1-2 |
3 | 3 |
4 | 1-4 |
5 | 5 |
6 | 5-6 |
7 | 7 |
8 | 1-8 |
- 下标i负责的区间长度是i的二进制最低位1所代表的数。
四、树状数组的操作——“查账和记账”
1. 查询前缀和(查账)
- 想查1到n天的总收入,只需要不断往前跳,查相关账本,把它们加起来。
- 跳的方式:每次减去当前下标的二进制最低位。
画面感:
像你查账时,每次都跳到上一本账本,直到查完为止。
2. 单点修改(记账)
- 某一天的收入变了,你要把相关账本都更新一下。
- 跳的方式:每次加上当前下标的二进制最低位。
画面感:
像你记账时,每次都把相关账本都补记一笔。
五、树状数组的代码实现(伪代码)
# 初始化
tree = [0] * (n + 1)
# 单点增加
def add(i, x):
while i <= n:
tree[i] += x
i += i & -i # 加上最低位的1
# 查询前缀和
def query(i):
res = 0
while i > 0:
res += tree[i]
i -= i & -i # 减去最低位的1
return res
六、树状数组的应用场景
1. 区间求和,单点修改
- 统计一段时间内的总收入、总分数、总销量等。
- 适合“查一段、改一天”的场景。
2. 逆序对计数
- 排序相关问题,比如统计数组中有多少对(i, j)满足i < j且a[i] > a[j]。
3. 动态排名、在线统计
- 比如实时统计某个分数段的人数、排名等。
4. 区间频率统计
- 比如统计某个区间内某个数字出现的次数。
七、生活小剧场
小明开了个小卖部,每天记账。
- 他用树状数组做账本,每天记账、查账都很快。
- 有一天,老板娘问:“1号到7号一共赚了多少钱?”
小明只翻了几本账本,立刻答出来。 - 有一天,发现3号的账记错了,小明只改了几本账本,所有统计都自动更新了。
八、口诀总结
树状数组分段记,
二进制巧合并。
查账快,记账灵,
区间求和最精明!
我们继续用生动形象的方式,深入讲讲树状数组的进阶内容,包括它和线段树的区别、复杂应用、以及生活化的理解。
一、树状数组和线段树的区别——“账本和账柜”
1. 账本(树状数组)
- 只适合“查一段、改一天”。
- 账本薄、查账快、记账快,但只能查总收入,不能查任意区间的详细账目。
2. 账柜(线段树)
- 适合“查任意区间、改任意区间”。
- 账柜大、查账灵活,但比账本重,操作稍慢。
画面感:
树状数组像一本流水账,查总账很快;
线段树像一个大账柜,能查每个抽屉的账,但搬动账柜要花点力气。
二、树状数组的进阶应用
1. 区间修改、单点查询(差分思想)
- 如果你想“某几天的收入都加100”,可以用差分数组+树状数组。
- 只需在区间起点加100,终点+1减100,查某一天时前缀和就是当天收入。
画面感:
像你在账本上记:“从3号到7号,每天都多赚100”,只需在3号记+100,8号记-100,查账时自动算好。
2. 区间查询、区间修改
- 经典树状数组只能“查一段、改一天”。
- 但可以用两个树状数组配合,实现“查一段、改一段”。
- 具体做法是维护两个树状数组,分别记录“加了多少”和“加了多少*天数”,查账时用公式算出区间和。
画面感:
像你有两本账本,一本记“加了多少钱”,一本记“加了多少钱*天数”,查账时合起来算。
3. 逆序对计数
- 给定一个数组,统计有多少对(i, j)满足i < j且a[i] > a[j]。
- 做法:从后往前遍历,每次用树状数组统计比当前数小的数有多少个。
画面感:
像你在排队,每来一个人,问前面有多少人比自己矮,树状数组能立刻告诉你。
4. 动态排名
- 比如实时统计某个分数在所有人中的排名。
- 做法:用树状数组统计每个分数出现的次数,查前缀和就是排名。
画面感:
像你有一排分数格子,每个格子里放着人数,查某个分数前面有多少人,树状数组一查就知道。
三、树状数组的生活化理解
1. “魔法账本”升级
- 你有一本账本,每一页都记着不同天数的总账。
- 记账时,只需翻几页账本,所有账目都自动更新。
- 查账时,也只需翻几页账本,立刻知道总账。
2. “二进制跳跃”
- 每次查账或记账时,你都像在玩跳格子游戏,每次跳的步数是2、4、8……,很快就能跳到终点。
四、树状数组的代码进阶(区间修改+区间查询)
# 区间加,区间求和
class BIT:
def __init__(self, n):
self.n = n
self.tree1 = [0] * (n + 2)
self.tree2 = [0] * (n + 2)
def _add(self, tree, i, x):
while i <= self.n:
tree[i] += x
i += i & -i
def range_add(self, l, r, x):
self._add(self.tree1, l, x)
self._add(self.tree1, r + 1, -x)
self._add(self.tree2, l, x * (l - 1))
self._add(self.tree2, r + 1, -x * r)
def _query(self, tree, i):
res = 0
while i > 0:
res += tree[i]
i -= i & -i
return res
def prefix_sum(self, i):
return self._query(self.tree1, i) * i - self._query(self.tree2, i)
def range_sum(self, l, r):
return self.prefix_sum(r) - self.prefix_sum(l - 1)
五、口诀升级
树状数组巧分段,
二进制跳格快。
查一段,改一天,
逆序排名都能算。
账本轻巧查账快,
区间操作有妙招!