嘛~最近刚刚学会树状数组,写个blog记录一下心得。
树状数组呢,核心是一个叫lowbit的东西,lowbit(x)=x&-x=x的最后一位1的大小。
一、一个经典问题
一个初始值为0的k位计数器,要求支持n次+1操作。时间复杂度?
经典解法:
法I:考虑第i位的改变次数,可得 O(∑k−1i=0n2i)≤O(∑∞i=0n2i)=O(n) 。
法II:考虑计数器中1的数量,显然每次只会增加1个(减少若干个),所以时间复杂度 O(n) 。
法III:考虑势能函数f(T)=计数器中1的数量,则显然单次操作均摊 O(1) 。
奇怪向做法:
对于x,考虑大于x的第一个y使得y&lowbit(x)==0(lowbit(x)第一次被进位),显然y=x+lowbit(x)。所以由x向x+lowbit(x)连一条边。这样的话显然会形成一棵树,计数器操作的代价就是点数+边数等于 O(n) 。
这棵树就是树状数组啦!
二、树状数组中的一些基本关系
x的父亲是x+lowbit(x)。
x的子树是(x-lowbit(x),x],(即所有能通过若干次+lowbit到达x的节点集合)。
考虑x一直沿着它父亲走,那么lowbit一定是严格单增的,所以树高是 O(log2n) 的。
考虑x的儿子,就是能通过一次+lowbit操作到达x的元素数量,它显然等于 log2lowbit(x) ,就等于 {x−2i|2i<lowbit(x)} ,所以一个节点的儿子数量也是 O(log2n) 的。更直观的说法是,x的儿子数量其实就等于从x-1 +1(上文中的例题)时被进位的(消失的)1的数量。
三、基本的树状数组怎么写?
我们先来考虑一个简单的问题,就是求区间和。要求支持单点修改,区间询问。
那么我们对于每个节点保存它的子树的和。
修改每个节点的时候直接沿着父亲一路找上去就可以了。
void add(int x,int delta){
for(;x<=n;x+=x&-x)bit[x]+=delta;
}
查询的时候我们可以把区间和改为两个前缀和的差。而一个前缀和可以拆分成 O(log2n) 棵子树。
int query(int x){
int ans=0;
for(;x;x-=x&-x)ans+=bit[x];
return ans;
}
四、如何初始化树状数组?
比如说我们有一个数组a,我们要建出它的bit,我们该怎么做呢?
我以前的做法是把n个数插入进去。
但显然这是不必要的。
我们可以从1~n递推,假设推到i时bit[i]已经推出来了,那么显然它只需贡献给bit[i+lowbit(i)]即可。
void build(){
for(int i=1,x;i<=n;++i){
bit[i]+=a[i];
if((x=i+(i&-i))<=n)bit[x]+=bit[i];
}
}
五、维护最值?
显然,树状数组维护最值的话,只能支持两种操作:增大一个位置的数,查询前缀最值。
这看起来非常苛刻,但是其实在很多情况下,都是满足的。最常见的是bit+扫描线/dp这种的。
但是,如果时间复杂度允许是 O(log22n) ,树状数组也是可以做到维护最值的。
初始化当然不必说。
void build(){
for(int i=1,x;i<=n;++i){
bit[i]=max(bit[i],a[i]);
if((x=i+(i&-i))<=n)bit[x]=max(bit[x],bit[i]);
}
}
修改的时候,我们只需要修改x的 O(log2n) 个祖先,而每个祖先又有 O(log2n) 个儿子。
void update(int x,int A){
a[x]=A;
for(int y,i;x<=n;x+=x&-x){
bit[x]=a[x];
for(y=x-1,i=1;y&i;y-=i,i<<=1)bit[x]=max(bit[x],bit[y]);
}
}
查询[l,r]的时候我们把它分成[l,r]路径上的点和被完全覆盖的子树两部分,因为[l,r]路径上只有 O(log2n) 个点,所以被完全覆盖的子树显然只有 O(log22n) 个。
int query(int l,int r){
int ans=-0x7fffffff;
--l;
for(int x;r>l;--r){
for(;r&&r-(r&-r)>=l;r-=r&-r)ans=max(ans,bit[r]);
if(r>l)ans=max(ans,a[r]);
}
return ans;
}
当然。。这 O(log22n) 的玩意儿显然是没什么卵用的东西。仅供娱乐~
六、维护后缀?
Q:如何支持单点修改,后缀和查询?
A:= =这不跟区间查询一样么。
Q:查两边前缀和?常数太大!不开心。
A:那就把原数组反过来不就行了么。
Q:坐标什么的反来反去,很麻烦的。好烦人,不开心!
A:。。。
其实。。我们只需要把修改和询问改一下下就好了!
先上代码:
void build(){
for(int i=n;i;--i){
bit[i]+=a[i];
bit[i-(i&-i)]+=bit[i];
}
}
void add(int x,int delta){
for(;x;x-=x&-x)bit[x]+=delta;
}
int query(int x){
int ans=0;
for(;x<=n;x+=x&-x)ans+=bit[x];
return ans;
}
←_←看起来就像是写残了的树状数组。。
但为什么可以这样搞?!
我们可以将修改看成是在树上打永久化的标记,查询就是在收集标记。
但是,我们需要更高逼格的解释方法。
注意到树状数组中x的父亲是x+lowbit(x),而如果x+lowbit(x)>n,那么其实它的父亲是不存在的,就是说其实它是一棵森林。我们在build的时候为了防止数组越界,还要特判一下,好烦人!
所以我们不妨把x的父亲改为x-lowbit(x),这样就是一棵以0为根的树啦!这样的话,x的子树就是[x,min(n,x+lowbit(x)-1)]。上述代码就变得显而易见了。