引言
有一组数,比如说n个,任务是支持一个查询操作Query(L,R):计算L到R区间的和。
如果每次循环来计算,每一个询问计算的复杂度是 O(n) 的,时间上不能接受。
考虑前缀和的思想,先 O(n) 预处理出前缀和,对于每次查询可以 O(1) 解决。
但如果可修改呢?比如可以修改某个元素的值,这时候前缀和失效,如果重新维护,时间上不能接受。
下面要介绍的树状数组(Binary Indexed Tree) 也称Fenwick Tree可以解决上面的在线查询问题。
基本方法(单点修改,区间查询)
重新思考上面前缀和的思想,它为什么可以将O(n)的sum降低为O(1)?显然,其天然保存了sum。为什么在线时会失效?因为它保存的sum一旦有修改会全部失效,需要全部更新,自然复杂度就上去了。
树状数组本质上和前缀数组思想是一样的。但它是partial sum in each node,也就是说我预处理的不是前面所有元素的和,而是部分。可以想到,如果我们在单点修改时,只会影响到含有这个点的node,且node数在量级上可以大幅减少,是不是就可以只对这些node进行更新,在时间上可以接受了。
所以这个部分和怎么去对应就是问题的关键
①是要对于单点修改,只需部分对应的node修改。
②是对于区间查询,利用各个node节点,可以在较优时间内计算出前缀和(两个前缀和之差即为区间和)。
这个对应法则就是二进制数的lowbit
首先我们定义一个数x的lowbit为这个数转为二进制表示后,取其最低位的1不动,其余全置零对应的数。其求法为
lowbit(x)=x&(-x)
为什么是这样呢,举个例子 十进制数116对应的二进制为01110100,显然我们直接观察其lowbit值为00000100
由 lowbit(x)=x&(−x) ,即01110100 & 10001100 确实是00000100
因为最低位的1 其右边全为0 x&-x后 对应1的右边肯定是0了 ,为什么这一位是1呢?正因为右边全为0 取反后全为1了,再加1(即负号取补码)正好为1的统统进位到那一位lowbit,因此 lowbit(x)=x&(−x) 。
这个lowbit有什么用呢?它实际上提供了一种维护partial sum的法则。如下图所示:(图片来自花花酱)
支持步骤可视化,点我
对于一个数,比如说2(0010) 我们规定所有的i=2,i+=lowbit(i) ,i<=n是2对应的所有node ,即对2的add操作,对4、8 node都要做;同理对5的add操作,对6,8 node也都做。这样每个node实际上都有一个“管辖”(覆盖)范围,对应法则就这样找好了。现在我们知道,对于一个修改操作,我们沿着法则(i+=lowbit)往上加就OK了。时间复杂度显然是 O(logn) ,因为是二进制位嘛。
下面关键是在这种规定下,如何去求前缀和
这时我们用 i−=lowbit(i) ,如下图
比如我要求1~3元素的和,我把node 3,2一加就OK ;我要求1~4元素的和,就是node 4 ;我要求1~7元素的和,即为node 4,6,7的和。 你会发现这时候正好是 i−=lowbit(i) 对应的结点。
对应上图和下图,不看什么lowbit公式,简单验证一下可以证明这种对应是正确的。
可是为什么 i+=lowbit(i)i−=lowbit(i) 这样就可以完成对应呢?
一个易于理解的解释是,考虑那些特例,比如2的次幂:1,2,4,8,16……观察它们的二进制:
1 | 00000 |
---|---|
2 | 00010 |
4 | 00100 |
8 | 01000 |
16 | 10000 |
你会发现这些node”好强”,它们能被各种node“到达”。
为啥呢,比如10000
01000+lowbit(01000)可以
01100+lowbit(01100)也可以
01110+lowbit(01110)也可以
01111+lowbit(01111)也可以
换句话说,它表示了上面这些数的和,它们lowbit之后又可以由其它数+lowbit(其他数)表示
另外,我们可以发现10000-lowbit(10000)=0
意思就是它能直接表示前16个元素的和!
再看一个数 15 二进制为1111,你会发现没有哪个数x x+lowbit(x)=15
再看 i−=lowbit(i)
1111-0001=1110(14)
1110-0010=1100(12)
1100-0100=1000(8)
1000-1000=0
也就是1~15元素的和=node15(其实就是元素15)+node14+node8
从上面的分析可以感觉出, i+=lowbit(i)i−=lowbit(i) 是利用二进制的特性,准确的说是两个2的次幂之间数的分布特征(0和1的比例)。这个特征决定了对应关系(即node结点能管辖的范围的多少)。
由于其关系画出来呈现出“分层”,且层数是 logn 级别的,因此形象的称node数组为树状数组。但其还是线性结构
代码实现
//洛谷 P3374
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=500000;
ll node[maxn+10];
ll a[maxn+10];
int n;
inline int lowbit(int x)
{
return (x&-x);
}
void add(int x,ll value)
{
for(int i=x;i<=n;i+=lowbit(i))
node[i]+=value;
}
ll get(int x)
{
ll sum=0;
for(int i=x;i;i-=lowbit(i))
sum+=node[i];
return sum;
}
int main()
{
ios::sync_with_stdio(false);
int m,op,tmp1,tmp2;
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>a[i];
add(i,a[i]);
}
while(m--)
{
cin>>op>>tmp1>>tmp2;
if(op==1){
add(tmp1,tmp2);
}
else{
cout<<get(tmp2)-get(tmp1-1)<<endl;
}
}
return 0;
}
区间修改 单点查询
这里有一种巧妙的构造方法,即差分方程,所谓差分方程其实就是构造a元素的差作为node结点的值
即node1=a1 , node2=a2-a1 , node3=a3-a2……
为什么要这样构造呢?
你会发现现在我的区间和变成了元素的单点值! (很好理解) 这样单点查询就解决了
那怎么进行区间修改呢?
会发现构造的是差值,那貌似只有边界两个点会有影响
即左边点node[L]要+add 右边点的右边邻点node[R+1]要-add
这样就把区间修改 单点查询 → 单点修改 区间查询
总结:差分思想很神奇
代码实现
//洛谷 P3368
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=500000;
ll node[maxn+10];//差分
ll a[maxn+10];
int n;
inline int lowbit(int x)
{
return (x&-x);
}
void add(int x,ll value)
{
for(int i=x;i<=n;i+=lowbit(i))
node[i]+=value;
}
ll get(int x)
{
ll sum=0;
for(int i=x;i;i-=lowbit(i))
sum+=node[i];
return sum;
}
int main()
{
ios::sync_with_stdio(false);
int m,op,l,r,tmp;
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>a[i];
add(i,a[i]-a[i-1]);
}
while(m--)
{
cin>>op;
if(op==1){
cin>>l>>r>>tmp;
add(l,tmp);
add(r+1,-tmp);
}
else{
cin>>tmp;
cout<<get(tmp)<<endl;
}
}
return 0;
}
区间修改 区间查询
有了上面差分数组的思想,我们来看是否可以将其用于“区间修改,区间查询”问题上。
对于区间查询,即求一个区间的和,即求前缀和 ∑ni=1ai ,而 ai=∑ij=1dj
于是前缀和 (prefix_sum=∑ni=1∑ij=1dj=∑ni=1(n−i+1)di=(n+1)∑ni=1di−∑ni=1i∗di
于是可以维护 di 和 i∗di 两个差分数组 O(logn) 来计算前缀和。
至于区间修改,这对差分数组来说是很容易的,即只有边界两个点会有影响。
具体见代码
代码实现
//poj 3468
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn=500000;
ll node1[maxn+10];//差分数组1 维护di
ll node2[maxn+10];//差分数组2 维护i*di
ll a[maxn+10];//原数组
int n;
inline int lowbit(int x)
{
return (x&-x);
}
void add(int x,ll value,ll *node)
{
for(int i=x;i<=n;i+=lowbit(i))
node[i]+=value;
}
ll get(int x,ll *node)
{
ll sum=0;
for(int i=x;i;i-=lowbit(i))
sum+=node[i];
return sum;
}
int main()
{
//freopen("read.txt","r",stdin);
ios::sync_with_stdio(false);
int m,l,r,tmp;
char op;
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>a[i];
add(i,a[i]-a[i-1],node1);
add(i,i*(a[i]-a[i-1]),node2);
}
while(m--)
{
cin>>op;
if(op=='C'){
cin>>l>>r>>tmp;
add(l,tmp,node1);
add(r+1,-tmp,node1);
add(l,l*tmp,node2);
add(r+1,-(r+1)*tmp,node2);
}
else{
cin>>l>>r;
cout<<((r+1)*get(r,node1)-get(r,node2))-(l*get(l-1,node1)-get(l-1,node2))<<endl;
}
}
return 0;
}
Wiki
This structure was proposed by Peter Fenwick in 1994 to improve the efficiency of arithmetic coding compression algorithms.[1]
Paper
Peter M. Fenwick (1994). “A new data structure for cumulative frequency tables” (PDF).