树状数组+题目

本文深入解析树状数组的工作原理,包括lowbit函数的作用、单点与区间更新查询的实现,以及在经典问题如逆序数计算、区间翻转等场景的应用。通过多个实例,如POJ2155、POJ2299等,详细阐述了树状数组在不同维度上的操作技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

lowbit函数:x&-x
思想:lowbit函数找的是二进制最右边的1。计算机机器语言中,-x实际上是x的补码,补码是x取反加1。
举个例子 x = 00001100, -x = 11110011 + 1 = 11110100。通过按位取余这样就可以愉快的找到最右边的1了。思想和巧妙,看看例子应该好理解。
找到最右边的1有什么用?x+lowbit(x)可以使最后一位1向前进。还是有点晕吧。看看下面的例子就明白了。
最经典的栗子:
C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。
举个例子,有几个数可以通过lowbit产生0100呢?0001–lowbit–>0010–>lowbit–>0011–>lowbit–>0100,有三个数可以通过lowbit产生0100,加上它本身,总共维护了2^2个数的和。比如5(0101)没有数可以通过lowbit产生,所以维护它本身一个。6(0110)可以有5(0101)lowbit产生,所以维护两个。

大家有没有发现要通过lowbit产生X1XX(X代表未知数),只能通过X0XX+lowbit产生,而后面的XX可以是01,10,11,2^k怎么得出来该明白了吧!以下就是add(i,x)函数了

void add(int i,int x){ //第i个位置加上x
while(i <= n){
d[i] += x;
i += lowbit(i);
}
}
我们可以看到每个以Ci维护的只是一段区间和,那我们求前缀和只需找到对应的几个Ci即可。

比如我们要求前6(0110)个数的前缀和为C6 + C4。C6维护的是a[5]+a[6]。现在只需x -= lowbit(x)。0110-0010=0100。而0100永远也不可能lowbit到0110(上面说过了),所以0110和0100维护的区间是完全没有关系的。0100维护的是a[1]+a[2]+a[3]+a[4]。正好找到前缀和。

为什么这样可行呢?0110后面维护21个数,0100维护的是22个数。加起来正好是6 = 0010 + 0100。再比如11100 = 00100 + 01000 + 10000,而这些数都可以通过lowbit产生,通过维护的个数和,我们很容易能够验证这样的方法可行的。一下是求和函数。

int sum(int i){
int s = 0;
while(i > 0){
s += d[i];
i -= lowbit(i);
}
return s;
}
单点更新单点查询
luogu3374
代码:
#include
using namespace std;
typedef long long ll;
int const N = 500000 + 10;
int d[N],n,m;
int lowbit(int x){return x&-x;}
ll sum(int i){ //区间询问
ll sum = 0;
while(i){
sum += d[i];
i -= lowbit(i);
}
return sum;
}

void add(int i,int x){ //构造线段树,在第i个位置上加x
while(i<=n){
d[i] += x;
i += lowbit(i);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
int a; scanf("%d",&a);
add(i,a);
}
while(m–){
int flag,x,y;
scanf("%d%d%d",&flag,&x,&y);
if(flag == 1) add(x,y);
else printf("%lld\n",sum(y) - sum(x-1));
}

}
区间更新单点查询
luogu3368
题解:
利用差分的思想,新建一个数组c[i] = a[i] - a[i-1],那么a[i] = c[i] + a[i-1]。
利用迭代得:a[i] = c[i] + a[i-1] = c[i] + c[i-1] + a[i-2] = c[i] + c[i-1] + c[i-2] + c[1] + a[0]。所以树状数组维护c[i]即可
如何更新区间[x,y]。直接add(x,val),add(y+1,-val)。
可以这样理解:a[n]=c[1]+c[2]+……+c[n] = (a[1]-a[0])+(a[2]-a[1])+……+(a[n]-a[n-1]+val) = a[n]+val。只要在x处加val,区间[x,y]内的每一个数都加了val。而y+1处减val,那么y后面的又恢复正常。
代码:
#include <bits/stdc++.h>
using namespace std;
int const N = 500000 + 10;
int n,m,d[N],a[N];
int lowbit(int x){return x&-x;}
void add(int i,int x){
while(i <= n){
d[i] += x;
i += lowbit(i);
}
}
int sum(int i){
int s = 0;
while(i > 0){
s += d[i];
i -= lowbit(i);
}
return s;
}
int main(){
scanf("%d%d",&n,&m);
int flag,k,x,y;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]);
for(int i=1;i<=m;i++){
scanf("%d",&flag);
if(flag == 1){
scanf("%d%d%d",&x,&y,&k);
add(x,k); add(y+1,-k);
}else{
scanf("%d",&x);
printf("%d\n",sum(x));
}
}
return 0;
}
区间修改区间查询
Code1082
题解:

