Fork me on GitHub

树状数组

一.概念

  树状数组(Binary Indexed Tree(B.I.T)也称作Fenwick Tree)是一个区间查询单点修改复杂度都为log(n)的数据结构。主要用于查询任意两点之间的所有元素之和

                                                         

1.问题的提出    

有一个一维数组,长度为n.

对这个数组做两种操作:

  1.修改,对第i~j之间的某元素增加 v 

   2.求和,求 i 到 j 的和 常见做法:用for循环从i到j依次求和,时间复杂度: O(n)      

缺陷:当数据规模极大的时候,将会变得效率低下。

解决办法1前缀和

问题:有一个一维数组长度为n,求区间[L,R]的和?

具体操作原数组为A[i],再定义一个数组Prev[i],i≤n+5。    Prev[1]=A[1];    Prev[2]=A[1]+A[2];    Prev[3]=A[1]+A[2]+A[3];    ……

      sum(A[L]+A[L+1]+……+A[R-1]+A[R]) = Prev[R]-Prev[L-1] 

前缀和总结:

  优点:输入原数组A时,预处理生成Prev数组,求和时只需一步相减即可。

  缺点:若原数组元素A[i] 进行修改后,Prev[i]和Prev[i]以后的元素都得改变,那么修改的时间复杂度为O(n),我们把这个修改操作定义为对原数组元素的更新,记作update。  

解决办法2:树状数组

 问题:有一个一维数组长度为n,求区间[L,R]的和,并且可以对原数组某一元素进行修改?

生成树状数组:

lowbit :        

  lowbit(i)的意思是将 i 转化成二进制数之后,只保留最低位的1及其后面的0,截断前面的内容,然后再转成十进制数,这个数也是树状数组中i号位的子叶个数。 

举例:             

  lowbit(22)的意思是将 22 转化成二进制数之后,得到10110,保留末位的1及其后的0,并截断前面的内容,得到10,转化为十进制数为2,即lowbit(22)=2,证明C[22]的子叶数为2个。 

求lowbit方法一:  原数为i(十进制),先将原数转化成二进制之后的最后一位1替换成0,然后再用原数减去替换掉最后一位1后的数(十进制相减),答案就是lowbit(i)的结果:

参考代码如下:     

lowbit(i) {
return i - ( i & ( i – 1 ) );
}

    说明:i的二进制可以看做A1B(A是最后一个1之前的部分,B是最后一个1之后的0)

     i-1的二进制可以看做A0C(C是和B一样长的1) 

     i & (i - 1)的二进制就是A1B & A0C = A0B

    i – (i & (i - 1))的二进制就是A1B – A0B = 0…010…0 

求lowbit方法二:      原数为i(十进制),先将原数转化成二进制之后,在与原数相反数的二进制按位与,答案就是lowbit(i)的结果;   

lowbit(i) {
return   i & -i;  
}  

  例如:lowbit(22)=2  ,2的二进制原码010110,正数的补码等于它的原码010110             

  -22的二进制原码110110,负数的补码等于它的原码取反加1,为101010              

  010110  & 101010 = 000010 正数转换成原码后依然是000010              

  所以lowbit(22)=2 

void update (int k, int x) {
for (int i = k; i <= n; i += lowbit (i)) {//由上图易得,第i个元素+lowbit (i)即为它的上级元素
bit[i] += x;
}
}

 

int sum (int k) {
int ans = 0;
for (int i = k; i > 0; i -= lowbit (i)) {//累加差分(bit)数组即为原数
ans += bit[i];
}
return ans;
}

lowbit的作用

作用一:构造树状数组C[i]                 

#include<cstdio>
int A[10]={0,1,2,3,4,5,6,7,8},C[10];
int lowbit(int x)
{
return x & -x;
}
int main()
{
for(int i = 1; i <= 8; i ++)
for(int j = i - lowbit(i) + 1; j <= i; j ++)
C[i] += A[j];
for(int i = 1; i <= 8; i ++)
printf("%d ",C[i]);
return 0;
}

 

 

作用二:对原数组A[i]进行更新(update)操作 

void update(int k,int x) // A[k]+x 操作
{
for(int i = k; i <= n; i += lowbit(i))
C[i] += x;
}

作用二延伸:update操作也可以对树状数组C[i]进行初始化 

