【笔记】树状数组
动态维护前缀的数据结构。以下均以前缀和为例。
构造树状数组
对于长度为 \(n\) 的原始数组 \(A\),我们把元素两两合并,在此之上再把区间两两合并,直到合并到整个数组,把这些区间的和存下来构造辅助数组,即可以把每个区间拆成不超过 \(\log n\) 个区间,以达到快速修改单点和查询区间的目的。

然后我们注意到每一层的第 \(2k\) 个都是可以通过上一层 \(-\) 下一层差分出来的,也就是不必维护,因此就可以把他们删掉。删掉后还剩 \(n\) 个数,将其按区间右端点从左到右的顺序排成一排。

这就是树状数组。具有树结构关系的数组,小区间为大区间的子节点。
其性质为:
- 对于节点 \(x\),其父结点编号为 \(x+\operatorname{lowbit}(x)\);
- 对于前缀 \([1,x]\),其前缀和等于 \(tr[x]+tr[x-\operatorname{lowbit}(x)]+tr[(x-\operatorname{lowbit}(x))-\operatorname{lowbit}(x-\operatorname{lowbit}(x))]+\cdots\) 直到下标 \(=0\)(不含)。
其中 \(\operatorname{lowbit}(x)\) 定义为 \(x\) 最低位的 \(1\) 及其后面的 \(0\) 构成的二进制数。如 \(\operatorname{lowbit}((10110110)_2)=(10)_2\)。
在 C++ 中,负数以补码存储,即原码取反 \(+1\),原码中低位的 \(0\) 取反后全为 \(1\),再 \(+1\) 又全变为了 \(0\);最低位 \(1\) 取反后为 \(0\),\(+1\) 因为进位又变为了 \(1\);高位全部取反不受 \(+1\) 影响,而 \(\operatorname{lowbit}\) 与原码相同。因此 \(\operatorname{lowbit}(x)\) 在 C++ 中等于 x&(-x)。
利用这两条性质我们可以很简单地实现单点修改和查询前缀的代码。
单点修改:
void update(int x,int k){
while(x<=n){
tr[x]+=k;
x+=lowbit(x);
}
return ;
}
(单点修改为特定值:同步修改原序列 \(a_i\),然后作差。)
update(x,k-a[x]);
a[x]=k;
查询前缀:
int query(int x){
int res=0;
while(x){
res+=tr[x];
x-=lowbit(x);
}
return res;
}
查询一般区间 \([l,r]\) 需使用差分,query(r)-query(l-1)。因此普通树状数组仅能维护可差分信息的区间。
维护不可差分信息的树状数组,理论时间复杂度比线段树劣,故此处不涉及。
区间修、单点查树状数组
维护原始数据的树状数组是做不了区间修改的。如果只查询单点,则考虑更换维护对象。
注意到差分数组的前缀和是每个点的数据。在差分数组中仅需修改两个点即可做到原数组的区间加,而树状数组又正好维护前缀和。因此用树状数组维护差分数组即可。
for(int i=1;i<=n;i++) update(i,a[i]-a[i-1]);
for(int i=1;i<=m;i++){
int op,x;
cin>>op>>x;
if(op==1){
int y,k;
cin>>y>>k;
update(x,k),update(y+1,-k);
}else cout<<query(x)<<'\n';
}
区间修改、区间查询树状数组较为科技且理论时间复杂度比如线段树劣,此处不涉及。
权值树状数组
很多时候我们要用树状数组维护值域解决问题。
Luogu P1908 逆序对
逆序对问题即对于每个元素求出他前面有多少个比他大的元素,并求和。想到从前往后加元素,找到元素加入后在序列中的位置。
考虑如何动态维护元素在当前序列中的相对位置。我们可以在值域上加权值,表示此值上有几个元素,所有比其小的元素数量即为值域上的标记前缀和,就可以拿树状数组维护这个东西了。
对于值域过大的情况,将其离散化即可。
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+1+n);
int l=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++){
int x=lower_bound(b+1,b+1+l,a[i])-b;
update(x,1);
ans+=i-query(x);
}
树状数组二分
很多时候我们想知道前缀和正好等于 \(x\) 的点在哪里(保证序列非负),我们知道前缀和是单增的,可以在树状数组查询外面套一个二分查找。但这样做是 \(O(n\log^2n)\) 的。
我们也可以以单 \(\log\) 的复杂度在树状数组上二分。具体思想是从根节点往下搜,比较 子区间右端点处的前缀和 与目标,决定向哪一子节点走。
性质:对于树状数组上的节点 \(x\),其从左到右的子节点编号为 \(x\) 分别减 \(2^k\),\(k\) 从 \(x\) 的二进制位数 \(-1\) 递减到 \(0\)。
我们就可以从根节点开始,判断其子节点决定接下来往哪里走。最左侧子节点的值是左半的前缀和,右侧有若干个子节点。对于一个子节点,其 左边所有子节点的值之和 + 自身的值 就是此子节点右端点处的前缀和。若该前缀和 \(\ge x\),就往该子节点走,否则就去判根的下一个子节点。直到走到叶子。
需要注意的是,树状数组二分要从一个总的根节点开始,这个根一定要能访问到所有节点,而普通树状数组若元素个数不是 \(2^k\) 则是不连通的,我们要建到一个 \(>n\) 的 \(2\) 的次幂才能连通整个树,这个次幂一定是 \(<2n\) 的,在单点修改时判到 \(2n\) 即可。
num=log(n)/log(2)+1;
root=(1<<num);
void update(int x,int k){
while(x<=root){//或 x<=2*n
tr[x]+=k;
x+=lowbit(x);
}
return ;
}
int find(int x){
int u=root,val=0;
for(int i=num-1;i>=0;i--){
int v=u-(1<<i);
if(val+tr[v]<x) val+=tr[v];
else u=v;
}
return u;
}
Luogu P1168 中位数
从前往后加元素,权值树状数组维护,找前缀和等于当前元素个数一半的位置。需要离散化。
for(int i=1;i<=n;i++){
update(a[i],1);
if(i==1) cout<<b[a[i]]<<'\n';
else if(i&1){
int x=find((i+1)/2);
cout<<b[x]<<'\n';
}
}
树状数组应用 - 离线二维数点
二维数点(二维偏序):在二维平面上有若干个坐标点,求某个矩形范围内点的数量。
实际上,上面提到的逆序对就是一种形式的二维数点。
Luogu P10814 【模板】离线二维数点
若每次都询问整个区间,则可使用权值树状数组直接求解。
对于区间 \([1,r]\),可以发现答案为从前往后加点到 \(r\) 时值域树状数组的结果,即整个树状数组的历史版本。而区间 \([l,r]\) 就可以差分,用 \(r\) 时的结果减 \(l-1\) 时的结果。
允许离线,那么我们只需要对所有查询端点排序,从左到右边加点边求解,左端点要减去所以标记是 \(-1\),右端点要加上所以标记是 \(1\)。类似扫描线的思想。
实际上这个问题也可以抽象成二维数点。我们令 \(x\) 轴为下标,\(y\) 轴为元素值,建立平面直角坐标系。原数组的元素 \(a[i]\) 就对应坐标点 \((i,a[i])\)。

