前言
745 …
lowbit操作
对于 l o w b i t lowbit lowbit的定义为"非负整数 n n n在二进制表示下最低位的1及其后面所有的0"所构成的数值,例如:当 n = 12 n=12 n=12时, 12 = ( 1100 ) 2 12=(1100)_2 12=(1100)2,所以 l o w b i t ( 12 ) = ( 100 ) 2 = 4 lowbit(12)=(100)_2=4 lowbit(12)=(100)2=4
那么如何实现 l o w b i t lowbit lowbit操作呢?
先将n取反,此时第k为变为1,第0~k-1位变为1,再令n=n+1,此时第k为1,0 ~ k-1为0。
进行完上面的操作后,n的第 k + 1 k+1 k+1位恰好与原来相反所以 n & ( n + 1 ) n\&(~n+1) n&( n+1)仅有第k位为1。
因此: l o w b i t ( n ) = n & ( n + 1 ) = n & ( − n ) lowbit(n)=n\ \&(~n+1)=n\ \&(-n) lowbit(n)=n &( n+1)=n &(−n)
听完概念,来举个例子:
对于整数12,它的二进制表示为 12 = ( 1100 ) 2 12=(1100)_2 12=(1100)2,将它取反变为 ( 0011 ) 2 (0011)_2 (0011)2,加1变为 ( 0100 ) 2 (0100)_2 (0100)2,可以观察到此时的二进制数为 ( 0100 ) 2 (0100)_2 (0100)2,与原数 ( 1100 ) 2 (1100)_2 (1100)2相比,恰好在 ( 1100 ) 2 (1100)_2 (1100)2最低位的为1。
树状数组的概念与性质
设想一下,在对一个较大的连续线性范围进行统计时,我们可以基于上述思想将它进行拆分。
将n拆分为: n = 2 p 1 + 2 p 2 + . . . + 2 p m n=2^{p_1}+2^{p_2}+...+2^{p_m} n=2p1+2p2+...+2pm
其中 p 1 < p 2 < p 3 < . . . < p m p_1<p_2<p_3<...<p_m p1<p2<p3<...<pm,
所以我们可以将此区间分为
1. 长 度 为 p 1 的 区 间 [ 1 , 2 p 1 ] 1.长度为p_1的区间[1,2^{p_1}] 1.长度为p1的区间[1,2p1]
2. 长 度 为 p 2 的 区 间 [ 2 p 2 + 1 , 2 p 1 + 2 p 2 ] 2.长度为p_2的区间[2^{p_2}+1,2^{p_1}+2^{p_2}] 2.长度为p2的区间[2p2+1,2p1+2p2]
…………
m . 长 度 为 2 p m 的 区 间 [ 2 p 1 + 2 p 2 + . . . 2 p m − 1 + 1 , 2 p 1 + 2 p 2 + . . . + 2 p m ] m.长度为2^{p_m}的区间[2^{p_1}+2^{p_2}+...2^{p_{m-1}}+1,2^{p_1}+2^{p_2}+...+2^{p_m}] m.长度为2pm的区间[2p1+2p2+...2pm−1+1,2p1+2p2+...+2pm]
如何找到这些区间呢?
就用到我们上面讲的lowbit操作了。
好了,一个新的数据结构就产生了,树状数组便是基于上述思想的一种数据结构,基本用于维护序列的前缀和(当然也有进阶操作),给你一个序列a,建立一个数组t,其中t[x]保存序列a中的区间, [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [x−lowbit(x)+1,x]中所有数的和。
图中最下面是序列的每个叶节点,代表
a
[
1
,
n
]
a[1,n]
a[1,n]
该数据结构满足以下性质
1.数组t[x]保存以它为根的子树中所有叶节点的值的和。
2.每个节点t[x]的子节点个数为 l o w b i t ( x ) lowbit(x) lowbit(x)的位数。
3.除树根外,每个节点t[x]的父节点是 t [ x + l o w b i t ( x ) ] t[x+lowbit(x)] t[x+lowbit(x)]。
4.树的深度为 O ( l o g n ) O(log\ n) O(log n)
基本运用
树状数组基本支持两个操作:
单点修改
对于任意一个节点x,其祖先最多有 l o g n log \ n log n个,我们在修改时逐一修改祖先即可。
void add(int x,int y)
{
for(;x<=n;x+=lowbit(x)) t[x]+=y;
}
区间查询
在查询时按上面方法分为 l o g n log\ n log n个小区间
int ask(int x)
{
int ans=0;
for(;x>=1;x+=lowbit(x)) ans+=t[x];
return x;
}
若要查询区间 [ l , r ] [l,r] [l,r]所有数的和,需计算 a s k ( r ) − a s k ( l − 1 ) ask(r)-ask(l-1) ask(r)−ask(l−1)
以上两种操作复杂度皆为 O ( log n ) O(\log n) O(logn)的
扩展运用
了解了树状数组的基本运用,来看一道 变式题
简单来说就是将树状数组所擅长的"单点修改,区间查询"变为了"区间修改,单点查询"
对于这两条指令,我们仔细想一想怎样把这样的问题转化为树状数组所擅长的?
新建一个数组b,对于每次 [ l , r ] [l,r] [l,r]的加上 d d d的操作,转化为在 a [ l ] a[l] a[l]上加上 d d d,在 b [ r + 1 ] b[r+1] b[r+1]上减去 d d d
此时观察b 数组的前缀和,如图所示:
可以发现,对于数组b的前缀和,他在
[
l
,
r
]
[l,r]
[l,r]区间执行了加上
d
d
d的操作。运用这样的方法即可将此题转化为树状数组。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=500086;
int n,m,a[MAXN],b[MAXN];
int lowbit(int k)
{
return k & -k;
}
void add(int x,int k)
{
while(x<=n)
{
b[x]+=k;
x+=lowbit(x);
}
}
int sum(int x)
{
int ans=0;
while(x!=0)
{
ans+=b[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
memset(b,0,sizeof(0));
for(int i=1;i<=m;i++)
{
int o;
scanf("%d",&o);
if(o==1)
{
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
add(l,k);
add(r+1,-k);
}
else {
int j;
scanf("%d",&j);
printf("%d\n",a[j]+sum(j));
}
}
return 0;
}