#include<cstdio>
int A[10], C[10]; //定义全局数组
int lowbit( int x ) //求lowbit
{
return x & -x;
}
void update(int k , int x) //更新C[i]
{
for(int i = k; i <= 8; i += lowbit(i))
C[i] += x;
}
int main()
{
for(int i = 1; i <= 8; i ++) //输入时预处理,构造C[i]
{
scanf("%d", &A[i]);
update( i, A[i]);
}
for(int i = 1; i <= 8; i ++) //输出C[i]
printf("%d ", C[i]);
return 0;
}

作用三:求前缀和(Sum)操作     (PS:此区间为前缀和,也就是1~i)        

int Sum(int k)
{
for(int i = k; i > 0; i -= lowbit(i) )
Prev[k] += C[i];
return Prev[k];
}

作用三延伸:Sum操作可以预处理,求到前缀和Prev[i] 

                                               

作用四: Sum操作可以预处理,求到前缀和Prev[i],利用Prev[i]求区间和 

  如果要求区间[L,R]的和,利用Prev数组完成:Prev[R] – Prev[L-1]

                                       

二.用树状数组求单点修改、区间查询 

问题:

已知一个数列,你需要进行下面两种操作:

   1、将数列中某一个元素加上x;

   2、求出某区间每一个数的和。 

Code

 #include<bits/stdc++.h>
using namespace std;
long long n,q;
long long op,x,y;
long long a[1000010];
long long c[1000010];
long long lowbit(long long x)
{
return x&-x;
}
void update(long long x,long long k)
{
for(;x<=n;x+=lowbit(x))
c[x]+=k;
}
long long Sum(long long x)
{
long long res=0;
for(;x>=1;x-=lowbit(x))
res+=c[x];
return res;
}
int main()
{
scanf("%lld%lld",&n,&q);
for(long long i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
update(i,a[i]);
}
for(long long i=1;i<=q;i++)
{
scanf("%lld%lld%lld",&op,&x,&y);
if(op==1)
update(x,y);
else
printf("%lld\n",Sum(y)-Sum(x-1));
}
return 0;
}

三.用树状数组求区间修改、单点查询 

问题:

已知一个数列,你需要进行下面两种操作:

  1、将数列中某个区间的每一个元素加上x;

   2、求出数列中某一个元素的值。 

解决:

   对原数组A[]建一个差分数组P[i]=A[i]-A[i-1]( 差分数组和前缀和数组的互逆关系 ),那么A[i]=P[1]+P[2]+……+P[i] 也就是将差分数组P[]作为原数组,建立BIT,那么单点查询就是Sum了,区间修改就是Update(left, x)和Update(right+1, -x),这样修改后,BIT求前缀和Sum就是区间修改后的单点查询了。 

Code

 #include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1e6 + 5;
long long bit[MAXN];
int a[MAXN];
int n, m;
int Lowbit (int x) {
return x & (-x);
}
void Update (int x, int num) {
for (int i = x; i <= n; i += Lowbit(i)) {
bit[i] += num;
}
}
long long Sum (int x) {
long long sum_ = 0;
for (int i = x; i >= 1; i -= Lowbit(i)) {
sum_ += bit[i];
}
return sum_;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
Update(i, a[i] - a[i - 1]);
}
for (int i = 1; i <= m; i++) {
int check, l, r, num, index;
scanf("%d", &check);
if (check == 1) {
scanf("%d %d %d", &l, &r, &num);
Update(l, num);
Update(r + 1, -num);
}
else {
scanf("%d", &index);
printf("%lld\n", Sum(index));
}
}
return 0;
}

四.用树状数组求区间修改、区间查询 

问题:

已知一个数列,你需要进行下面两种操作:

   1、将数列中某一个区间的所有元素加上x;

   2、求出某区间每一个元素的和。 

解决:

   P[]仍为A[]的差分数组,那么原数组的前缀和

  A[1]+A[2]+……+ A[n]

=P[1]+(P[1]+P[2])+(P[1]+P[2]+P[3])+……+(P[1]+P[2]+……+P[n])

=n*P[1]+(n-1)*P[2]+(n-2)*P[3]+……+P[n]

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

  观察减式两边,分别将P[i]和(i-1)p[i]建立两个树状数组BIT1和BIT2,BIT1就是差分数组,区间修改按上一例进行;BIT2的增量就不是x了,而是x*(i-1)。至于区间查询,我们已经知道原数组前缀和了,直接相减即可查询区间和。 

  差分数组的前缀和为原数组,前缀和的差分为原数组

Code

 #include<bits/stdc++.h>
