关于树状数组

树状数组,即 Binary Indexed Tree ,主要用于维护查询前缀和

属于 log 型数据结构

和线段树比较

都是 log 级别

树状数组常数、耗费的空间、代码量都比线段树小

树状数组无法完成复杂的区间操作,功能有限

树状数组介绍

二叉树大家一定不陌生



然而真实的树状数组省去了一些空间



其中黑色的是原数组,红色的是树状数组

根据图可以看出 S[] 的由来

S[1] = A[1]

S[2] = A[1] + A[2]

S[3] = A[3]

S[4] = A[1] + A[2] + A[3] + A[4]

按照上面的规律:

S[5] = A[5]

S[6] = A[5] + A[6]

······

可以发现:这颗树是有规律的

S[i] = A[i-2k+1] + A[i-2k+2] + ··· + A[i]

其中 k 是 i 在 2 进制下末尾连续 0 的个数

比如 i=4D=100B 则 k=2

那如何求和呢,如要求位置 6 的和,就应该是 S[6]+S[4]

根据上式可以算出每个位置的前缀和 V[i]=S[i]+S[i-2k1]+S[(i-2k1)-2k2]+ ···

新的问题来了: 2k 怎么求?

有两种方法: i&(i^(i-1)) 和 i&-i ,他们统一叫做 lowbit

lowbit 原理

lowbit 相当于求二进制从末尾到第一个 1 这一段

如 lowbit(1010B)=10B

方法 1

i-1 就是 i 在二进制中从末尾到末尾第一个 1 全部取反

如 20D=10100B 19D=10011B

把它们位异或一下,使得末尾有若干个 1 ,并去掉了前面相同的部分,如 10100B^10011B=00111B

再与原数位与一下,由于除了原数末尾第一个 1 以外都不同,所以其余都是 0

如 10100B&00111B=100B ,就是 lowbit 了

方法 2

然而现实中用的更多还是这个也许这个好记

-x 即为 x 的反码加一

而反码在加一时由于取反了,后面有一段都是 1 ,所以就会一直进位直到遇到 0 并使其变成 1

由于取反了,只有那一位 1 是相同的,这样只要位与一下,只留下那个 1 就行了

树状数组的操作

既然 get 到了精髓,后面的操作也简单了许多

约定

变量名 意义
n 原数组长度
t[] 树状数组

单点修改

上面说了 S[i] = A[i-2k+1] + A[i-2k+2] + ··· + A[i]

那既然 A[i] 修改了, S[i+2k] 、 S[i+2k+2k] ··· 都被修改了

  1. inline void add(int p,int v){
  2. for(;p<=n;p+=p&-p)
  3. t[p]+=v;
  4. }

单查

前面也给出公式了,直接循环

  1. inline int sum(int p){
  2. register int ans=0;
  3. for(;p;p-=p&-p)
  4. ans+=t[p];
  5. return ans;
  6. }

区查

有了前缀和自然可以求区间和

直接返回sum(r)-sum(l-1)

例题

单修 + 区查

洛谷 P3374

前面已经讲过了

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. inline char nc(){
  4. static char buf[100000],*S=buf,*T=buf;
  5. return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
  6. }
  7. inline int read(){
  8. static char c=nc();register int f=1,x=0;
  9. for(;c>'9'||c<'0';c=nc()) c==45?f=-1:1;
  10. for(;c>'/'&&c<':';c=nc()) x=(x<<3)+(x<<1)+(c^48);
  11. return x*f;
  12. }
  13. char fwt[100000],*ohed=fwt;
  14. const char *otal=ohed+100000;
  15. inline void pc(char ch){
  16. if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
  17. *ohed++=ch;
  18. }
  19. inline void write(int x){
  20. if(x<0) pc('-'),x=-x;
  21. if(x>9) write(x/10);
  22. pc(x%10+'0');
  23. }
  24. int n,m,opt,x,y,t[500002];
  25. inline void add(int p,int v){
  26. for(;p<=n;p+=p&-p)
  27. t[p]+=v;
  28. }
  29. inline int sum(int p){
  30. register int ans=0;
  31. for(;p;p-=p&-p)
  32. ans+=t[p];
  33. return ans;
  34. }
  35. int main(){
  36. n=read(),m=read();
  37. for(register int i=1;i<=n;i++){
  38. x=read();
  39. add(i,x);
  40. }
  41. while(m--){
  42. opt=read(),x=read(),y=read();
  43. if(opt==1) add(x,y);
  44. else write(sum(y)-sum(x-1)),pc('\n');
  45. }
  46. fwrite(fwt,1,ohed-fwt,stdout);
  47. }

