一、前言:树状数组的进阶之路
树状数组(BIT)的核心本质是以树形结构高效维护数组前缀和 ,原生仅支持两种基础操作:单点修改 、前缀和查询。依托这一核心特性,结合差分思想,我们已经解锁了两种经典用法:
-
基础版:维护原数组,实现「单点修改、区间查询」(洛谷P3374);
-
进阶1版:维护差分数组,实现「区间修改、单点查询」(洛谷P3368)。
但在算法竞赛中,更通用、更高频的需求是:同时支持区间修改、区间查询 。普通单树状数组无法满足该需求,因此我们基于「差分降维思想」进一步推导,使用两个树状数组配合差分,实现树状数组的终极进阶用法。
核心思路不变:利用差分将复杂的区间修改,降维转化为树状数组擅长的单点修改,再通过数学推导弥补区间查询的能力缺失。
二、前置知识回顾
1. 差分核心定义
设原数组为 a[]a[]a[],差分数组为 d[]d[]d[],满足:d[i]=a[i]−a[i−1]d[i] = a[i] - a[i-1]d[i]=a[i]−a[i−1](规定 a[0]=0a[0]=0a[0]=0)。
两大核心性质:
-
区间修改:对 [l,r][l,r][l,r] 全体加 kkk,等价于两次单点修改:d[l]+=k、d[r+1]−=kd[l]+=k、d[r+1]-=kd[l]+=k、d[r+1]−=k;
-
单点查询:原数组单点值 a[x]=∑i=1xd[i]a[x] = \sum_{i=1}^x d[i]a[x]=∑i=1xd[i](差分数组前缀和)。
2. 旧版局限
单树状数组维护差分数组,只能求出原数组单点值,无法直接求出原数组的区间和。想要实现区间查询,必须重新推导数学公式,引入第二个树状数组。
三、核心数学推导:双树状数组的由来
我们的目标:求出原数组 a[1∼x]a[1\sim x]a[1∼x] 的前缀和 S(x)=∑i=1xa[i]S(x) = \sum_{i=1}^x a[i]S(x)=∑i=1xa[i]。
由差分性质可知 a[i]=∑j=1id[j]a[i] = \sum_{j=1}^i d[j]a[i]=∑j=1id[j],将其代入前缀和公式,得到双重求和式:
S(x)=∑i=1x∑j=1id[j]S(x) = \sum_{i=1}^x \sum_{j=1}^i d[j]S(x)=∑i=1x∑j=1id[j]
通过交换求和顺序 化简(核心步骤):原本是先枚举原数组下标,再枚举差分前缀;交换后统计每个 d[j]d[j]d[j] 被累加的次数。
对于 d[j]d[j]d[j],在 1∼x1\sim x1∼x 的范围内,会被累加 x−j+1x-j+1x−j+1 次,因此:
S(x)=∑j=1xd[j]⋅(x+1−j)S(x) = \sum_{j=1}^x d[j] \cdot (x+1-j)S(x)=∑j=1xd[j]⋅(x+1−j)
展开拆分公式:
S(x)=(x+1)∑j=1xd[j]−∑j=1xj⋅d[j]S(x) = (x+1)\sum_{j=1}^x d[j] - \sum_{j=1}^x j\cdot d[j]S(x)=(x+1)∑j=1xd[j]−∑j=1xj⋅d[j]
结论:必须维护两个数组
从最终公式可以看出,想要计算原数组的前缀和,需要两个前缀和结果:
-
∑d[j]\sum d[j]∑d[j]:差分数组的前缀和;
-
∑j⋅d[j]\sum j\cdot d[j]∑j⋅d[j]:下标乘差分数组的前缀和。
因此我们用两个树状数组分别维护:
-
tr1 :维护差分数组 d[i]d[i]d[i];
-
tr2 :维护加权差分数组 i⋅d[i]i\cdot d[i]i⋅d[i]。
四、重点精讲:双树状数组的更新规则
这是90%教程跳过的核心细节!我们已知区间修改 [l,r]+k[l,r]+k[l,r]+k 只会修改差分数组的两个位置:d[l]+kd[l]+kd[l]+k、d[r+1]−kd[r+1]-kd[r+1]−k。
1. tr1 更新逻辑(常规)
tr1 直接维护 d[i]d[i]d[i],因此跟随差分规则单点修改:
Plain
update(tr1, l, k)
update(tr1, r+1, -k)
2. tr2 更新逻辑(核心重点)
tr2 维护的是 i×d[i]i \times d[i]i×d[i],这是一个被动加权数组 :d[i] 的变化量,会被下标 i 加权放大。
-
位置 lll:d[l]+=kd[l] += kd[l]+=k → 加权值变化:l×kl \times kl×k →
update(tr2, l, l*k); -
位置 r+1r+1r+1:d[r+1]−=kd[r+1] -= kd[r+1]−=k → 加权值变化:−(r+1)×k-(r+1) \times k−(r+1)×k →
update(tr2, r+1, -(r+1)*k)。
五、区间查询公式实现
根据推导的前缀和公式,封装求 [1,x][1,x][1,x] 前缀和的逻辑,再通过「前缀和相减」得到任意区间和 [l,r][l,r][l,r]:
Plain
# 求 [l, r] 区间和
def get(l, r):
# S(r) = (r+1)*sum(d[1~r]) - sum(i*d[i][1~r])
val_r = (r+1)*query(tr1,r) - query(tr2,r)
# S(l-1) = l*sum(d[1~l-1]) - sum(i*d[i][1~l-1])
val_l_1 = l*query(tr1,l-1) - query(tr2,l-1)
return val_r - val_l_1
六、完整实战代码
适配洛谷P3372【模板】线段树1(区间修改、区间查询),采用极简全局函数风格、快速读入、批量输出,无冗余代码,时间复杂度最优 O((n+m)logn)O((n+m)\log n)O((n+m)logn)。
Plain
import sys
input = lambda: sys.stdin.readline().strip()
# 树状数组核心基础函数
def lowbit(x):
return x & -x
def update(tr, x, k):
while x <= n:
tr[x] += k
x += lowbit(x)
def query(tr, x):
res = 0
while x > 0:
res += tr[x]
x -= lowbit(x)
return res
# 计算区间 [l, r] 的和
def get(l, r):
val_r = (r+1)*query(tr1,r) - query(tr2,r)
val_l_1 = l*query(tr1,l-1) - query(tr2,l-1)
return val_r - val_l_1
# 主程序
n, m = map(int, input().split())
a = list(map(int, input().split()))
a = [0] + a # 转为1下标,适配树状数组
# 初始化两个树状数组
tr1 = [0] * (n+2)
tr2 = [0] * (n+2)
# 构建差分数组,初始化双树状数组
for i in range(1, n+1):
dif = a[i] - a[i-1]
update(tr1, i, dif)
update(tr2, i, i * dif)
ans = []
for _ in range(m):
op = list(map(int, input().split()))
if op[0] == 1:
# 区间修改:[x,y] 整体加 k
x, y, k = op[1], op[2], op[3]
update(tr1, x, k)
update(tr1, y+1, -k)
# 重点:加权差分树状数组更新规则
update(tr2, x, x*k)
update(tr2, y+1, -(y+1)*k)
else:
# 区间查询:输出[x,y]区间和
x, y = op[1], op[2]
ans.append(str(get(x, y)))
# 批量输出,避免Python超时
print('\n'.join(ans))
七、复杂度与核心总结
1. 时间复杂度
-
初始化:O(nlogn)O(n\log n)O(nlogn);
-
单次区间修改/区间查询:O(logn)O(\log n)O(logn);
-
整体复杂度:O((n+m)logn)O((n+m)\log n)O((n+m)logn),完全通过洛谷1e5数据范围,无超时风险。
2. 全套树状数组体系闭环
-
单点改、区间查:单树状数组维护原数组;
-
区间改、单点查:单树状数组维护差分数组;
-
区间改、区间查:双树状数组维护「差分、加权差分」。
3. 核心精髓
树状数组本身只擅长单点修改+前缀查询,所有区间操作的能力,全部来自差分降维;而双树状数组的额外加权更新,只是为了抵消双重求和的权重,完美适配区间查询需求。