区间查询,所以维护a[1]+a[2]+……+a[n]的前缀和

a[1]+a[2]+……+a[n] = c[1]+(c[1]+c[2])+(c[1]+c[2]+c[3])+……+(c[1]+c[2]+……+c[n]) = n*(c[1]+c[2]+……+c[n])-(0c[1]+1c[2]+2*c[3]+……+(n-1)*c[n])

前面c[1]+c[2]+……+c[n]这一段的区间和可以维护,为了维护后面这一段,我们新建数组c2[i] = (i-1)*c[i]即可。

每次c[i]修改量为val,那么c2[i]=(i-1)(c[i]+val) = (i-1)*c[i]+(i-1)*val。所以修改偏移量为(i-1)*val

代码:
#include <bits/stdc++.h>
using namespace std;
int const N = 200000 + 10;
typedef long long ll;
ll c1[N],c2[N],a[N];
int n,m,k;
ll lowbit(ll x){return x&-x;}
void add(ll c[N],ll i,ll x){
while(i <= n){
c[i] += x;
i += lowbit(i);
}
}
ll getsum(ll c[N],ll i){
ll s = 0;
while(i > 0){
s += c[i];
i -= lowbit(i);
}
return s;
}
int main(){
ll x,y,v;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<=n;i++) add(c1,i,a[i]-a[i-1]), add(c2,i,(i-1)(a[i]-a[i-1]));
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d%lld%lld",&k,&x,&y);
if(k == 1) scanf("%lld",&v), add(c1,x,v), add(c1,y+1,-v), add(c2,x,v
(x-1)), add(c2,y+1,-yv);
else printf("%lld\n",(y
getsum(c1,y)-getsum(c2,y)) - ((x-1)*getsum(c1,x-1)-getsum(c2,x-1)));
}
}
loj10114
题解:
左右括号的方法。在一个区间内种树,相当于加一对括号。用树状数组维护从起始到这个位置的左右括号的数量。区间内有左括号那么一定有这一种类型的树,只有离开了对应的右括号这种树才没有了。所以为了统计区间[x,y]内的树种类,只需把y左边左括号的个数-(x-1)左边右括号的个数即可。