区改 + 单查

虽然看上去没大变化,但是如果按照之前的思路,复杂度为 \(O(mn\ log\ n)\) ,比普通数组还差

所以需要运用差分的思想,设 d[i] 为 a[i] 的差分数组,且 d[i]=a[i]-a[i-1]

那么 \(a_i = \sum\limits_{j=1}^i d_j\)

因为是单点查询,所以我们考虑直接维护 d 这个数组的前缀和

怎么区间修改?运用差分思想,可以先从 l 开始加上那个值,再从 r 开始减去那个值,最后求和时就相当于区间修改了

洛谷 P3368

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. inline char gc(){
  4. static char buf[100000],*S=buf,*T=buf;
  5. return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
  6. }
  7. inline int read(){
  8. static char c=gc();register int f=1,x=0;
  9. for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
  10. for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
  11. return x*f;
  12. }
  13. char fwt[100000],*ohed=fwt;
  14. const char *otal=ohed+100000;
  15. inline void pc(char ch){
  16. if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
  17. *ohed++=ch;
  18. }
  19. inline void write(int x){
  20. if(x<0) x=-x,pc('-');
  21. if(x>9) write(x/10);
  22. pc(x%10+'0');
  23. }
  24. int n,m,opt,x,y,k,lst,t[500002];
  25. inline void add(int p,int v){
  26. for(;p<=n;p+=p&-p)
  27. t[p]+=v;
  28. }
  29. inline int sum(int p){
  30. register int ans=0;
  31. for(;p;p-=p&-p)
  32. ans+=t[p];
  33. return ans;
  34. }
  35. int main(){
  36. n=read(),m=read();
  37. for(register int i=1;i<=n;i++){
  38. x=read();
  39. add(i,x-lst);
  40. lst=x;
  41. }
  42. while(m--){
  43. opt=read(),x=read();
  44. if(opt==1){
  45. y=read(),k=read();
  46. add(x,k),add(y+1,-k);
  47. }
  48. else write(sum(x)),pc('\n');
  49. }
  50. fwrite(fwt,1,ohed-fwt,stdout);
  51. }

区改 + 区查

还是运用差分思想,但是如何在差分数组中求前缀和呢?

已知 \(sum_i = \sum\limits_{j=1}^i a_j\)

把 a[j] 换成差分数组,得到 \(sum_i = \sum\limits_{j=1}^i \sum\limits_{k=1}^j d_k\)

可以看出每个元素出现的次数是递减的,变换一下,得 \(i*(d_1+d_2+d_3+···)-(0*d_1+1*d_2+2*d_3+···)\)

写成求和公式: \((i*\sum\limits_{j=1}^i d_j)-(\sum\limits_{j=1}^i (j-1)*d_j)\)

这时我们发现:后面那一部分可以用树状数组存下来,快速求和

洛谷 P3372

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. typedef long long ll;
  4. inline char gc(){
  5. static char buf[100000],*S=buf,*T=buf;
  6. return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
  7. }
  8. inline ll read(){
  9. static char c=gc();register ll f=1,x=0;
  10. for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
  11. for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
  12. return x*f;
  13. }
  14. char fwt[100000],*ohed=fwt;
  15. const char *otal=ohed+100000;
  16. inline void pc(char ch){
  17. if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
  18. *ohed++=ch;
  19. }
  20. inline void write(ll x){
  21. if(x<0) x=-x,pc('-');
  22. if(x>9) write(x/10);
  23. pc(x%10+'0');
  24. }
  25. ll x,y,ls,rs,tmp,t1[100005],t2[100005];
  26. int n,m,opt,lst,k;
  27. inline void add(int p,int v,ll t[]){
  28. for(;p<=n;p+=p&-p)
  29. t[p]+=v;
  30. }
  31. inline ll sum(int p,ll t[]){
  32. ll ans=0;
  33. for(;p;p-=p&-p)
  34. ans+=t[p];
  35. return ans;
  36. }
  37. int main(){
  38. n=read(),m=read();
  39. for(register int i=1;i<=n;i++){
  40. x=read(),tmp=x-lst;
  41. add(i,tmp,t1);
  42. add(i,tmp*(i-1),t2);
  43. lst=x;
  44. }
  45. while(m--){
  46. opt=read(),x=read(),y=read();
  47. if(opt==1){
  48. k=read();
  49. add(x,k,t1);
  50. add(x,k*(x-1),t2);
  51. add(y+1,-k,t1);
  52. add(y+1,-k*y,t2);
  53. }
  54. else{
  55. rs=y*sum(y,t1)-sum(y,t2);
  56. ls=(x-1)*sum(x-1,t1)-sum(x-1,t2);
  57. write(rs-ls),pc('\n');
  58. }
  59. }
  60. fwrite(fwt,1,ohed-fwt,stdout);
  61. }

高级操作

代替平衡树

你没看错,树状数组可以代替平衡树

这时我们的 t[] 维护的就是数字的个数。

sum 便是 x 的排名

加入 x 直接add(x,1);,删除 x 直接add(x,-1);

求出第 k 大

可以直接二分

  1. int kth(int k){
  2. int l=1,r=n,mid;
  3. while(l<r){
  4. mid=(l+r)/2;
  5. if(sum(mid)<k) l=mid+1;
  6. else r=mid;
  7. }
  8. return r;
  9. }

或者通过倍增进行差分(比二分快许多)

  1. int kth(int k){
  2. int ans=0;
  3. for(int i=30;i>=0;i--){
  4. ans+=(1<<i);
  5. if(ans>n||t[ans]>=k)ans-=(1<<i);
  6. else k-=t[ans];
  7. }
  8. return ++ans;
  9. }
前驱

就是排名为 x-1 (小一点点)的数

kth(sum(x-1))

后继

就是排名比 x 大一的数

kth(sum(x)+1)

例题

洛谷 P3369

要注意一点:输入有负数,每输入一个就把它加上 107 ,输出时减去 107 ,那么 n 就应该是 2*107

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. inline char gc(){
  4. static char buf[100000],*S=buf,*T=buf;
  5. return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
  6. }
  7. inline int read(){
  8. static char c=gc();register int f=1,x=0;
  9. for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
  10. for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
  11. return x*f;
  12. }
  13. char fwt[100000],*ohed=fwt;
  14. const char *otal=ohed+100000;
  15. inline void pc(char ch){
  16. if(ohed==otal) fwrite(fwt,1,100000,stdout),ohed=fwt;
  17. *ohed++=ch;
  18. }
  19. inline void write(int x){
  20. if(x<0) x=-x,pc('-');
  21. if(x>9) write(x/10);
  22. pc(x%10+'0');
  23. }
  24. int T,opt,x,t[20000001],sz,n;
  25. void add(int p,int v){
  26. for(;p<=n;p+=(p&-p))
  27. t[p]+=v;
  28. }
  29. int sum(int p){
  30. int ans=0;
  31. for(;p;p-=(p&-p))
  32. ans+=t[p];
  33. return ans;
  34. }
  35. int kth(int k){
  36. int ans=0;
  37. for(int i=30;i>=0;i--){
  38. ans+=(1<<i);
  39. if(ans>n||t[ans]>=k) ans-=(1<<i);
  40. else k-=t[ans];
  41. }
  42. return ++ans;
  43. }
  44. //int kth(int k){
  45. // int l=1,r=n,mid;
  46. // while(l<r){
  47. // mid=(l+r)/2;
  48. // if(sum(mid)<k) l=mid+1;
  49. // else r=mid;
  50. // }
  51. // return r;
  52. //}
  53. int main(){
  54. n=20000000;
  55. T=read();
  56. while(T--){
  57. opt=read(),x=read();
  58. if(opt!=4) x+=10000000;
  59. if(opt==1) add(x,1);
  60. else if(opt==2) add(x,-1);
  61. else if(opt==3) write(sum(x-1)+1),pc('\n');
  62. else if(opt==4) write(kth(x)-10000000),pc('\n');
  63. else if(opt==5) write(kth(sum(x-1))-10000000),pc('\n');
  64. else write(kth(sum(x)+1)-10000000),pc('\n');
  65. }
  66. fwrite(fwt,1,ohed-fwt,stdout);
  67. }

求逆序对数

同样,我们用 t[] 维护数字的个数

那么,对于第 i 个数 x ,逆序对数就是 i-sum(x)

离散化

如果直接维护,输入的数据较大(如 109 ),空间无法开那么大

所以可以定义一个结构体, val 表示数, id 表示下标

按照 val 从小到大排序, id 作为第二关键字,排完序后发现: id 等效与 val

这样空间只需开元素数量个

例题

洛谷 P1908

由于 add 的值始终是 1 ,所以简化了 add 函数

  1. #include<bits/stdc++.h>
  2. #define N 500005
  3. using namespace std;
  4. typedef long long ll;
  5. inline char gc(){
  6. static char buf[100000],*S=buf,*T=buf;
  7. return S==T&&(T=(S=buf)+fread(buf,1,100000,stdin),S==T)?EOF:*S++;
  8. }
  9. inline int read(){
  10. static char c=gc();register int f=1,x=0;
  11. for(;c>'9'||c<'0';c=gc()) c==45?f=-1:1;
  12. for(;c>'/'&&c<':';c=gc()) x=(x<<3)+(x<<1)+(c^48);
  13. return x*f;
  14. }
  15. struct opt{
  16. int val,id;
  17. bool operator < (const opt &x) const
  18. {
  19. if(val==x.val) return id<x.id;
  20. return val<x.val;
  21. }
  22. bool operator > (const opt &x) const
  23. {
  24. if(val==x.val) return id>x.id;
  25. return val>x.val;
  26. }
  27. }x[N];
  28. void qs(int l,int r){
  29. int i=l,j=r;
  30. opt mid=x[rand()%(r-l)+l];
  31. while(i<=j){
  32. while(x[i]<mid) i++;
  33. while(x[j]>mid) j--;
  34. if(i<=j){
  35. swap(x[i],x[j]);
  36. i++,j--;
  37. }
  38. }
  39. if(i<r) qs(i,r);
  40. if(j>l) qs(l,j);
  41. }
  42. int n,t[N];
  43. ll s;
  44. inline void add(int p){
  45. for(;p<=n;p+=p&-p)
  46. t[p]++;
  47. }
  48. inline ll sum(int p){
  49. ll ans=0;
  50. for(;p;p-=p&-p)
  51. ans+=t[p];
  52. return ans;
  53. }
  54. int main(){
  55. n=read();
  56. for(register int i=1;i<=n;i++){
  57. x[i].val=read();
  58. x[i].id=i;
  59. }
  60. if(n>1) qs(1,n);
  61. for(register int i=1;i<=n;i++){
  62. add(x[i].id);
  63. s+=i-sum(x[i].id);
  64. }
  65. printf("%lld",s);
  66. }


The End

c++ 树状数组的更多相关文章

  1. BZOJ 1103: [POI2007]大都市meg [DFS序 树状数组]

    1103: [POI2007]大都市meg Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 2221  Solved: 1179[Submit][Sta ...

  2. bzoj1878--离线+树状数组

    这题在线做很麻烦,所以我们选择离线. 首先预处理出数组next[i]表示i这个位置的颜色下一次出现的位置. 然后对与每种颜色第一次出现的位置x,将a[x]++. 将每个询问按左端点排序,再从左往右扫, ...

  3. codeforces 597C C. Subsequences(dp+树状数组)

    题目链接: C. Subsequences time limit per test 1 second memory limit per test 256 megabytes input standar ...

  4. BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]

    2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 2545  Solved: 1419[Submit][Sta ...

  5. BZOJ 3529: [Sdoi2014]数表 [莫比乌斯反演 树状数组]

    3529: [Sdoi2014]数表 Time Limit: 10 Sec  Memory Limit: 512 MBSubmit: 1399  Solved: 694[Submit][Status] ...

  6. BZOJ 3289: Mato的文件管理[莫队算法 树状数组]

    3289: Mato的文件管理 Time Limit: 40 Sec  Memory Limit: 128 MBSubmit: 2399  Solved: 988[Submit][Status][Di ...

  7. 【Codeforces163E】e-Government AC自动机fail树 + DFS序 + 树状数组

    E. e-Government time limit per test:1 second memory limit per test:256 megabytes input:standard inpu ...

  8. 【BZOJ-3881】Divljak AC自动机fail树 + 树链剖分+ 树状数组 + DFS序

    3881: [Coci2015]Divljak Time Limit: 20 Sec  Memory Limit: 768 MBSubmit: 508  Solved: 158[Submit][Sta ...

  9. 树形DP+DFS序+树状数组 HDOJ 5293 Tree chain problem(树链问题)

    题目链接 题意: 有n个点的一棵树.其中树上有m条已知的链,每条链有一个权值.从中选出任意个不相交的链使得链的权值和最大. 思路: 树形DP.设dp[i]表示i的子树下的最优权值和,sum[i]表示不 ...

  10. bzoj2743离线+树状数组

    奇葩染色,对于每一个点关心的是前前个同颜色的位置,但是处理方法相同 离线比较神奇,按照右端点排序,然后每次用的是左端点,就不用建可持久化树状数组(什么鬼)了 区间修改+单点查询 果断差分以后用树状数组 ...

随机推荐

  1. 关于表达式&& 和 || 有多项的时候的取值

    && 表达式只有两项的时候,如果表达式为false, 返回为false 的那一个 ,为true的时候    返回最后一个值 ||  只有两项的时候,返回为true 的那一个;都为fal ...

  2. C++---变量、数据类型和运算符

    内存 计算机使用内存来记忆或存储计算时所使用的的数据. 计算机执行程序时, 组成程序的指令和程序所操作的数据都必须存放在某个地方, 而这个地方就是计算机的内存, 也称为主存, 或随机访问存储器(RAM ...

  3. linux原生命令行看上面的内容

    上一页:Shift+pageUp 下一页:Shift+pageDown

  4. 自定义user表签发token、自定义认证类、simpleui模块使用

    今日内容概要 自定义User表,签发token 自定义认证类 simpleui的使用 多方式登陆接口(后面也写 内容详细 1.自定义User表,签发token # 如果项目中的User表使用auth的 ...

  5. 黑客入门——渗透必备神器Burpsuit的安装和简单使用教程

    ​ 很多人没有听说过burp全称(BurpSuite)BurpSuite是一款白帽子,黑帽子渗透测试必备工具,通过拦截HTTP/HTTPS的web数据包,当浏览器和相关应用程序的中间人,进行拦截.修改 ...

  6. python源码方式安装后如何卸载

    可以重新源码安装,此时需要记录安装文件细节,可通过--record XX来记录,如: python setup.py install --record setup.log 这时所有的安装细节都写到lo ...

  7. 使用 bitnami/postgresql-repmgr 镜像快速设置 PostgreSQL HA

    什么是 PostgreSQL HA? 此 PostgreSQL 集群解决方案包括 PostgreSQL 复制管理器(replication manager),这是一种用于管理 PostgreSQL 集 ...

  8. 攻防世界-MISC:give_you_flag

    这是攻防世界新手练习区的第四题,题目如下: 点击附件一下载,打开后发现是一个gif动图 可以看到动图有一瞬间出现了一个二维码,找一个网站给他分离一下 得到一张不完整的二维码(然后就不知道该怎么办了,菜 ...

  9. QCustomPlot开发笔记(一):QCustomPlot简介、下载以及基础绘图

    前言   QCustomPlot开发笔记系列整理集合,这是目前使用最为广泛的Qt图表类(Qt的QWidget代码方向只有QtCharts,Qwt,QCustomPlot),使用多年,系统性的整理,过目 ...

  10. jfinal极速开发

    下载jfinal项目,上面都配置好了不用自己新建从头配置.https://jfinal.com/ idea打开项目 配置数据库 resources目录下demo-config-dev.txt # co ...