一.使用场景
1.前缀和查询
2.单点更新
3.区间查询和更新
4.逆序对计数
5.数组频率计数
二.lowbit()运算
在使用树状数组的时候我们先要学习,lowbit()运算。即非负整数n在二进制表示下最低为1及其后面的0构成的数值。
考虑一个正整数 x 的二进制表示形式,例如 x = 12,其二进制表示为 1100。我们观察到,x 的二进制表示中最低位的 1 所对应的值是 4(即 x & (-x))。
lowbit 的原理就是利用了这个性质,通过按位与运算(&)来获取二进制数中最低位的 1 所代表的数值。具体而言,对于任意一个正整数 x,其 lowbit 值就是 x & (-x)。
举例来说,对于 x = 12,其二进制表示为 1100,那么 x & (-x) = 4。同样地,对于 x = 10,其二进制表示为 1010,那么 x & (-x) = 2。
lowbit 的作用在于帮助确定树状数组中每个索引位置所代表的区间大小。在树状数组中,每个索引位置 i 的二进制表示中,最低位的 1 所对应的区间大小就是 i & (-i)。代码如下:
//非负整数n在二进制表示下最低为1及其后面的0构成的数值
int lowbit(int i)
{
return i & -i;
}
例如,索引 6 的二进制表示为 110(从右到左按位编号),那么对应区间的大小为 2,即包括原始数组中的第 6、5 两个元素。类似地,索引 10 的二进制表示为 1010,对应区间的大小为 2,即包括原始数组中的第 10、9 两个元素。
三.原理
如果要计算a1到a8的和,我们需要从a1加到a8,这样太过麻烦。我们可以两两相加,如a1+a2=a9。以此类推,最后计算得到总和a15 = a14 +a13。
现在假设有一个树状数组 sums_,其中 sums_[i] 表示原始数组中前 i 个元素的前缀和。那么 sums_[i] 可以被分割成若干个区间的和,每个区间的长度都是某个 2 的幂次(例如 1、2、4、8...)。
其中 sums_[i] 的二进制表示中最低位的 1 对应一个区间的和,而剩下的部分可以用来表示包含第 i 个元素的若干区间的和。这样,我们可以通过修改和查询树状数组的特定索引位置的值,实现单点修改和区间查询操作。
因此我们可以构建一个森林结构(图不太好看)
t[x]保存以x为根的子树中叶节点值的和
将每个t[x]的x转化为二进制后,我们发现每一层末尾的0的个数相同,0的个数与其覆盖的长度有关。我们还能发现t[x]节点的父节点为t[x + lowbit(x)]。
我们可以得到:
三.实现操作
1.单点修改
先上代码:
// 单点修改,将第 i 个元素加上 delta
void update(int i, int delta)
{
while (i < n)
{
arr[i] += delta;
i += lowbit(i);
}
}
我们在进行单点修改的时候,我们需要在整棵树上维护这个值,需要一层层向上找到父节点(运用上述公式)
例如,如果我要对数组中的a[5]+delta,即我要对t[5]及其父节点都加上delta。则t[5],t[6],t[8]都要加5。
2.前缀和
先上代码:
// 查询区间 [1, i] 的和
int query(int i)
{
int sum = 0;
while (i > 0)
{
sum += arr[i];
i -= lowbit(i);
}
return sum;
}
查询这个点的前缀和,需要从这个点向左上找到上一个节点,回到上一个节点的操作即为: i - lowbit(i),i为这个节点的下标。例如我要求sum(7),按照我们上面画的森林图,我需要加上t[7],t[6],t[4]。
四.总代码
#include<iostream>
using namespace std;
#define n 5//定义n为5
//初始化数组为n
int arr[n];
//非负整数n在二进制表示下最低为1及其后面的0构成的数值
int lowbit(int i)
{
return i & -i;
}
// 单点修改,将第 i 个元素加上 delta
void update(int i, int delta)
{
while (i < n)
{
arr[i] += delta;
i += lowbit(i);
}
}
// 查询区间 [1, i] 的和
int query(int i)
{
int sum = 0;
while (i > 0)
{
sum += arr[i];
i -= lowbit(i);
}
return sum;
}