树状数组 Binary Indexed Tree
2016-06-29 00:21
316 查看
树状数组 (Binary Indexed Tree)
树状数组 Binary Indexed Tree引入
问题形式
基本思想
实现
子集和划分
查询前缀和
修改子集和
常用技巧
查询任意区间和
利用sumsum数组求aia_i
成倍扩大缩小
扩展到高维
例题
逆序对
引入
在一些数列问题中,使用一般的算法时间复杂度太高,空间复杂度又无法让人接受,那么此时树状数组将会是一个很好的选择。另外,树状数组能解的问题线段树也能解,而线段树能解的问题树状数组不一定能解,但线段树和树状数组都可解的问题中,树状数组往往比线段树更加好写,时间复杂度往往会更低一些。问题形式
定义一个数组a[n],一般树状数组可实现一下两种操作1、修改操作:给a[i]增加一个增量delta
2、查询操作:询问a[1...index]的前缀和,即∑indexi=1a[i]
朴素的算法能在O(1)完成修改,但是查询需要O(n),而采用树状数组,则能解决较多次查询的问题
基本思想
对于每次求前缀和,我们将其分解为一系列的子集,进而进行求和,但是要注意,分解出来的子集不能相交。例如,将15可以分解为23+22+21+20,那么类似的,可以将∑indexi=1a[i]也分解为几个子集的和。那么怎么分呢?一般来说,如果index的二进制表示中有k个1,那么我们就将其分解为k个子集的和,所以,我们可以给出以下的子集划分:下标 | 二进制 | 子集 |
---|---|---|
1 | 1 | 1 |
2 | 10 | 1~2 |
3 | 11 | 3 |
4 | 100 | 1~4 |
5 | 101 | 5 |
6 | 110 | 5~6 |
7 | 111 | 7 |
8 | 1000 | 1~8 |
9 | 1001 | 9 |
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | !2 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
a数组 | 2 | 0 | 1 | 1 | 0 | 2 | 3 | 0 | 1 | 0 | 2 | 1 |
前缀和 | 2 | 2 | 3 | 4 | 4 | 6 | 9 | 9 | 10 | 10 | 12 | 13 |
子集和sum | 2 | 2 | 1 | 4 | 0 | 2 | 3 | 9 | 1 | 1 | 2 | 4 |
子集和 | 查询树 |
---|---|
如图所示: 图中的每一个长方形都代表每个子集对应的部分和,深色代表了自己下标所对应的值a[i],浅色代表还需要维护的别的值a[k...i−1]。 | 对于每个查询,我们如果顺着黑色的实线依次走下去,就可以得到对应的前缀和了。如图: 如此一来,对于每个查询,我们找到树中对应标号的结点,顺着边一直走到0,依次累加对应的子集和,到根的时候,我们就得到了最终的前缀和。通过观察我们还能发现:每个结点的深度代表了对应查询所需要累加子集和的数量,也就是对应数字的二进制中1的个数。另外,除了那个虚拟的根结点0之外,其他的每个结点的孩子个数都等于其二进制中末尾0的个数。因此我们叫它树状数组也叫Binary Indexed Tree |
实现
子集和划分
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
二进制 | 1 | 10 | 11 | 100 | 101 | 110 | 111 | 1000 |
元素个数C | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 8 |
C的二进制表示 | 1 | 10 | 1 | 100 | 1 | 10 | 1 | 1000 |
不难得到求法:
方法1:C(i)=i−(iAnd(i−1))
方法2:C(i)=iAnd(−i)
(根据计算机中的补码思想,想想为什么)
查询前缀和
int C(int i){ //lowbit return i&-i; } int query(int i){ int ans=0; while (i>0) { ans+=sum[i]; i-=C(i); return ans; }
时间复杂度O(logn)
修改子集和
当要修改一个元素a[i](给其增加delta)的时候,注意要将其所有的父节点的sum值都更新。void change(int i,int delta){ while (i<=n){ sum[i]+=delta; i=i+C(i); } }
常用技巧
查询任意区间和:
查询∑pi=ka[i]时,只需要计算query(p)−query(k−1)即可利用sum数组求a[i]
要计算a[i],只需要计算query(i)−query(i−1),进一步,从查询树中,我们可以发现:a[i]=sum[i]−(query(i−1)−query(LCA(i,i−1)))其中LCA为最近公共祖先。
int getval(int i){ int ans=sum[i]; int lca=i-C(i); i--; while (i!=lca) { ans-=sum[i]; i-=C(i);} return ans; }
成倍扩大/缩小
如何让a中所有元素变为原来的一半呢?如果直接对树状数组中所有的值除以2,由于取整的原因会造成不连续,但我们如果用倒序修改的方式就能完美解决此问题:void enlarge(int x){ for (int i=n;i>=1;i--) change(i,-getval(i)/2); }
扩展到高维
我们先来看一下二维的树状数组:定义一个二维数组a[n][n],维护两种操作:
1、修改:给a[i][j]加一个delta;
2、查询:求∑xi=1∑yj=1a[i][j]
那么便可以将sum定义为sum[x][y]=∑xi=x−C(x)+1∑yj=y−C(y)+1a[i][j]
那么代码就很简单了:
int query2d(int x,int y){ int ans=0; while (x>0) { int ty=y; while (ty>0){ ans+=sum[x][ty]; ty-=C(ty); } x-=C(x); } return ans; } void change2d(int x,int y,int delta){ while (x<=n){ int ty=y; while (ty<=n){ sum[x][ty]+=delta; ty+=C(ty); } x+=C(x); } }
同理,我们也可以扩展到n维,笔者在这里就不详细写了。
例题
逆序对
求数组a[n]中逆序对的数量,逆序对(i,j)满足1<=i<j<=n,a[i]>a[j]。输入:第一行一个正整数n,代表数字个数;接下来一行有n个整数,代表数组的元素
输出:一个整数,代表逆序对个数
示例代码:
#include<iostream> using namespace std; typedef long long ll; struct node { ll v; int d; node(){ v=d=0; } }; int n; node a[100001]; int sum[100001]; int c[100001]; inline int lb(int x){ //lowbit return x&(-x); } void qs(int l,int r){ //快速排序 int i=l,j=r,mid=a[(l+r)/2].v; do { while (a[i].v<mid) i++; while (a[j].v>mid) j--; if (i<=j){ node mm=a[i]; a[i]=a[j]; a[j]=mm; i++,j--; } }while (i<=j); if (i>l) qs(i,r); if (j<r) qs(l,j); } ll query(int x){ ll ans=0; while (x>0){ ans+=sum[x]; x-=lb(x); } return ans; } void insert(int x){ while (x<=n){ sum[x]++; x+=lb(x); } return ; } int main(){ ios::sync_with_stdio(false); //此句话可加快iostream的速度 cin>>n; for (int i=1;i<=n;i++){ cin>>a[i].v; a[i].d=i; } qs(1,n); int last=-1,temp=-1,k=0; for (int i=1;i<=n;i++){ temp=a[i].v; if (temp!=last){ last=temp; k++; } c[a[i].d]=k; } ll s=0; for (int i=n;i>=1;i--){ insert(c[i]); s+=query(c[i]-1); } cout<<s; return 0; }
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- 关于指针的一些事情
- c++ primer 第五版 笔记前言
- share_ptr的几个注意点
- 书评:《算法之美( Algorithms to Live By )》
- 动易2006序列号破解算法公布
- C#递归算法之分而治之策略
- Ruby实现的矩阵连乘算法
- C#插入法排序算法实例分析
- C#算法之大牛生小牛的问题高效解决方法
- Lua中调用C++函数示例
- Lua教程(一):在C++中嵌入Lua脚本
- Lua教程(二):C++和Lua相互传递数据示例
- C#算法函数:获取一个字符串中的最大长度的数字
- 超大数据量存储常用数据库分表分库算法总结
- C#数据结构与算法揭秘二
- C#冒泡法排序算法实例分析
- 算法练习之从String.indexOf的模拟实现开始
- C#算法之关于大牛生小牛的问题
- C++联合体转换成C#结构的实现方法