树状数组就是将一个大区间分为若干个小区间,小区间的共同特点:
若区间结尾为 ,则区间长度就等于 的“二进制分解”下最小的 的次幂,即 。如 所以
树状数组就是基于上述思想的数据结构,基本用途是维护序列的前缀和。
我们建立数组 保存序列 的区间 中所有数的和,即 。我们可以将 看作树形结构,该结构满足一下性质:
1. 每个内部节点 保存以它为根的子树中所有叶子节点的和。
2. 每个内部节点 的子节点个数等于 。
3. 除树根外,每个内部节点 的父节点是 。
其中
树状数组支持两个基本操作:
1. 查询前缀和
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr[i];
return res;
}
2. 单点增加
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i))
tr[i]+=c;
}
3. 操作
int lowbit(int x){
return x&-x;
}
核心代码:
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i))
tr[i]+=c;
}
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr[i];
return res;
}
初始化操作:
1. 直接建立一个全为 0 的数组
2. 对每个位置 执行
y总的图
适用范围:
1. 单点修改,区间查询和
2. 区间修改,单点查询值
3. 区间修改,区间查询和
实质:只能做到单点修改加查询
例子:
1. AcWing 241. 楼兰图腾
以 V 为例:
先正序扫描,利用树状数组求出每一个点的前面大于它的个数
再倒序扫描,利用树状数组求出每一个点的后面大于它的个数
同理
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e5+10;
int tr[N],n,a[N];
ll great[N],low[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i))
tr[i]+=c;
}
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr[i];
return res;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
int y=a[i];
great[i]=sum(n)-sum(y);
low[i]=sum(y-1);
add(y,1);
}
memset(tr,0,sizeof(tr));
ll res1=0,res2=0;
for(int i=n;i>=1;i--){
int y=a[i];
res1=res1+(ll)great[i]*(sum(n)-sum(y));
res2=res2+(ll)low[i]*(sum(y-1));
add(y,1);
}
printf("%lld %lld\n",res1,res2);
return 0;
}
2. AcWing 242. 一个简单的整数问题
区间修改+单点查询
本题指令是“区间增加”+“单点查询”。
利用树状数组巧妙地变为“单点增加”+“区间查询”。
解法:对于指令 ,我们令 加上 , 减去 ,这样利用树状数组的性质,这个操作只会影响 区间,其他的地方没有影响。
模板题
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+10;
int n,m,a[N];
ll tr[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
ll sum(int x){
ll res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr[i];
return res;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
add(i,a[i]-a[i-1]);
}
while(m--){
char op[2];
int l,r,d;
scanf("%s%d",op,&l);
if(op[0]=='C'){
scanf("%d%d",&r,&d);
add(l,d); add(r+1,-d);
}
else{
printf("%lld\n",sum(l));
}
}
return 0;
}
3. AcWing 243. 一个简单的整数问题2
区间修改+区间查询和
利用两个树状数组
下面是y总的解法
本题指令为“区间增加”+“区间查询”
我们利用树状数组先按照上一题思路对原数组进行差分,得到对应的
则序列 的前缀和 为:
所以我们建立两个树状数组 和 ,对于每条指令 执行4个操作:
1. 在树状数组 中,把位置 上的数加 。
2. 在树状数组 中,把位置 上的数减 。
3. 在树状数组 中,把位置 上的数加 。
4. 在树状数组 中,把位置 上的数减 。
对于每条指令 ,我们还是拆成 和 两个部分,二者相减。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
#define ll long long
int n,m,a[N];
ll tr1[N];// b[i]前缀和
ll tr2[N];// i*b[i]前缀和
int lowbit(int x){
return x&-x;
}
void add(ll tr[],int x,ll c){
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
ll sum(ll tr[],int x){
ll res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr[i];
return res;
}
ll presum(int x){
return sum(tr1,x)*(x+1)-sum(tr2,x);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
ll b=a[i]-a[i-1];
add(tr1,i,b);
add(tr2,i,i*b);
}
while(m--){
char op[2]; int l,r,d;
scanf("%s%d",op,&l);
if(op[0]=='C'){
scanf("%d%d",&r,&d);
add(tr1,l,d); add(tr2,l,l*d);
add(tr1,r+1,-d); add(tr2,r+1,(r+1)*-d);
}
else{
scanf("%d",&r);
printf("%lld\n",presum(r)-presum(l-1));
}
}
return 0;
}
4. AcWing 244. 谜一样的牛
核心算法:二分 树状数组
逆序开始,对每一头牛进行二分,直到找到最小的 , 即为当前牛的身高
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
#define ll long long
int n,h[N],ans[N],tr[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i))
tr[i]+=c;
}
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i))
res+=tr[i];
return res;
}
int main(){
scanf("%d",&n);
for(int i=2;i<=n;i++) scanf("%d",&h[i]);
for(int i=1;i<=n;i++) add(i,1);
for(int i=n;i>=1;i--){
int k=h[i]+1;
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if(sum(mid)>=k) r=mid;
else l=mid+1;
}
ans[i]=r;
add(r,-1);
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}