树状数组

  • 欲完成修改值和查询区间和两种操作
  • 求前缀和的做法时间复杂度为O(n)O(n)O(n)
  • 使用树状数组时间复杂度降为O(logn)O(logn)O(logn)
  • lowbit
    • 1、x&(-x)
    • 2、x- ( x&(x-1) )
    • 3、x&(x^(x-1))
    • lowbit累计1出现的次数
  • 树状数组思想
    • 求前缀和操作
    • 修改更新操作
  • 注意点
  • 练兵场
    • [【模板】树状数组 1——单点更新,区间查询](https://www.luogu.com.cn/problem/P3374)
    • 241. 楼兰图腾(y轴上区间段)
  • 三、Cows(树状数组)
  • 参考

欲完成修改值和查询区间和两种操作

给出一个长度为n的数组,完成以下两种操作:

  1. 将第iii个数加上kkk
  2. 查询区间[i,j][i,j][i,j]内每个数的和

求前缀和的做法时间复杂度为O(n)O(n)O(n)

  1. 单点修改:O(1)O(1)O(1)
  2. 区间和查询:O(n)O(n)O(n)

两种操作一起的时间复杂度取决于时间复杂度大的那个
那么对于mmm次查询,长度为nnn的数组,时间复杂度就是m.nm.nm.n,只要数据范围超过1e61e61e6,绝对超时

查询区间和以前的做法要么就是查询很慢,修改很快,那怎么办呢,那就存储前缀和来提高查询速度,但这样一来修改了之后要更新这些前缀和,更新又很慢;

树状数组就完美地综合了这两种做法,存储后缀和,更新后缀和,通过lowbitlowbitlowbit来限定后缀和的长度,利用二进制使得查询、更新的时间复杂度都在O(logn)O(logn)O(logn)。

使用树状数组时间复杂度降为O(logn)O(logn)O(logn)

  1. 单点修改:O(logn)O(logn)O(logn)
  2. 区间查询:O(logn)O(logn)O(logn)
    两种操作的时间复杂度持平了,快的没那么快,慢的没那么慢

lowbit

lowbit()函数用来取一个二进制最低位的一与后边的0组成的数
例:
5(101),lowbit(5)=1(1)

12(1100),lowbit(12)=4(100)

1、x&(-x)

int lowbit(int x)
{return x&(-x);
}

原理,二进制数的负数是原码对应的补码,即各位取反加一

12(1100),-12(0100)
(正数补码反码都和原码一样啦)
曾记否,负数一般用补码表示从而进行运算,补码的朴素求法就是对各位取反再加1,但在计算机组成原理中介绍了一种更高明的求负数补码做法:以最低位的1为基准(该位保持不变),左侧全部取反,右侧的0保持不变
这样一来,x&(-x), 最低位的1左侧变为全0,右侧的全0继续保持,从而 保留二进制下最低位出现的1的位置,其余位置置0

2、x- ( x&(x-1) )

先消掉最后一位1,然后再用原数减去消掉最后一位1后的数,答案就是lowbit(x)的结果
曾记否,x&(x-1)用于消去最低位的1,因为x−1x-1x−1, 根据小学数学减法运算的借位原则(滑稽),对一个二进制数进行减1,那么会出现从这个这个数的最
后一个1开始到最后的所有数都取反,即构成一个01111⋯的串, x-1相对于x,最低位的1左边不变,最低位的1变为0,最低位1的右边由全0变为全1,则x&(x-1)就可以消去最低位的0
可用于求一个二进制数中各位上1的总个数

int x;
cin>>x;
int cnt=0;
while(x){x=x&(x-1);cnt++;
}
cout<<cnt;

3、x&(x^(x-1))

记前两种就好啦
任何数x和1相与得到非x
任何数x和0相与得到x本身

lowbit累计1出现的次数

int x;
cin>>x;
int cnt=0;
while(x){x-=(x&-x);cnt++;
}
cout<<cnt;

我们可以使用lowbit运算统计一个整数的二进制形式下1的个数。

实现原理很简单啦,就是:我们先用lowbit运算找出lowbit(x),然后用原数减去这个数,依次循环,直到为0为止。

树状数组思想

求前缀和操作

类比在求前缀和数组中求前缀和(有n个前缀和)的做法,树状数组中求的是区间和(求近似lognlog nlogn个区间),那么我们怎么将长度为xxx的区间划分这近似lognlognlogn个子区间呢?
我们需要从xxx的二进制表示寻找灵感,假设x的二进制表示有y位,那么2y2^{y}2y>x,即 y<logxy<log xy<logx,也就是说x的二进制表示位数恰好近似logxlog xlogx,x的二进制表示中111的位数不超过logxlog xlogx,我们可以从右往左构想划分的这近似lognlog nlogn个区间。
首先找到x的二进制表示最低位的111,若拿掉这个111,那么x的值就会相应地减小这个111对应的数量级大小

比如 14对应二进制表示 1110,最低位的1对应的数量级是2,拿掉1后对应的数是12=14 - 2=(1110−10)2(1110-10)_2(1110−10)2​=(1100)2(1100)_2(1100)2​

按照这种做法,依次找到x最低位的1,并把该1拿去,这位1对应的数量级大小对应的是从后往前划分子区间的长度
像y总表示的那样,每个子区间(从右往左划分的第a+1a+1a+1个子区间)可表示位(x−2i1−2i2−2i3……−2ia−2ia+1(x-2^{i_1}-2^{i_2}-2^{i_3}……-2^{i_a}-2^{i_{a+1}}(x−2i1​−2i2​−2i3​……−2ia​−2ia+1​ ~ x−2i1−2i2−2i3……−2ia]x-2^{i_1}-2^{i_2}-2^{i_3}……-2^{i_a}]x−2i1​−2i2​−2i3​……−2ia​]
左开右闭,左界限代表拿掉了xxx的第a+1a+1a+1个1之后的大小
每个子区间(L(L(L~ R]R]R]的长度一定是R的二进制表示最后一位1对应的幂次。

上图就非常形象地展示了各个子区间的划分情况
上方y总举的例子是针对于R为 2的k次幂的情况,
子区间元素之和tree[R]=tree[R−lowbit(R)+1,R]=ax+tree[R−1]+tree[R−1−lowbit(R−1)]+tree[R−1−lowbit(R−1)−lowbit(R−1−lowbit(R−1))+……]tree[R]=tree[R-lowbit(R)+1,R]=a_x+tree[R-1]+tree[R-1-lowbit(R-1)]+tree[R-1-lowbit(R-1)-lowbit( R-1-lowbit(R-1) )+……]tree[R]=tree[R−lowbit(R)+1,R]=ax​+tree[R−1]+tree[R−1−lowbit(R−1)]+tree[R−1−lowbit(R−1)−lowbit(R−1−lowbit(R−1))+……]
首先进行x−1x-1x−1操作就可以最低位1变为0,其右边的所有0全部变为1,这所有通过x−1x-1x−1操作变为0的1中,每一个1的数量级都对应着一个子区间的长度。
当然了,对于R为奇数的情况,要求R的前缀和 也是一样的操作,只是拿掉的第一个1就是最末尾的1,对应的区间段就是1
如下图

其实我不太明白为什么要"看似多余"地先进行x−1x-1x−1(当然了这种说法完全正确),从图上就可以看出来,要求R的前缀和,不管x是奇数还是偶数,都是依次找到最低位1对应的从右往左的子区间,并拿掉这个1,把找到的所有1对应的子区间的tree值累加。
举两个栗子
譬如R=16, R的前缀和,直接就是一个tree[16]这一个区间,(10000)2(10000)_2(10000)2​拿掉一个1就变为全0找不到1了,所以求16的前缀和只用tree[16]tree[16]tree[16]这一个区间代表就好了
再譬如第二张图中,R=7,RR=7,RR=7,R的前缀和就是tree[7]、tree[6]、tree[4]tree[7]、tree[6]、tree[4]tree[7]、tree[6]、tree[4]这3个子区间的和。
按照模板代码也是这个说法,就是寻找低位1对应的子区间的和进行累加,至于这些子区间对应的tree值,在确定a数组的元素时,就可以通过update函数进行更新了

值得注意是,tree【i】的值代表的可不是数组a【i】的前缀和,而是以a【i】为区间右端点,长度为lowbit(i)的子区间的元素和。

int sum(int x){int res=0;for(int i=x;i;i-=lowbit(i)){res+=tree[x];}
return res;
}

修改更新操作


可以理解为一个递归的操作,x、x-lowbit(x) 、

根据插入各元素进而更新子区间和的规律可知,加入a[i],只会对tree【j】造成更新(j>=i),
更新元素a【i】的值之后,找到所有包含a【i】的子区间和
每修改一个元素后,它所直接影响到的子区间是唯一的,首先是更新a【i】这个节点直接的父区间,再到父区间的父区间……
非常巧妙地发现,
一个子区间和它的所有子区间,它们的右边界的关系无非就是父区间的最低位1的数量级大于子区间最低位1的数量级,只要将子区间右边界加上lowbit(子区间右边界),就能得到其直接父区间的右边界,p=R+lowbit(R)看图中例子。
每更新一个父区间,a【i】末尾的0会增加1个,由于a【i】最多只有loga[i]loga[i]loga[i]位,它所影响到的区间最多是loga[i]log a[i]loga[i]个。因此修改更新操作的时间复杂度也是loga[i]log a[i]loga[i](这里说的a【i】就是在求前缀和步骤中,某个子区间的右边界R或者说是y总讲解中的右边界x)

void update(int x,int c){//x既代表元素数组下标,也是所属子区间的右边界
//n是数组长度,整个区间的右边界 for(int i=x;i<=n;i+=lowbit(i)){tree[i]+=c;}
}

注意点

搞清楚树状数组利用的下标所代表的含义、变量、范围(要对哪个下标确定的前缀和范围进行查询),在update函数中需要用到下标最大值,如果不能明确找到,就用题目给的数据范围
树状数组下标必须从1开始,否则在update函数中陷入死循环,对于题目给的下标从0开始时,必须把所有下标加1

练兵场

【模板】树状数组 1——单点更新,区间查询

#include<iostream>
#include<algorithm>
using namespace std;
const int N=5e5+10;
//int a[N];
int tree[N];int n,m;
int lowbit(int x){return x&(-x);
}
void update(int x,int c){for(int i=x;i<=n;i+=lowbit(i)){tree[i]+=c;}
}
//请记住,这些进行lowbit操作、和遍历的变量表示的都是
//区间下标(区间右边界)
int getSum(int x){//获得1~x这个区间段的和,x的前缀和 int res=0;for(int i=x;i;i-=lowbit(i)){res+=tree[i];} return res;
}
int main(){cin>>n>>m;int v;for(int i=1;i<=n;i++){cin>>v;update(i,v);}
//注意了,为什么update比add要更贴切
//对于长度位n的数组a来讲,一开始有着初始值但只消获取其子区间和tree[i]
//至于后来对数组a中的某个元素a[i]进行修改,也可以直接更新子区间和
//可以当作,传入a数组中n个元素的值时就是进行了n次对子区间的更新操作
//一开始就当作a数组元素全0 int op,x,k,y;while(m--){cin>>op;if(op==1){cin>>x>>k;update(x,k);//x是区间下标,是a[x]所属子区间的右边界 }else{cin>>x>>y;cout<<getSum(y)-getSum(x-1);//x~y这段子区间的和 ,getSum求的是前缀子区间的和if(m)cout<<endl; } }return 0;
}

241. 楼兰图腾(y轴上区间段)

原题链接



题意:在二维坐标系中给出n个点的坐标,它们的横坐标分别为1~n,纵坐标输入给定。求横坐标相邻的三个点组成’V’和’A’的情况总和

思路:集合思想,针对每一个点(x,y),求出它左边纵坐标大于y的点的总个数 和 它右边纵坐标大于y的点的总个数,相乘就是组成’V’的个数。同理,针对每一个点(x,y),求出它左边纵坐标小于y的点的总个数 和 它右边纵坐标小于y的点的总个数,相乘就是组成’A’的个数。累加各个顶点组合情况就ok
一句话实现思路y轴上的区间段 利用树状数组 求前缀和、修改元素值

从时间复杂度稍微简化点考虑对于每个顶点,都要掌握它左边比它高的点
如果暴力时间复杂度是O(n^2),肯定TLE
由于只有区间段求和和更新两个操作,想到树状数组
掌握顶点(x,y)左边比它高的点的个数之和,所以是更高的顶点数之和
考虑区间和的含义,这里应该考虑y轴上的区间段,以y轴正方向为左求前缀和
这里考虑的是点的个数,每个顶点对前缀和贡献是1
由于考虑的是点(x,y)左边的点,那么对于Greater数组的值,只能是在加入点(x,y)以前得到的,试想一股脑加入所有的点,那么大于y值的点可能
有多个,在点(x,y)以后加入相当于更新了y轴区间段上某个元素的值,那么无法判断是在点(x,y)左边还是右边,而是把高于点(x,y)的所有点数都算进来了
注意因为是y轴上的区间段,树状数组中所有描述区间边界和下标的变量都是点的y值 ,写的过程中千万注意这一点,一不小心惯性思维就会将x轴坐标作为区间段的边界

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=2e5+10;
int a[N];
int tree[N];
int n;
int Greater[N];
int Lower[N];
int lowbit(int x){return x&(-x);
}
void update(int x,int c){//因为y1~yn是1到n的一个排列,整个y轴上区间的右边界是n for(int i=x;i<=n;i+=lowbit(i)){tree[i]+=c;}
}
int getSum(int x){int res=0;for(int i=x;i;i-=lowbit(i)){res+=tree[i];}return res;
}
int main(){cin>>n;
//  int v;
//  for(int i=1;i<=n;i++){//      cin>>v;
//      update(i,v);
    按照树状数组的套路,就是根据输入的n个值一次性将区间和更新n次
//  }for(int i=1;i<=n;i++){cin>>a[i];}//为什么一定要保存起来,
//  因为做到后面发现要求后缀数组(从后往前加时对应的前缀和数组) for(int i=1;i<=n;i++){//      cin>>y;int y=a[i];Greater[i]=getSum(n)-getSum(y);//tree数组放的是y轴区间段上的子区间和
//在前面已加入树状数组的所有数中统计在区间[y + 1, n]的数字的出现次数Lower[i]=getSum(y-1);update(y,1); //将y加入树状数组,即数字y出现1次}//注意因为是y轴上的区间段,树状数组中所有描述区间边界和下标的变量都是点的y值 fill(tree,tree+N,0);ll res1=0;ll res2=0;for(int i=n;i>=1;i--){int y=a[i];res1+=(ll)Greater[i]*(getSum(n)-getSum(y));res2+=(ll)Lower[i]*(getSum(y-1));update(y,1);}cout<<res1<<" "<<res2; return 0;
}

三、Cows(树状数组)

给定每头牛的吃草范围,问对于每头牛,有几头牛的吃草范围完全包括它的吃草范围

搞清楚树状数组利用的下标所代表的含义、变量、范围(要对哪个下标确定的前缀和范围进行查询),在update函数中需要用到下标最大值,如果不能明确找到,就用题目给的数据范围
树状数组下标必须从1开始,否则在update函数中陷入死循环,对于题目给的下标从0开始时,必须把所有下标加1

1、Hint建议用scanf输入时记得添加,ios:sync_with_stdio(false);
cin.tie(0)
不然就可能超时

2、区间完全相等时不能算作更强,因此需要特判

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+5;
struct node{int l,r;int id;
//  node(int l,int r,int id):l(l),r(r),id(id){}bool operator<(const node& p)const{if(r==p.r)return l<p.l; return r>p.r;}
//给所有牛排序,更强的牛排在前面,
//等求每头牛res时,才能遍历过所有可能比它强的
//何谓更强,右边界更大,左边界更小
//当然了,也可以更强的排在后面,从后往前遍历就是了
}a[N];
int res[N];//对于每头牛,比它更强的牛的数量
//排好序之后,之前遍历过的牛的右边界一定大于或等于现在牛的右边界
//于是只要考虑现在牛的左边界的左边有多少之前遍历过的牛的左边界
int maxx;
int tree[N];//树状数组
int lowbit(int x){return x&(-x);
}
void update(int x,int c){for(int i=x;i<=N;i+=lowbit(i)){tree[i]+=c; }
}
int getSum(int x){int ans=0;for(int i=x;i;i-=lowbit(i)){ans+=tree[i];}return ans;
}
int main(){ios::sync_with_stdio(false);cin.tie(0);int n;while(cin>>n&&n){fill(tree,tree+N,0); fill(res,res+N,0);for(int i=1;i<=n;i++){cin>>a[i].l>>a[i].r;a[i].l++;//!!!因为l,r范围从0开始,树状数组下标必须从1开始 a[i].r++;a[i].id=i;}sort(a+1,a+n+1);
//      maxx=a[n].l+1;以右边界为第一关键字的,不能确定最大的左边界
//  可以看到我们利用树状数组 update和getSum都是根据 吃草区间的左边界为下标的
//  首先该下标必须从1开始,为0的话在update函数中就会陷入死循环
//  其次要确定该下标的最大范围,左边界的最大值是不能确定的(除非逐一比较)
//  干脆就取maxx为题目所给的最大范围N for(int i=1;i<=n;i++){if((a[i].r==a[i-1].r)&&(a[i].l==a[i-1].l))res[a[i].id]=res[a[i-1].id];else res[a[i].id]=getSum(a[i].l);update(a[i].l,1);} for(int i=1;i<=n;i++){cout<<res[i];if(i!=n)cout<<" ";}cout<<endl;}return 0;
}

参考

B站简明讲解,图很靓
树状数组
树状数组简单易懂的详解
图很抽象
内容详尽,图很靓丽
待看,区间更新区间查询

树状数组(求子区间和+更新元素值)相关推荐

  1. hdu1754(树状数组求最值问题)

    I Hate It Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total ...

  2. 牛客练习赛33 D tokitsukaze and Inverse Number (树状数组求逆序对,结论)

    链接:https://ac.nowcoder.com/acm/contest/308/D 来源:牛客网 tokitsukaze and Inverse Number 时间限制:C/C++ 1秒,其他语 ...

  3. nyoj 1261 音痴又音痴的LT(离散化+树状数组求K小数)

    题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=1261 解题思路:比较水的题,用离散化+树状数组求K小数即可,先用一次离线处理. #inc ...

  4. poj 2299 Ultra-QuickSort(树状数组求逆序数+离散化)

    题目链接:http://poj.org/problem?id=2299 Description In this problem, you have to analyze a particular so ...

  5. 离散化+树状数组求逆序数

    题目:http://poj.org/problem?id=2299 离散化是一种常用的技巧,有时数据范围太大,可以用来放缩到我们能处理的范围 因为其中需排序的数的范围0--- 999999999:显然 ...

  6. loj #535. 「LibreOJ Round #6」花火 树状数组求逆序对+主席树二维数点+整体二分...

    $ \color{#0066ff}{ 题目描述 }$ 「Hanabi, hanabi--」 一听说祭典上没有烟火,Karen 一脸沮丧. 「有的哦-- 虽然比不上大型烟花就是了.」 还好 Shinob ...

  7. 【dfs序+树状数组】多次更新+求结点子树和操作,牛客小白月赛24 I题 求和

    前置知识点 dfs遍历 树状数组/线段树知识 链接 I题 求和. 题意 已知有 n 个节点,有 n−1 条边,形成一个树的结构. 给定一个根节点 k,每个节点都有一个权值,节点i的权值为 vi 给 m ...

  8. 树状数组求逆序对_区间和的个数(树状数组)

    327. 区间和的个数 给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper. 区间和 S(i, j) 表示在 nums 中,位置从 i ...

  9. 树状数组求逆序对_初识树状数组

    树状数组是用来解决数列多次单点修改和前缀和查询的利器. 首先我们来看问题的原型: 已知一个长度为n(n<=10 0000)的数列,初始值都是零,现在我们要对数列施加两种类型的操作共q(q< ...

最新文章

  1. vue 优化CDN加速
  2. java set 取第一个_set集合取第一个元素的几种方法
  3. 操作系统01_进程和线程管理
  4. android studio 如何导入工程文件,Android studio如何导入已有的eclipse工程
  5. 电脑 你离我有多远!
  6. MySQL学习笔记1(增删查改)
  7. python调用数据库存储过程_python调用MySql存储过程
  8. Ansible之roles使用
  9. Eclipse 中如何设置字体大小与样式
  10. 记一篇IT培训日记005-Hello Java
  11. 你了解光学中群的概念么(群时延、群速度、群速度折射率、群时延色散)
  12. 逆流而上的你,送给现在的你
  13. Vue 自定义移动端的 滑动事件
  14. c语言算个人所得税的源代码,C语言编写一个计算个人所得税的程序,要求输入收入金额,能够输...
  15. 应届生面试的5大技巧,附600字自我介绍范文
  16. C#WinForm实现雷速网站比赛MQTT逆向采集
  17. GrabCut图像分割
  18. iOS开发者必备:自己总结的iOS、mac开源项目及库
  19. Lua语言中的冒号:和点.
  20. 「11」Python实战篇:利用KNN进行电影分类

热门文章

  1. PHPStorm配置mysql
  2. Linux基础知识、常用命令
  3. 2023年人力资源管理师报名和培训费用是多少
  4. html网页制作教程按钮添加,网页制作html5自定义video标签的海报与播放按钮功能...
  5. 【0513】 将字符串转换成时间格式
  6. 《牛津字典精华总结》- 初阶系列 - 字母 - U
  7. java毕业设计独龙族民族特色服务网站Mybatis+系统+数据库+调试部署
  8. 给Mac版微信手动添加URL Scheme
  9. axios和ajax区别
  10. c语言输出3010进制3位数排列组合代码