您的位置:首页 > 其它

BIT(树状数组简介)

2018-01-16 11:43 274 查看

引言

有一组数,比如说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……观察它们的二进制:

100000
200010
400100
801000
1610000
你会发现这些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).
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: