我和你徜徉在一片花海之中,你的笑颜化为一缕清风拂过我的心,莹莹目光饱含深情与温柔。
你指着前面问,“gigo,那是什么?”
我望了望,笑着回到,“那是一片树状数组哇。”
咳咳,进入正题。
树状数组,顾名思义,是长得像树一样的数组。我来上张图。
明显的百度来的对吧ovo。
下面的A数组是原来的数组ovo,而上面的C是你自己开的树状数组。(下文我叫它t)
你可以看到它的管理方式就像把线段树挤到了右边去一样。实际上树状数组和线段树基本没差,但树状数组常数小,好写。可惜的是树状数组对询问的要求比较苛刻,这个我们稍后再谈。
树状数组最爱用的一个函数叫lowbit。这个函数要自己写,代码超级超级短。
int lowbit(x){
return x&(-x);
}
我们设原来的x在二进制下,末尾的连续0有a个,则这个函数返回的值是。
有什么用呢?我们来观察一下。如果我们修改了结点1,那受其影响的结点有1,2,4,8。
然后你会惊讶的发现
1+lowbit(1)=2;
2+lowbit(2)=4;
4+lowbit(4)=8;
再举个栗子,我修改了结点3,受之影响的有3,4,8;
3+lowbit(3)=4;
4+lowbit(4)=8;
哈,也就是说每当我们修改了一个结点,也就是单点修改的时候,update就可以直接用当前序号加上lowbit(当前序号)就可以啦!
上代码。
void update(int id,int key){//id是结点号,key是本次增加的值,即a[id]=a[id]+key;
while(id<=n){
t[id]+=key;
id+=lowbit(id);
}//总结点数n
}
那查询呢?
有了之前的经验,你会更轻松地发现
8-lowbit(8)=0;
7-lowbit(7)=6;
6-lowbit(6)=4;
……
发现什么了?你会轻松地发现,任何一个数减去自己的lowbit,得到的就是自己管辖不到左边的最近的结点号。参照下图。
也就是说,查询1~x的前缀和变得不再困难,代码超级简单。
int query(int i){
int ans=0;
while(i>0){
ans+=t[i];
i-=lowbit(i);
}
return ans;
}
总结一下,树状数组包含的操作主要有update,query,lowbit。前两个操作因题而异。
下面我们说一下树状数组的三个用途。
1.单点修改,区间查询。
把上文的代码拼到一起就可以了ovo。查询l到r的区间和时,直接输出query(r)-query(l-1);(差分的思想嘛)。
2.区间修改,单点查询。
这个稍稍复杂一点。因为这里要用到差分的思想。
因为我修改一个数,会影响到它上面的。所以区间修改的时候,我是不是只需要修改一个呢?
答案当然是肯定的,要不然我就不用写博客了。
我想让l到r的所有数加上x,直接update(l,x),再update(r+1,-x);赤裸裸的差分。
查询不变,就可以啦,读者要思考哦。
3.求逆序对
这个是高中后大佬们最爱用的求逆序对方法。当然一般都要用到离散化,而蒟蒻的我又没有学并且现在懒得自学了QAQ所以下次再说。
简要提一下就是加入一个数把大于它的所有lowbit都加一:a[i+lowbit[i]]++;然后最后查的时候用总数减去查出数就是逆序对数。