查询 \(l,r,x\) 相当于询问左下角是 \((l,0)\) 右上角是 \((r,x)\) 的矩形中有多少个点,也就可以用接到 \(y\) 轴的大矩形相减得到。

实现方法同上,权值树状数组维护即可。时间复杂度 \(O((n+m)\log m)\)。
若强制在线,则需要用到主席树等更高级的数据结构(维护历史版本)。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,m;
int a[N],maxa;
struct Node{
int id,val,vis;
};
vector<Node> q[N];
int tr[N],ans[N];
int lowbit(int x){
return x&(-x);
}
void update(int x){
while(x<=maxa){
tr[x]++;
x+=lowbit(x);
}
}
int query(int x){
int res=0;
while(x){
res+=tr[x];
x-=lowbit(x);
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
maxa=max(maxa,a[i]);
}
for(int i=1;i<=m;i++){
int l,r,x;
cin>>l>>r>>x;
maxa=max(maxa,x);
q[l-1].push_back((Node){i,x,-1});
q[r].push_back((Node){i,x,1});
}
for(int i=1;i<=n;i++){
update(a[i]);
for(int j=0;j<q[i].size();j++){
int t=query(q[i][j].val);
ans[q[i][j].id]+=t*q[i][j].vis;
}
}
for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
return 0;
}
对于更标准的离线二维数点(查询任意矩形),只需要在查询 \(l-1\) 和 \(r\) 时分别减去他们下端点到 \(x\) 轴之间的那个矩形。也可以使用二维前缀和的思想,两者是等价的。
for(int i=1;i<=lenx;i++){
while(p<=n&&e[p].a==i){
update(e[p].b);
p++;
}
for(int j=0;j<q[i].size();j++){
int t=query(q[i][j].yb)-query(q[i][j].ya-1);
ans[q[i][j].id]+=t*q[i][j].vis;
}
}
Luogu P1972 [SDOI2009] HH 的项链
容易发现原问题等价于不重复加 \(y\) 坐标相同的点的二维数点,但这就意味着一个点需要在多个包含相同元素的查询区间里出现,此时有两种处理方法。
方法 1:只保留当前最后一个
显然,我们只需保留还可能出现在查询序列的那一个点。于是我们把查询区间按右端点从小到大求解,保证当前最右侧的点都 \(\le\) 当前查询区间的右端点,更靠左的点就自然不必保留了。遇到重复元素就把之前的点删掉再加上新点即可。
struct node{
int id,l;
};
for(int i=1;i<=m;i++){
cin>>l>>r;
Q[r].push_back((node){i,l});
}
for(int i=1;i<=n;i++){
if(vis[a[i]]==0){
update(i,1);
vis[a[i]]=i;
}
else{
update(vis[a[i]],-1);
update(i,1);
vis[a[i]]=i;
}
for(int j=0;j<Q[i].size();j++){
node u=Q[i][j];
Ans[u.id]=query(i)-query(u.l-1);
}
}
方法 2:维护上一次出现位置
这也是更标准的区间数颜色的 trick。
定义 \(pre_i\):与 \(a_i\) 相同的元素上一次出现时的下标。
那么对于区间 \([l,r]\),所有不是在 \([l,r]\) 中第一个出现的元素的 \(pre_i\) 必然会指向 \(\ge l\) 的结果。反过来 \(pre_i<l\) 的元素都是 \([l,r]\) 中第一个出现的该元素,这些元素的数量就是问题的答案。即统计:
- \(i\in[l,r]\)
- \(pre_i\in[0,l)\)
的数量。
以横坐标为 \(x\),\(pre_i\) 为 \(y\) 建立平面直角坐标系,每个区间端点询问 \(y<l\) 点的数量,直接跑离线二维数点即可。
然后注意在整个序列中第一次出现的元素的 \(pre\) 全部指向 \(0\),而树状数组只能从 \(1\) 开始,要在存储和查询时全部 \(+1\)。
for(int i=1;i<=n;i++){
cin>>a[i].val;
if(!mp[a[i].val]) mp[a[i].val]=i,a[i].pre=1;
else a[i].pre=mp[a[i].val]+1,mp[a[i].val]=i;
}
for(int i=1;i<=n;i++){
update(a[i].pre);
for(int j=0;j<q[i].size();j++){
int res=query(q[i][j].l);
ans[q[i][j].id]+=res*q[i][j].vis;
}
}
树状数组应用 - 整体二分
待补充。
练习题
普通树状数组
Luogu P4868 Preprefix sum
待补充。
Luogu P5094 [USACO04OPEN] MooFest G 加强版
待补充。
权值树状数组
Luogu P2184 贪婪大陆
问题转化为询问区间 \([l,r]\) 此前被多少个区间覆盖过,考虑维护区间端点,所有左端点 \(\le r\) 且右端点 \(\ge l\) 的区间可以计算在内,用两个权值树状数组维护即可。
if(op==1){
ss1.update(l);//维护左端点
ss2.update(r);//维护右端点
}else cout<<ss1.query(r)-ss2.query(l-1)<<'\n';//左端点小于等于r的区间数量减去右端点小于l的区间数量

浙公网安备 33010602011771号