树状数组学习笔记
2022/9/2 23:25:10
本文主要是介绍树状数组学习笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一:树状数组定义
望文生义,树状数组就是用树形结构来模拟数组的一种数据结构。
二:图解(纯手绘,难看勿喷)
编辑
C表示从1-k的和,
C[1]=a[1]
C[2]=C[1]+a[2]
C[3]=a[3]
C[4]=C[2]+C[3]+a[4]
C[5]=a[5]
C[6]=C[5]+a[6]
C[7]=a[7]
C[8]=C[4]+C[6]+C[7]+a[8]
C[9]=a[9]
C[10]=C[9]+a[10]
C[11]=a[11]
C[12]=C[10]+C[11]+a[12]
C[13]=a[13]
C[14]=C[13]+a[12]
C[15]=a[15]
C[16]=C[8]+C[12]+C[14]+C[15]+a[16]
三:可解决问题
单点修改,区间求和。
四:为何建立树状数组
我们可以发现树状数组中,一个数直接对应的数最多有logk+1个数,因此我们在求解一个区间和时,可以在O(log n) 内求解出。
五:与线段树的区别
树状数组能解决的问题线段树都能解决,线段树能解决的问题树状数组不一定能解决,但是!!!树状数组更快!!!树状数组更好调试!!!毕竟,能简单点谁不想简单呢。
六:如何查询树状数组子节点
根据上述建树过程我们可以发现,当前点所存的范围是(x-lowbit(x)+1,x)。
那么怎么实现lowbit()呢?
这需要用到二进制了。
计算机中有源码,反码和补码。
源码就是数据本来对应的二进制数;
正数的反码等于它本身。
负数的反码等于在其源码的基础上,符号位不变,其余位取反。
正数的补码等于它本身。
负数的补码等于其反码加一。
例如14=1110
反码:1110
-14的补码:1111
那么他的最低一位1可以油 14&-14。
我们也可以这样想,只要x不为0,那么x必有一位是1,取得他的负数后,最后那些是0的位数都会变为1,再加上1,则会将1都变为0,现在出现的最低一位1就是所求的最低的1,即lowbit()。
由于我们在树状数组中需要经常用到lowbit,因此我们可以将其写为一个函数,以便使用。
int lowbit(x) { return x & -x }
七:如何修改树状数组的值
对于上面的建树过程分析,我们可以发现当前这个数x会涵盖(x-lowbit(x)+1,x)内的元素,因此不难知道,对于每一个x的修改,都会影响到x+lowbit(x)这个数,直到影响到自己设定的最大值。
可以写出add函数
void add(int x,int k) { for(int i=x;i<=maxx;i+=lowbit(x)) tr[i]+=k; }
八:如何求和
继续分析建树过程,我们会发现x不会涵盖x-lowbit(x)的内容,所以我们求和时只需要加上
x-lowbit(x)所涵盖的元素即可。
void sum(int x) { int sum=0; for(int i=x;i;i-=lowbit(i)) sum+=tr[i]; return sum; }
九:模板题目:洛谷:P3374 【模板】树状数组 1
1 #include<iostream> 2 3 using namespace std; 4 const int N=5e5+10; 5 int tr[N],n,m; 6 7 int lowbit(int x) 8 { 9 return x & -x; 10 } 11 12 void add(int x,int k) 13 { 14 for(;x<=n;x+=lowbit(x)) tr[x]+=k; 15 } 16 17 int sum(int x) 18 { 19 int sum=0; 20 for(;x;x-=lowbit(x)) sum+=tr[x]; 21 return sum; 22 } 23 24 int main() 25 { 26 ios::sync_with_stdio(false);//树状数组题目中数据范围一般都比较大,最好用scanf或者cin加速读入 27 cin.tie(0); 28 29 cin>>n>>m; 30 31 for(int i=1;i<=n;i++) 32 { 33 int x; 34 cin>>x; 35 add(i,x);//在第i个位置加上x 36 } 37 38 while(m--) 39 { 40 int op; 41 cin>>op; 42 int x,y; 43 cin>>x>>y; 44 45 if(op==1) add(x,y); 46 else cout<<sum(y)-sum(x-1)<<endl;//求出1到y和1到x-1的和,相减即为x-y的和 47 } 48 }
模板题,就不需要再做过多解释了吧。
模板题二:洛谷:【模板】树状数组 2
有些小伙伴可能看见这题,就会问了,树状数组不是只能做单点修改吗?
是的,但是我们可以用差分做这题啊!
想一想,1-n的差分加起来不就是n这个数么?有没有恍然大悟的感觉?
我们只需要在第x个位置上加上数据k,再在第y+1个位置上减去k不就能实现求任何一个位置的数了么?
1 #include<iostream> 2 3 using namespace std; 4 const int N=5e5+10; 5 int tr[N],n,m; 6 7 int lowbit(int x) 8 { 9 return x & -x; 10 } 11 12 void add(int x,int k) 13 { 14 for(;x<=n;x+=lowbit(x)) tr[x]+=k; 15 } 16 17 int sum(int x) 18 { 19 int sum=0; 20 for(;x;x-=lowbit(x)) sum+=tr[x]; 21 return sum; 22 } 23 24 int main() 25 { 26 ios::sync_with_stdio(false); 27 cin.tie(0); 28 29 cin>>n>>m; 30 31 int last=0; 32 for(int i=1;i<=n;i++) 33 { 34 int x; 35 cin>>x; 36 add(i,x-last); 37 last=x; 38 } 39 40 while(m--) 41 { 42 int op; 43 cin>>op; 44 45 if(op==1) 46 { 47 int x,y,k; 48 cin>>x>>y>>k; 49 50 add(x,k),add(y+1,-k); 51 } 52 53 else 54 { 55 int x; 56 cin>>x; 57 cout<<sum(x)<<endl; 58 } 59 } 60 }
十:拓展内容:
1:求逆序对:洛谷:P1908 逆序对
此题可以用树状数组边插入边求解。
何谓逆序对?就是i<j时,第i个数大于第j个数此类的。
我们可以用树状数组,在这个数的数值的位置加上一代表这个数被加入到了树状数组。(是数值,不是序号!!!)
那么怎么求有多少个逆序对呢?
我们可以用sum(x)求出在x之前有多少个数,可以边add()边求逆序对,因此第i个数逆序对的数量就等于i-sum(a[i]).
总体逆序对的数量只需要对其求和即可。
但是有一个问题,如果数据过大咋办,我们是不能开这么大的数组的。
这时候就需要用到离散化了。(可以选择的离散方式有很多,这里选择哈希离散)
1 #include<iostream> 2 #include<unordered_map> 3 #include<algorithm> 4 5 using namespace std; 6 const int N=5e5+10; 7 int tr[N],n,a[N]; 8 9 inline int lowbit(int x) 10 { 11 return x& -x; 12 } 13 14 inline void add(int x,int k) 15 { 16 for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=k; 17 } 18 19 inline int sum(int x) 20 { 21 int sum=0; 22 for(int i=x;i;i-=lowbit(i)) sum+=tr[i]; 23 return sum; 24 } 25 26 unordered_map<int,int> s; 27 int l=0; 28 inline int get(int x) 29 { 30 if(!s.count(x)) s[x]=++l; 31 return s[x]; 32 } 33 34 int b[N]; 35 inline void pai() 36 { 37 copy(begin(a),end(a),begin(b)); 38 sort(b+1,b+1+n); 39 for(int i=1;i<=n;i++) get(b[i]); 40 41 } 42 43 44 int main() 45 { 46 47 scanf("%d",&n); 48 49 long long ans=0; 50 51 for(int i=1;i<=n;i++) scanf("%d",&a[i]); 52 53 pai(); 54 55 for(int i=1;i<=n;i++) 56 { 57 int id=get(a[i]); 58 ans=ans+i-1-sum(id); 59 add(id,1); 60 } 61 62 cout<<ans<<endl; 63 return 0; 64 }
注:由于笔者比较笨,又使用了一次copy函数,导致此题慢了很多。
2 .求树状数组中第k大数
1.二分法:
int find(int x) { int l=0,r=maxx+1; while(l<r) { int mid=l+r>>1; if(sum(mid)<x) l=mid-1; else r=mid+1; } return r; }
2.二进制法:
int find(int x) { int ans=0,cnt=0; for(int i=20 ;i>=0;i--)//可以随意设置需要的大小,表示从二进制第i为开始 { ans+=(1<<i); if(ans>=maxx||cnt+tr[ans]>=x) ans-=(1<<i);//目前的数大于最大值或者已经求出来的个数+当前值对应的数的个数>=自己要求的数则返回 else cnt+=tr[ans]; } return ans+1;//求出的是第x-1大的数,则ans+1为第x大的数。 }
(学习自acwing和网上大佬)
笔者CSDN:https://blog.csdn.net/qq_69908563/article/details/126669563
洛谷blog:https://www.luogu.com.cn/blog/buqiming/#
这篇关于树状数组学习笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南