代码:
#include <bits/stdc++.h>
using namespace std;
int const N = 50000 + 10;
int c1[N],c2[N];
int n,m;
int lowbit(int x){return x&-x;}
void add(int c[N],int i){
while(i <= n){
c[i]++;
i += lowbit(i);
}
}
int getsum(int c[N],int i){
int s = 0;
while(i > 0){
s += c[i];
i -= lowbit(i);
}
return s;
}
int main(){
int k,x,y;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&k,&x,&y);
if(k == 1){
add(c1,x), add(c2,y); //c1记录左括号的个数,c2记录右括号的个数
}else{
printf("%d\n",getsum(c1,y) - getsum(c2,x-1));
}
}
}
POJ2155
题解:
这是一道二维树状数组模板题。二维和一维套路是一样的。看看代码就明白了。
翻转一个区间相当于区间的每个数加1,最后是0还是1就模2即可。
树状数组维护区间和,(x1,y1)(x2+1,y2+1)(x1,y2+1) (x2+1,y1)都要加1。画一个矩形框框选一下就明白了。懒得画图。
代码:
#include
#include
#include
#include
#include
using namespace std;
int const N = 1000 + 10;
int n,m,c[N][N];
int lowbit(int x){return x&-x;}
void add(int x,int y){
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=n;j+=lowbit(j))
c[i][j]++;
}
int sum(int x,int y){
int s = 0;
for(int i=x;i>0;i-=lowbit(i))
for(int j=y;j>0;j-=lowbit(j))
s += c[i][j];
return s;
}
int main(){
int T;
scanf("%d",&T);
bool first = true;;
while(T–){
if(first) first = false;
else printf("\n");
memset(c,0,sizeof©);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
char p;
scanf(" %c",&p);
if(p == ‘C’){
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
add(x1,y1);
add(x2+1,y1);
add(x1,y2+1);
add(x2+1,y2+1);
}else{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",sum(x,y)%2);
}
}
}
return 0;
}
POJ2299
题解:
这是一道树状数组求逆序数的经典题目。注意要离散化,最后要开long long。

代码
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
int const N = 500000 + 10;
int a[N],n,d[N];
ll ans;
vectorv;
int lowbit(int x){return x&-x;}
void add(int i,int x){
while(i <= n){
d[i] += x;
i += lowbit(i);
}
}
int sum(int i){
int s = 0;
while(i){
s += d[i];
i -=lowbit(i);
}
return s;
}
int main(){
while(cin>>n&&n){
v.clear();
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
v.push_back(a[i]);
}
sort(v.begin(),v.end());
v.erase(unique(v.begin(),v.end()),v.end());
memset(d,0,sizeof(d));
ans = 0;
for(int i=1;i<=n;i++){
int k = lower_bound(v.begin(),v.end(),a[i]) - v.begin();
ans += (i-1-sum(++k));
add(k,1);
}
printf("%lld\n",ans);
}
return 0;
}
POJ3067
题解:
又是一道求逆序数的题目。先排序,x升序,x相同y升序。注意要long long,数据范围不准确。

代码:
#include
#include
#include
#include
#include
using namespace std;
int const N = 1000 + 10;
typedef long long ll;
int n,m,k,d[N];
ll ans;
struct Road
{
int x,y;
bool operator < (const Road &e) const{
return e.x == x ? y < e.y : x < e.x;
}
}road[N*N];
int lowbit(int x){return x&-x;}
void add(int i,int x){
while(i <= m){
d[i] += x;
i += lowbit(i);
}
}
int sum(int i){
int s = 0;
while(i){
s += d[i];
i -= lowbit(i);
}
return s;
}
int main(){
int T;
scanf("%d",&T);
int caser = 0;
while(T–){
scanf("%d%d%d",&n,&m,&k);
for(int i=0;i<k;i++) scanf("%d%d",&road[i].x,&road[i].y);
sort(road,road+k);
memset(d,0,sizeof(d));
ans = 0;
for(int i=0;i<k;i++){
ans += (i - sum(road[i].y));
add(road[i].y,1);
}
printf(“Test case %d: %lld\n”,++caser,ans);
}
return 0;
}
POJ2352
题解:
又是一道求逆序数的题目。题目顺序都处理好了。注意向右平移一个单位,因为0号位置也有star

代码:
#include <bits/stdc++.h>
using namespace std;
int const N = 15000 + 10;
int const M = 32000 + 10;
int n,d[M],num[N];
int lowbit(int x){return x&-x;}
void add(int i,int x){
while(i <= M){
d[i] += x;
i += lowbit(i);
}
}
int sum(int i){
int s = 0;
while(i > 0){
s += d[i];
i -= lowbit(i);
}
return s;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){ //0好位置有
int x,y;
scanf("%d%d",&x,&y);
++x;
num[sum(x)]++;
add(x,1);
}
for(int i=0;i<=n-1;i++) printf("%d\n",num[i]);
return 0;
}
转自:https://blog.youkuaiyun.com/weixin_42264485/article/details/88092469

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值