树状数组
一.概念
树状数组(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 简单题
本文来自博客园,作者:Doria_tt,转载请注明原文链接:https://www.cnblogs.com/pangtuan666/p/16521832.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现