树状数组是一种支持单点修改和区间查询的,代码量小的数据结构,在这里我们可以理解为线段树的简化版本
- 树状数组对单点修改和区间查询的时间复杂度都为
- 对于前缀和,树状数组在更新值的时候
引入
对于数组 其中元素为 []
有次询问,每次询问可能涉及两种操作
- 对 加
- 给定区间,求区间和
-
对数组,我们朴素的使用前缀和也可以实现上面的两个操作,但是对于询问次数特别多且特别大的时候,前缀和在修改的时间复杂度就不能满足题目要求
-
我们试想,将的每个元素两两加和得到一个新的,对做操作这样我们需要查询和修改的就变为了
-
我们继续重复以上步骤,将每次的的元素都两两加和
-
最终我们会得到下面的一幅图:
-
我们发现在我们进行修改和区间和的时候,有些元素是完全不会被用上的
- 即:每行的第偶数个元素都是没必要的
- 比如我们需要计算 的区间和,我们只需要计算最下面数组的 + 倒数第二行的 即可。其余的都证明
-
所以我们将从上到下每个数组偶数个元素删去
-
发现剩下的数据正好有个,即我们的新数组可以表示为一个为长度的新数组,即树状数组
-
数组中的每一个元素,都代表莫一个区间
-
则我们可以在这个新的数组中了解树状数组两个操作,即单点修改和区间查询
-
那我们如何查找每个元素对应的区间呢?
lowbit(i)
函数,这个函数会返回数 i 最后一位二进制为1的数对应的数 如 则lowbit(5)
lowbit(i){ return i&(-i);
- 我们发现对上图的每个元素下标 , 即为从下到上的区间位置和区间长度
- 也就是说,序号为的序列正好就是长度为lowBit()且以结尾的序列。
回到引入题目,我们现在需要构建一个树状数组,那我们从树状数组的单点修改入手:
单点修改
- 当我们想要修改某一个数的时候,我们需要找到包含这个数的所有区间
- 而树状数组的性质告诉我们,某一个序列上方的序列在树状数组的下标中为:
- 则我们只需要不断的向上累加直到超过长度就可以了
// 在i位置加k
void add(int i,int k)
{
while(i < N){
b[i] += k;
i += lowbit(i);
}
}
- 而对于初始化,我们只需要一开始设置一个长度为n,初始值0的数组,将每个位加上元素值即可
void init()
{
for(int i = 1 ; i <= n ; i++) add(i,v[i]);
}
Important
我们约定俗成的规定树状数组的下标由1开始,避免lowbit(0)的死循环问题
区间查询
- 树状数组能够做到的是对求到的前缀和,而根据数上前缀和的原理,我们便可以求解区间
- 前缀和的求解非常简单,只需要求i位置对应的区间及其前面的区间即可,可以用lowbit定位到每一个区间
int getsum(int i)
{
int cnt = 0;
while(i > 0){
cnt += b[i];
i -= lowbit(i);
}
return cnt;
}
- 而对于区间求和就可以使用数上前缀和的方法
int range_sum(int l ,int r)
{
return getsum(r) - getsum(l-1);
}
Add
下面给出一个简单易于书写的fenwick数模板
struct fenwick{
i64 n;
vector<i64> bit;
fenwick(i64 n): n(n) ,bit(n+1,0){}
void add(i64 i,i64 v = 1){for(;i <= n;i += ( i & -i )) bit[i]+=v; }
i64 sum(i64 i){i64 r = 0;for(;i >= 0;i -= i&-i)r += bit[i];return r;}
i64 sum(i64 r,i64 l){return sum(r) - sum(l - 1);}
}