树状数组
一、简介
树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。
普通树状数组维护的信息及运算要满足 结合律 且 可差分,如加法(和)、乘法(积)、异或等。
事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小。
树状数组能快速求解信息的原因:我们总能将一段前缀
1.管辖区间
树状数组其中的一位会管辖原数组的一位或多位并维护相应的信息
那么每一位具体的管辖区间是多少?
树状数组中,规定
- 设二进制最低位为第
位,则 恰好为 二进制表示中,最低位的1
所在的二进制位数; ( 的管辖区间长度)恰好为 二进制表示中,最低位的1
以及后面所有0
组成的数。
我们记 1
以及后面的 0
组成的数为
这里注意:1
所在的位数 1
和后面所有 0
组成的
怎么计算 lowbit
?根据位运算知识,可以得到 lowbit(x) = x & -x
。‘’
1.1怎么计算 ?
首先我们要知道负数在存储的时候是用的补码,也就是反码
设原先
2.区间查询
任何一个区间查询都可以这么做:查询
我们可以写出查询
- 从
开始往前跳,有 管辖 ; - 令
,如果 说明已经跳到尽头了,终止循环;否则回到第一步。 - 将跳到的
合并。
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
3.树状数组与其树形态的性质
具体树状数组的其它性质可以参见OI-WIKI这篇文章
我们在这里就可以把树状数组的树形态理解为
4.单点修改
目标是快速正确地维护
管辖
设
- 初始令
。 - 修改
。 - 令
,如果 说明已经跳到尽头了,终止循环;否则回到第二步。
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
5.建树
也就是根据最开始给出的序列,将树状数组建出来(
一般可以直接转化为
但是也有
inline void build()
{
for(int i=1;i<=n;i++)
{
t[i]+=a[i];
int j=i+lowbit(i);
if(j<=n) t[j]+=t[i];
}
}
6.权值树状数组
6.1权值数组
首先来了解权值数组的定义,一个数组
6.2 单点修改,查询全局第 小
运用权值树状数组,我们可以解决一些经典问题。
首先这个问题还是只需要数据之间的相对大小,所以可以离散化。只不过需要注意的是,要把单点修改中涉及到的值一起离散化。
对于单点修改,可以转化为对权值数组的单点修改,即当原数组
对于查询第
考虑一个更快的做法——倍增。
设
- 查询权值数组中
的区间和 。 - 如果
,则拓展成功, , ;否则拓展失败不操作
这样得到的
为什么这样做更快?
因为这里的前缀和只需要访问
考虑
这样的话直接访问
二、题单
1. 【模板】树状数组 1
思路:
经典的单点修改,区间查询,用模板即可
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 500050
int n,m,a[N];
int t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void build()
{
for(int i=1;i<=n;i++)
{
t[i]+=a[i];
int j=i+lowbit(i);
if(j<=n) t[j]+=t[i];
}
}
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=read();
build();
for(int i=1;i<=m;i++)
{
int opt,x,y;
opt=read();x=read();y=read();
if(opt==1) add(x,y);
else cout<<query(y)-query(x-1)<<endl;
}
return 0;
}
2.【模板】树状数组 2
思路:
本题关键在于转化,想到我们已经会了单点修改区间查询,那就要往这上面转化,
考虑维护一个
于是我们发现单点查询已经搞定了(已经变成了区间和),
那单点修改呢,
我们发现如果维护一个与
那么对于
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 500050
int n,m,a[N];
int b[N],t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void build()
{
for(int i=1;i<=n;i++)
{
t[i]+=b[i];
int j=i+lowbit(i);
if(j<=n) t[j]+=t[i];
}
}
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
m=read();
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
for(int i=1;i<=n;i++)
{
a[i]=read();
b[i]=a[i]-a[i-1];
}
build();
for(int i=1;i<=m;i++)
{
int opt,x,y,k;
opt=read();x=read();
if(opt==1)
{
y=read();k=read();
add(x,k);add(y+1,-k);
}
else cout<<query(x)<<endl;
}
return 0;
}
3.简单题
思路:
诶这道题其实就是上面的模板题的小变式,
然后自己推导一下,发现异或运算满足结合律,并且可差分,
那就说明可以用树状数组维护,
又看到是区间修改,单点查询,那直接维护一个差分树状数组即可,
只不过这里的差分并不是加法的逆运算,而是异或的逆运算。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,m,a[N];
int t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x)
{
while(x<=n)
{
t[x]^=1;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=t[x];x-=lowbit(x);
while(x>0)
{
sum^=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();m=read();
memset(a,0,sizeof(a));
for(int i=1;i<=m;i++)
{
int opt,x,y;
opt=read();x=read();
if(opt==1)
{
y=read();
add(x);add(y+1);
}
else printf("%d\n",query(x));
}
return 0;
}
4.约瑟夫问题
方法一:
思路:有一个用链表做的比较好想的思路,就是开一个
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1050
int m,n;
int Next[N];
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++) Next[i]=i+1;
Next[n]=1;
int p=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<m;j++) p=Next[p];
cout<<Next[p]<<" ";
Next[p]=Next[Next[p]];
}
return 0;
}
方法二:
思路:
用权值树状数组维护,初始所有权值数组均为
然后去查全局第
每次查完
取模可以写成
取模完把这个人删掉即可。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 150
int n,m;
int t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int kth(int k)
{
int x=0,sum=0;
for(int i=log2(n);i>=0;i--)
{
if(x+(1<<i)>n||sum+t[x+(1<<i)]>=k) continue;
x+=(1<<i);
sum+=t[x];
}
return x+1;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) add(i,1);
int k=m;
for(int i=1;i<=n;i++)
{
int x=kth(k);
cout<<x<<" ";
if(i==n) break;
add(x,-1);
k=(k+m-1-1)%(n-i)+1;
}
return 0;
}
5.Sleepy Cow Sorting
思路:
首先先考虑最小操作次数,发现是从右向左第一个上升的位置(手玩一下不难发现),
然后考虑前面每一个数的操作数,
由题意可知第
我们用
每次移动队列最前面的那头牛要考虑,
前面有多少头比这头牛编号小的已经排好序了,
那么操作数就是
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,a[N];
int t[N],cnt,tot,ans[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=n;i>=1;i--)
{
add(a[i],1);cnt++;
if(a[i]<a[i-1]) break;
}
tot=n-cnt;
for(int i=1;i<=tot;i++)
{
ans[i]=n-cnt+query(a[i])-1;
add(a[i],1);
cnt++;
}
cout<<tot<<endl;
for(int i=1;i<=tot;i++) cout<<ans[i]<<" ";
return 0;
}
6.逆序对
思路:
一道权值树状数组题目。
想了好久终于想明白了,首先看到数据范围比较大,
并且我们要维护的是逆序对,所以数字实际大小其实没什么意义,只需要相对大小就可以了,那我们也可以离散化了,
离散化以后,每个数就变成了它所在数组中的排名,
以从小到大排序为例,题目中的
然后我们考虑建树,
从左向右,将树状数组中
然后我们去查询,
查询是向下查询的,即
如果
当第
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 500050
typedef long long ll;
int n,a[N];
int t[N],tmp[N],len;
ll ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),tmp[i]=a[i];
sort(tmp+1,tmp+1+n);
len=unique(tmp+1,tmp+1+n)-(tmp+1);
for(int i=1;i<=n;i++)
a[i]=lower_bound(tmp+1,tmp+len+1,a[i])-(tmp);
for(int i=1;i<=n;i++)
{
add(a[i],1);
ans+=i-query(a[i]);
}
printf("%lld",ans);
return 0;
}
实际上,逆序对求解部分还有另一种写法如下:
for(int i=n;i>=1;i--)
{
ans+=query(a[i]-1);
add(a[i],1);
}
从
如果
7.递增
思路:
正常的 dp 比较好想,设
虽然树状数组维护区间最大值是
所以我们对数据离散化以后,直接树状数组维护前缀最大值即可,
简单说说
所以
最后答案就是
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,a[N];
int len,tmp[N];
int f[N],t[N],ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]=max(t[x],k);
x+=lowbit(x);
}
}
inline int query(int x)
{
int maxx=0;
while(x>0)
{
maxx=max(maxx,t[x]);
x-=lowbit(x);
}
return maxx;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),tmp[i]=a[i];
sort(tmp+1,tmp+1+n);
len=unique(tmp+1,tmp+1+n)-tmp-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(tmp+1,tmp+1+len,a[i])-tmp;
for(int i=1;i<=n;i++)
{
f[a[i]]=query(a[i]-1)+1;
add(a[i],f[a[i]]);
}
ans=n-query(n);
cout<<ans<<endl;
return 0;
}
8.火柴排队
思路:
首先还是发现,题目中我们只需要相对高度,所以还是先对两列火柴离散化,
然后对两列火柴分别操作和只对一列火柴操作是等价的,
所以我们不妨把第一列火柴看作是“模板” ,
因为每次只能交换相邻的火柴,
所以只需要求第二列火柴在第一列火柴意义下的逆序数(自己起的名字)即可。
下面我们以样例
第一列火柴为:
第二列火柴为:
在第一列火柴意义下的逆序数就是把第一列火柴看为
那么第二列火柴就被看为
所以如果第一列火柴用
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
#define MOD 99999997
typedef long long ll;
int n,a[N],b[N];
int len1,tmp1[N],len2,tmp2[N];
int q[N],t[N];
ll ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-')f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline ll query(int x)
{
ll sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read(),tmp1[i]=a[i];
for(int i=1;i<=n;i++) b[i]=read(),tmp2[i]=b[i];
sort(tmp1+1,tmp1+1+n);sort(tmp2+1,tmp2+1+n);
len1=unique(tmp1+1,tmp1+1+n)-tmp1-1;
len2=unique(tmp2+1,tmp2+1+n)-tmp2-1;
for(int i=1;i<=n;i++)
{
a[i]=lower_bound(tmp1+1,tmp1+1+len1,a[i])-tmp1;
b[i]=lower_bound(tmp2+1,tmp2+1+len2,b[i])-tmp2;
}
for(int i=1;i<=n;i++) q[a[i]]=i;
for(int i=n;i>=1;i--)
{
ans=(ans%MOD+query(q[b[i]]-1)%MOD)%MOD;
add(q[b[i]],1);
}
printf("%lld\n",ans);
return 0;
}
9.Promotion Counting
思路:
题意简化为就树上的逆序对,还是考虑使用树状数组,
我们可以利用一个 dfs 来求解,从根开始遍历,一直到叶子节点,经过查询、更新等操作一直往父节点上传,直到求出最后的答案。
在这种情况下
所以每一次 dfs 我们先把当前节点的答案初始值设为
代码:
#include<bits/stdc++.h>
#define N 200050
using namespace std;
typedef long long ll;
int n,p[N];
int len,tmp[N],t[N],ans[N];
int head[N],Next[N],ver[N],tot=-1;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline void add(int x,int y)
{
ver[++tot]=y;
Next[tot]=head[x];
head[x]=tot;
}
inline int lowbit(int x) { return x&-x; }
inline void update(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
void dfs(int x,int fa)
{
ans[x]=-(query(n)-query(p[x])); //初始化一开始有多少比 x 强的
for(int i=head[x];~i;i=Next[i]) //如果叶子节点就不会进 for 循环
{
int y=ver[i];
if(y==fa) continue;
dfs(y,x); //添加下属会一直递归到叶子节点
}
ans[x]+=(query(n)-query(p[x])); //叶子节点或递归回来的父亲节点会执行
//递归回来的父亲节点统计一下加了下属以后比 x 强的有多少 然后累加即可
update(p[x],1); //叶子节点或父亲节点统计完以后添加到树状数组中
}
int main()
{
memset(head,-1,sizeof(head));
n=read();
for(int i=1;i<=n;i++) p[i]=read(),tmp[i]=p[i];
sort(tmp+1,tmp+1+n);
len=unique(tmp+1,tmp+1+n)-tmp-1;
for(int i=1;i<=n;i++) p[i]=lower_bound(tmp+1,tmp+1+len,p[i])-tmp;
for(int i=2;i<=n;i++)
{
int fa=read();
add(fa,i);add(i,fa);
}
dfs(1,-1);
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}
10.上帝造题的七分钟 2 / 花神游历各国
思路:
本题的难点在于如何处理开平方根的操作,
没有数据结构可以维护根号操作,线段树的懒标记也不好操作,
所以我么就考虑最简单的暴力,但是要加点小技巧,
我们发现
于是我们可以加一个并查集维护,
所以修改的时候只需要修改
代码:
#include<bits/stdc++.h>
#define N 100050
using namespace std;
typedef long long ll;
ll n,m,a[N];
ll k,l,r;
ll t[N],fa[N];
inline ll read()
{
ll x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
int Find(int x)
{
if(x==fa[x]) return x;
return fa[x]=Find(fa[x]);
}
inline ll lowbit(ll x) { return x&-x; }
inline void add(ll x,ll k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline ll query(ll x)
{
ll sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(ll i=1;i<=n;i++)
{
fa[i]=i;
a[i]=read();
add(i,a[i]);
}
fa[n+1]=n+1;
m=read();
while(m--)
{
k=read();l=read();r=read();
if(l>r) l^=r^=l^=r;
if(k) printf("%lld\n",query(r)-query(l-1));
else
{
for(ll i=l;i<=r;)
{
ll tmp=(ll)sqrt(a[i]);
add(i,tmp-a[i]);
a[i]=tmp;
if(a[i]<=1) { fa[i]=i+1;i=Find(fa[i]); }
//先令 fa[i]=i+1 这个时候并不知道 i+1 的具体情况
//然后 i 变为第一个不是 1 的数还能顺便更新 fa[i]
else { fa[i]=i;i++; } //正常 i++ 看下一位
}
}
}
return 0;
}
11.HH的项链
思路:
这道题的树状数组跟前面几道又有点不太一样,首先我们先考虑权值树状数组,但是权值和序列是两个维度(仔细体会一下?)
换句话说,我们没法维护区间权值,这也就是为什么我们没法用树状数组维护区间众数,
那我们换个思路,我们发现对于一次右端点为
所以我们可以维护某个位置上是否有数字,当一个贝壳重复出现的时候,把它原来的位置改成没有数字,把当前出现的位置改为有数字,
举个例子现在有
先查询
再查询
所以我们的思路是离线所有查询,将其按照右端点排序,用
具体细节看代码
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1000050
int n,m,a[N];
struct node{ int l,r,id; }q[N];
int pos,ans[N],vis[N],t[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1;ch=getchar(); }
while(ch>='0'&&ch<='9') { x=x*10+ch-48;ch=getchar(); }
return x*f;
}
inline bool cmp(node a,node b) { return a.r<b.r; }
inline int lowbit(int x) { return x&-x; }
inline void add(int x,int k)
{
while(x<=n)
{
t[x]+=k;
x+=lowbit(x);
}
}
inline int query(int x)
{
int sum=0;
while(x>0)
{
sum+=t[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
m=read();
for(int i=1;i<=m;i++)
{
q[i].l=read();
q[i].r=read();
q[i].id=i;//记录每个查询的编号,因为得按题目顺序输出
}
sort(q+1,q+1+m,cmp);
pos=1;
for(int i=1;i<=m;i++)
{
for(int j=pos;j<=q[i].r;j++)//从上一次处理的位置开始
{
if(vis[a[j]]) add(vis[a[j]],-1);//出现过就先把之前的删去
vis[a[j]]=j;//记录本次出现的位置
add(vis[a[j]],1);//把该位置改为 1
}
pos=q[i].r+1;
ans[q[i].id]=query(q[i].r)-query(q[i].l-1);//前缀和查询答案
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
12.上帝造题的七分钟
思路:还没做完后面补上
代码:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY