树状数组
题目:BIT-1
题目信息
题目描述
给定数组 a 1 , a 2 , a 3 . . . a n a_1,a_2,a_3...a_n a1,a2,a3...an进行 q q q次操作,操作有两种:
1 i x:将 a i + x a_i+x ai+x
2 l r:求 a l + . . . + a r a_l+...+a_r al+...+ar
输入样例
3 2
1 2 3
1 2 1
2 1 3
输出样例
7
数据描述
1
≤
n
,
q
≤
1
0
6
1≤n,q≤10^6
1≤n,q≤106
∣
a
i
∣
≤
1
0
6
|a_i|≤10^6
∣ai∣≤106
对于输入:保证
1
≤
l
≤
r
≤
n
,
∣
x
∣
≤
1
0
6
1≤l≤r≤n,|x|≤10^6
1≤l≤r≤n,∣x∣≤106
题目思路
运用树状数组求出区间和(注:树状数组只能返回前缀和,还需要处理,如: c h a x u n ( r ) − c h a x u n ( l − 1 ) chaxun(r)-chaxun(l-1) chaxun(r)−chaxun(l−1))
- 输入各值,利用修改函数,创建树状数组(详见后面“知识点:树状数组”)。
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
int a;
scanf("%d",&a);
xiugai(i,a);
}
void xiugai(int idx,int num){
for(int i=idx;i<=n;i+=lowbit(i)){
c[i]+=num;
}
}
- 循环输入进行查询。
for(int i=1;i<=q;i++){
int num,l,r;
scanf("%d %d %d",&num,&l,&r);
}
- 如果是1就执行修改操作
int lowbit(int x){
return x&-x;
}
void xiugai(int idx,int num){
for(int i=idx;i<=n;i+=lowbit(i)){
c[i]+=num;
}
}
- 如果是2就执行求和操作并输出操作。
if(num==1){
xiugai(l,r);
}
else{
printf("%lld\n",chaxun(r)-chaxun(l-1));
}
long long chaxun(int r){
long long sum=0;
for(int i=r;i>0;i-=lowbit(i)){
sum+=c[i];
}
return sum;
}
AC代码
#include<iostream>
#include<cstdio>
using namespace std;
int n,q;
long long c[1000005];
int lowbit(int x){
return x&-x;
}
void xiugai(int idx,int num){
for(int i=idx;i<=n;i+=lowbit(i)){
c[i]+=num;
}
}
long long chaxun(int r){
long long sum=0;
for(int i=r;i>0;i-=lowbit(i)){
sum+=c[i];
}
return sum;
}
int main(){
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
int a;
scanf("%d",&a);
xiugai(i,a);
}
for(int i=1;i<=q;i++){
int num,l,r;
scanf("%d %d %d",&num,&l,&r);
if(num==1){
xiugai(l,r);
}
else{
printf("%lld\n",chaxun(r)-chaxun(l-1));
}
}
return 0;
}
知识点:树状数组
树状数组简介
树状数组是一种解决动态区间和的数组。
它的查询修改时间复杂度严格小于等于O(nlogn)。
它的操作用到了lowbit函数,它求的是这个数的二进制最右边的1所对应的数值。
通过观察下标二进制可知,lowbit(下标)为C[下标]可以分解成的数组元素个数,
如:C[8]=C[7]+C[6]+C[4]+A[8];
int lowbit(int x){
return x&-x;
}
树状数组结构
这是树状数组的示意图(A是原数组,C是树状数组)
我们可以得出:
C[ 1 ] = A[ 1 ]
C[ 2 ] = A[ 2 ] + C [ 1 ]
C[ 3 ] = A[ 3 ]
C[ 4 ] = A[ 4 ] + C [ 3 ] + C [ 2 ]
C[ 5 ] = A[ 5 ]
C[ 6 ] = A[ 6 ] + C [ 5 ]
C[ 7 ] = A[ 7 ]
C[ 8 ] = A[ 8 ] + C [ 7 ] + C [ 6 ] + C [ 4 ] = A[ 1 ] + A[ 2 ] + A[ 3 ] + A[ 4 ] + A[ 5 ] + A[ 6 ] + A[ 7 ] + A[ 8 ]
树状数组实现
修改函数
void xiugai(int idx,int num){
for(int i=idx;i<=n;i+=lowbit(i)){
c[i]+=num;
}
}
假设我们要给a[1]增加3
我们要修改c[1( 000 1 2 0001_2 00012)],c[2( 001 0 2 0010_2 00102)],c[4( 010 0 2 0100_2 01002)],c[8( 100 0 2 1000_2 10002)]。
通过二进制可以看出:
从1( 000 1 2 0001_2 00012)到2( 001 0 2 0010_2 00102)是加了lowbit(1)=1。
从2( 001 0 2 0010_2 00102)到4( 010 0 2 0100_2 01002)是加了lowbit(2)=2。
从4( 010 0 2 0100_2 01002)到8( 100 0 2 1000_2 10002)是加了lowbit(4)=4。
由此可得上述代码。
编写注意事项:num为a[idx]增加的值
查询函数
long long chaxun(int r){
long long sum=0;
for(int i=r;i>0;i-=lowbit(i)){
sum+=c[i];
}
return sum;
}
假设要求从头到a[7]的前缀和。
要求c[7],c[6],c[4]之和。
通过二进制可以看出:
从7( 011 1 2 0111_2 01112)到6( 011 0 2 0110_2 01102)是减了lowbit(7)=1。
从6( 011 0 2 0110_2 01102)到4( 010 0 2 0100_2 01002)是减了lowbit(6)=2。
由此可得上述代码。
编写注意事项:该函数只能查询前缀和
树状数组进阶用法
区间增值单点查询
例题题面:BIT-2
给定长度为n(
1
≤
n
≤
1
0
6
1≤n≤10^6
1≤n≤106)的数组
a
1
,
a
2
.
.
.
a
n
a_1,a_2...a_n
a1,a2...an(
1
≤
a
i
≤
1
0
6
1≤a_i≤10^6
1≤ai≤106),进行q(
1
≤
n
≤
1
0
6
1≤n≤10^6
1≤n≤106)次操作,操作有两种:
1 l r k:将
a
l
a_l
al~
a
r
a_r
ar每个数都加上 k;(
1
≤
∣
k
∣
≤
1
0
6
1≤|k|≤10^6
1≤∣k∣≤106
1
≤
l
,
r
≤
n
1≤l,r≤n
1≤l,r≤n)
2 k:输出
a
k
a_k
ak(
1
≤
k
≤
n
1≤k≤n
1≤k≤n)
实现思路
快速区间增减值,让我们想起了差分
差分是前缀和的逆运算
因为差分数组c的c[i]=a[i]-a[i-1]
所以给c[i]+100就相当于给a[i]~a[n]都加了100。
假设把区间[2,4]加10,求a[3]。
就可以把c[2]+10,c[5]-10,再利用树状数组求1~3前缀和。
AC代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e6+5;
long long n,a[N],c[N],maxx=0,q;
long long lowbit(long long x){
return x&-x;
}
void xiugai(int l,int r,int x){
for(int i=l;i<=n;i+=lowbit(i)){
c[i]+=x;
}
for(int i=r+1;i<=n;i+=lowbit(i)){
c[i]-=x;
}
}
long long chaxun(int x){
long long ans=0;
for(int i=x;i>0;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
int main(){
scanf("%d %d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
xiugai(i,i,a[i]);
}
for(int i=1;i<=q;i++){
int f;
scanf("%d",&f);
if(f==1){
int l,r,k;
scanf("%d %d %d",&l,&r,&k);
xiugai(l,r,k);
}
else{
int x;
scanf("%d",&x);
printf("%lld\n",chaxun(x));
}
}
return 0;
}
求动态区间最大值
例题题面
给定 N 个数,求这 N 个数的最长上升子序列的长度,就是求出一段不断严格上升的部分,它不一定要连续。
实现思路
直接把求和改成求最大值,因为这道题中只是用他求前面最优解,在新的长度添加前求出就不用考虑修改前值的影响。
AC代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5,M=1e6+5;
long long n,a[N],c[M],maxx=0;
long long lowbit(long long x){
return x&-x;
}
void xiugai(long long x,long long y){
for(int i=x;i<=maxx;i+=lowbit(i)){
c[i]=max(c[i],y);
}
}
long long chaxun(long long x){
long long ans=0;
for(long long i=x;i>0;i-=lowbit(i)){
ans=max(c[i],ans);
}
return ans;
}
int main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
a[i]++;
maxx=max(maxx,a[i]);
}
for(int i=1;i<=n;i++){
long long ans=chaxun(a[i]-1);
xiugai(a[i],ans+1);
}
printf("%lld",chaxun(maxx));
return 0;
}
总结
这次我主要介绍了以下内容:
- 树状数组的实现
- 树状数组结构
- 题目:BIT-1
2023.11.11
我又添加了一些树状数组的进阶用法