树状数组:魔法账本,快速查账与记账

树状数组是一种高效的数据结构,常用于处理区间求和与单点修改问题。它通过将数据分段存储,并利用二进制的特性,实现了快速查询和更新。树状数组的核心思想是将区间划分为多个小段,每段长度为2的幂次,从而在查询和修改时只需操作相关段,大幅提升效率。其应用场景广泛,包括区间求和、逆序对计数、动态排名等。与线段树相比,树状数组更轻量,适合处理“查一段、改一天”的问题,但在复杂区间操作上稍显不足。通过树状数组,用户可以像使用“魔法账本”一样,快速完成数据的统计与更新。


一、树状数组是什么?——“魔法账本”

比喻:树状数组就像一个会魔法的账本,能快速帮你算出一段时间内的总收入,还能随时修改某一天的收入。

现实问题

假如你每天记账,想知道第1天到第n天的总收入,或者某一天的收入变了,怎么高效处理?

  • 普通做法:每次都把1到n天的收入加一遍,慢!
  • 树状数组:只用查几页账本,立刻算出结果,快!

二、树状数组的实现原理——“分段记账,巧妙合并”

1. 分段记账

  • 你把账本分成很多小段,每一段都记着一部分天数的总收入。
  • 某一天的收入变了,只需要更新相关的小段。
  • 查询某几天的总收入,只需要查查相关的小段,把它们加起来。

2. 巧妙合并

  • 每一段的长度不是随便分的,而是2的幂次(1、2、4、8……)。
  • 这样可以用二进制巧妙地合并和拆分。

画面感:
像你有一摞账本,每本账本记着1天、2天、4天、8天……的收入。
查账时,你只需要翻几本账本就能算出总收入。


三、树状数组的结构——“二进制魔法”

  • 树状数组其实是一个一维数组,但每个位置都代表着一段区间的和。
  • 这些区间的划分方式和二进制有关。

举例:

假设有8天的收入,树状数组的下标和区间如下:

下标代表的区间
11
21-2
33
41-4
55
65-6
77
81-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)

五、口诀升级

树状数组巧分段,
二进制跳格快。
查一段,改一天,
逆序排名都能算。
账本轻巧查账快,
区间操作有妙招!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值