using namespace std;
long long n,q,w,op,x,y;
long long a[1000010],sum1[1000010],sum2[1000010];
long long lowbit(long long x)
{
return x&-x;
}
void update(long long x,long long w){
for (long long i=x;i<=n;i+=lowbit(i)){
sum1[i]+=w;
sum2[i]+=w*(x-1);
}
}
long long Sum(long long x){
long long ans=0;
for (long long i=x;i>=1;i-=lowbit(i)){
ans+=x*sum1[i]-sum2[i];
}
return ans;
}
int main()
{
scanf("%lld%lld",&n,&q);
for(long long i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
update(i,a[i]-a[i-1]);
}
for(long long i=1;i<=q;i++)
{
scanf("%lld%lld%lld",&op,&x,&y);
if(op==1){
scanf("%lld",&w);
update(x,w);
update(y+1,-w);
}
else
printf("%lld\n",Sum(y)-Sum(x-1));
}
return 0;
}

五.用树状数组求逆序对总数

★拓展知识:离散化 

  离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。

有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对位置有关,而与具体是多少无关时,可以进行离散化。比如当数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化成更小的值,能够实现更多的算法。

例如:

                           

离散化常见的两种方式:

1、数组离散化

for(int i = 1; i <= n; i ++)
{
cin >> a[i].val;
a[i].id = i;
}
sort(a + 1, a + n + 1); //定义结构体时按val从小到大重载
for(int i = 1; i <= n; i ++)
b[a[i].id] = i; //将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值

    

2、用STL+二分离散化 

#include<algorithm> // 需要头文件
//n原数组大小 num原数组中的元素 lsh离散化的数组 cnt离散化后的数组大小
int lsh[MAXN] , cnt , num[MAXN] , n;
for(int i=1; i<=n; i++)
{
scanf("%d",&num[i]);
lsh[i] = num[i]; //复制一份原数组
}
sort(lsh+1 , lsh+n+1); //排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt = unique(lsh+1 , lsh+n+1) - lsh - 1; //unique返回去重之后最后一位后一位地址 - 数组首地址 - 1
for(int i=1; i<=n; i++)
num[i] = lower_bound(lsh+1 , lsh+cnt+1 , num[i]) - lsh;
//lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址 - 数组首地址 ,从而实现离散化

树状数组求逆序对    

  逆序对的概念就不说了,实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。 

  离散化就是另开一个数组,d, d[i]用来存放第i大的数在原序列的什么位置,比如原序列a={5,3,4,2,1},第一大就是5,他在a中的位是1,所以d[1]=1,同理d[2]=3,········所以d数组为{1,3,2,4,5},

  转换之后,空间复杂度就没这么高了,但不是求d中的逆序对了,而是求d中的正序对,来看一下怎么求的:

首先把1放到树状数组t中,此时t只有一个数1,t中比1小的数没有,sum+=0
再把3放到树状数组t中,此时t只有两个数1,3,比3小的数只有一个,sum+=1
把2放到树状数组t中,此时t只有两个数1,2,3,比2小的数只有一个,sum+=1
把4放到树状数组t中,此时t只有两个数1,2,3,4,比4小的数有三个,sum+=3
把5放到树状数组t中,此时t只有两个数1,2,3,4,5,比5小的数有四个,sum+=4
最后算出来,总共有9个逆序对,可以手算一下原序列a,也是9个逆序对,
决定这个数有多少个逆序对的因素只有它前面的数,而它前面的数比他先放,比它小的在前,大的在后。在自己之前出现,说明这个数在自己前面,求前缀和sum(n),算在自己前面比自己小的数加上自己,用总的个数减去这个数,就是在自己前面比自己大的数。

#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int tree[N],Rank[N],n; //注:rank是C++的保留字,这里用Rank
#define lowbit(x) ((x) & - (x))
void update(int x, int d) {
while(x <= N) {
tree[x] += d;
x += lowbit(x);
}
}
int sum(int x) {
int ans = 0;
while(x > 0){
ans += tree[x];
x -= lowbit(x);
}
return ans;
}
struct point{ int num,val;} a[N];
bool cmp(point x,point y){
if(x.val == y.val) return x.num < y.num;
return x.val < y.val;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) {
scanf("%d",&a[i].val);
a[i].num = i; //记录顺序,用于离散化
}
sort(a+1,a+1+n,cmp); //排序
for(int i=1;i<=n;i++) //离散化,得到新的数字序列rank[]
Rank[a[i].num]=i;
long long ans=0;
for(int i=n;i>0;--i){ //倒序处理
update(Rank[i],1);
ans += sum(Rank[i]-1);
}
printf("%lld",ans);
return 0;
}

Show time

No.1 简单题

 

posted @   Doria_tt  阅读(90)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示