目录:
一.原理
1.简介(百度介绍)
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
这种数据结构(算法)并没有C++和Java的库支持,需要自己手动实现。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不一定能解决。相比较而言,树状数组效率要高很多。
2.具体原理
先观察下面这张表(设原数组为a[],前缀数组为b[],树状数组中维护子集和的数组sum[],下面出现相应不再解释)
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
下标二进制 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 |
a数组 | 1 | 2 | 0 | 1 | 0 | 0 | 2 | 2 | 1 | 2 |
b数组 | 1 | 3 | 3 | 4 | 4 | 4 | 6 | 8 | 9 | 11 |
sum数组 | 1 | 3 | 0 | 4 | 0 | 0 | 2 | 8 | 1 | 3 |
sum维护子集区间 | 1 | 1-2 | 3 | 1-4 | 5 | 5-6 | 7 | 1-8 | 9 | 9-10 |
由表我们可以看出,sum[i]维护的区间大小是i二进制最低非零位所表示的数的大小,如i为2时,其最低为零位所表示的数为2,所以sum[2]维护区间【a[1],a[2]】的和,如i为8时,其最低非零位所表示的数为8所以sum[8]维护的是【a[1],a[2]…a[8]】的和。
下图将更为直观
知道sum要维护的东西之后,我们还要知道怎样利用sum[]求前缀和.我们发现b[8]=sum[8],b[7]=sum[7]+sum[6]+sum[4],也即是b[1000]=sum[1000],b[0111]=sum[0111]+sum[0110]+sum[0100].(里面是下标的二进制)由此我们可以知道前k个数的和 为 k依次减去其对应的最低非零位的数 的sum和。
由此我们就需要求出一个数的最低非零位所对应的数,也即是lowbit.如果学过补码你会知道 lowbit(x)=x&(-x) 当然你也可以这样算 lowbit(x)=x-(x&(x-1))(将x最低位的1以及后面所有的0都变成了0,然后被x减去之后,就得到想要的数了)
例如: x= 1011000 , (x&(x-1)) 1011000 & 1010111 =1010000 ,1011000-1010000=10000,即得到x最低非零位所对应的数
之后就是修改单点的值了,比如我们要修改a[2]的值,那么sum[2],sum[4],sum[8]…都要相应的改变,通过上图观察也可以看出来,位于其上面的区间段都要改变。假设x是我们要改变值的下标,那么受其影响的sum数组下标都是谁呢,显然是x,x+lowbit(x)(值为c),c+lowbit( c)(值为d),d+lowbit(d)…可以思考下为啥,可以类比求前缀和,此处不再解释.
原理清楚了,可以开始试着写了。
二.操作
1.求lowbit
int lowbit(int x){
return x&(-x);//写起来简单,不懂就直接记吧
//return x-(x&(x-1));
}
2.求前缀和
int getsum(int x){
int ans=0;
while(x>0){
ans+=sum[x];
x-=lowbit(x);//减去x最低非零位所表示的数
}
return ans;
}
3.单点更新
void update(int x,int value){
while(x<=n){
sum[x]+=value;
x+=lowbit(x);//加上x最低非零位所表示的数即为下一个受影响的sum下表
}
}
4.求区间和
//求区间[l,r]的和,调用getsum(r)-getsum(l-1)即可
//如果原数组a[]没开,怎样查询单点的a[x]的值呢
//调用getsum(x)-getsum(x-1)太慢,如果学过LCA我们可以这样求a[x]
// a[x]=sum[x]-(getsum(x-1)-getsum(LCA(x,x-1)))
//例如a[12]=sum[12]-(a[11]+a[10]+a[9])
//也即是a[12]=sum[12]-(getsum(11)-getsum(8))
//而8是怎么得来的呢,8其实是11和12的LCA所以这个式子看似要算两遍,但更省时间。
int get_sum(int x){
int ans=sum[x];
int lca=x-lowbit(x);
x--;
while(x!=lca){
ans-=sum[x];
x-=lowbit(x);
}
return ans;
}
5.查询某个前缀下标(元素非负)
//二分
int get_index(int v){
int x=0,len=n;
while(len!=0){
int now=x+len;
if(v>=sum[now]){
x=now;
v-=sum[now];
}
len/=2;
}
return x;
}
6.封装板子
struct BIT{
int bit[maxn];
inline void init(){ for(int i=1;i<=n;i++)bit[i]=0; }//初始化
inline int lowbit(int x){ return x&(-x); }//求lowbit
inline void update(int x,int val){ for(int i=x;i<=n;i+=lowbit(i)) bit[i]+=val; }//单点更新
inline int query(int x,int res=0){ for(int i=x;i;i-=lowbit(i))res+=bit[i]; return res; }//前缀和查询
inline int get_index(int v,int x=0){//查询前缀和下标
while(len!=0){ int now=x+len; if(v>=sum[now]){ x=now,v-=sum[now]; } len/=2; } return x;
}
}s;
三.二维树状数组
1.原理
类比一维树状数组,二维树状数组sum定义就很明确了:
s u m [ x ] [ y ] = ∑ i = x − l o w b i t ( x ) + 1 x ∑ j = y − l o w b i t ( y ) + 1 y a [ i ] [ j ] sum[x][y]=\sum_{i=x-lowbit(x)+1}^{x}\sum_{j=y-lowbit(y)+1}^{y}a[i][j] sum[x][y]=∑i=x−lowbit(x)+1x∑j=y−lowbit(y)+1ya[i][j]
时间复杂度: O ( ( l o g n ) 2 ) O((logn)^2) O((logn)2)(查询 修改)
2.代码实现
(1)二维前缀和
int getsum(int x,int y){//类比一维就多套了一个循环
int ans=0;
while(x>0){
int dy=y;
while(dy>0){
ans+=sum[x][dy];
dy-=lowbit(dy);
}
x-=lowbit(x);
}
return ans;
}
(2)二维修改操作
void update(int x,int y,int v){
while(x<=n){
int dy=y;
while(dy<=n){
sum[x][dy]+=v;
dy+=lowbit(dy);
}
x+=lowbit(x);
}
}
四.维护区间加
首先我们需要把区间加转化为单点加,假设对原数组a[] [ l , r ] [l,r] [l,r]整体加上v,利用差分的思想,可以新建一个数组c[] c [ i ] = a [ i ] − a [ i − 1 ] c[i]=a[i]-a[i-1] c[i]=a[i]−a[i−1]
也就是 c [ l ] + = v , c [ r + 1 ] − = v c[l]+=v,c[r+1]-=v c[l]+=v,c[r+1]−=v这是为什么呢,根据差分,我们知道在 a [ l ] − a [ r ] a[l]-a[r] a[l]−a[r]之间 在加v和不加v时 c [ l + 1 ] − c [ r ] c[l+1]-c[r] c[l+1]−c[r]的值没有发生变化,而 c [ l ] c[l] c[l]和 c [ r + 1 ] c[r+1] c[r+1]的值发生变化了,其中 c [ l ] = a [ l ] − a [ l − 1 ] c[l]=a[l]-a[l-1] c[l]=a[l]−a[l−1]比原来多了d,所以要加d, c [ r + 1 ] = a [ r + 1 ] − a [ r ] c[r+1]=a[r+1]-a[r] c[r+1]=a[r+1]−a[r]比原来少了d,所以要减d。附上一张表:(下标从1开始设 a [ 0 ] = c [ 0 ] = s u m [ 0 ] = 0 a[0]=c[0]=sum[0]=0 a[0]=c[0]=sum[0]=0)
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
a数组 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 1 | 1 |
c数组 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | -1 | 0 |
sum数组 | 1 | 2 | 1 | 5 | 2 | 4 | 2 | 13 | 1 | 2 |
将 a [ 4 ] a[4] a[4]到 a [ 8 ] a[8] a[8]的值加5
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
a数组 | 1 | 1 | 1 | 7 | 7 | 7 | 7 | 7 | 1 | 1 |
c数组 | 1 | 0 | 0 | 1+5 | 0 | 0 | 0 | 0 | -1-5 | 0 |
sum数组 | 1 | 2 | 1 | 5+5 | 2+5 | 4+10 | 2+5 | 13+20 | 1 | 2 |
假设现在更改过的a数组为s[],不难得出:
s [ i ] = a [ i ] + c [ 1 ] + c [ 2 ] + . . . + c [ i ] s[i]=a[i]+c[1]+c[2]+...+c[i] s[i]=a[i]+c[1]+c[2]+...+c[i]
所以:
t [ i ] = s [ 1 ] + s [ 2 ] + . . . + s [ i ] = ( a [ 1 ] + c [ 1 ] ) + ( a [ 2 ] + c [ 1 ] + c [ 2 ] ) + ( a [ 3 ] + c [ 1 ] + c [ 2 ] + c [ 3 ] ) + . . . + ( a [ i ] + b [ 1 ] + . . . + b [ i ] ) ( t [ ] 为 当 前 维 护 的 前 缀 数 组 ) t[i]=s[1]+s[2]+...+s[i]=(a[1]+c[1])+(a[2]+c[1]+c[2])+(a[3]+c[1]+c[2]+c[3])+...+(a[i]+b[1]+...+b[i])(t[]为当前维护的前缀数组) t[i]=s[1]+s[2]+...+s[i]=(a[1]+c[1])+(a[2]+c[1]+c[2])+(a[3]+c[1]+c[2]+c[3])+...+(a[i]+b[1]+...+b[i])(t[]为当前维护的前缀数组)
去掉括号整理得:
t [ i ] = ( a [ 1 ] + a [ 2 ] + . . . + a [ i ] ) + i × c [ 1 ] + ( i − 1 ) × c [ 2 ] + . . . + 1 × c [ i ] t[i]=(a[1]+a[2]+...+a[i])+i×c[1]+(i-1)×c[2]+...+1×c[i] t[i]=(a[1]+a[2]+...+a[i])+i×c[1]+(i−1)×c[2]+...+1×c[i]
其中 a [ 1 ] + . . + a [ i ] a[1]+..+a[i] a[1]+..+a[i]可以预处理得到(也就是前面提到的b[]前缀数组),但是后面却不好得到,不妨让每个c[]都乘i+1次
则:
t [ i ] = ( a [ 1 ] + a [ 2 ] + . . + a [ i ] ) + ( i + 1 ) × ( c [ 1 ] + c [ 2 ] + . . + c [ i ] ) − ( 1 × c [ 1 ] + 2 × c [ 1 ] + . . . + i × c [ i ] ) t[i]=(a[1]+a[2]+..+a[i])+(i+1)×(c[1]+c[2]+..+c[i])-(1×c[1]+2×c[1]+...+i×c[i]) t[i]=(a[1]+a[2]+..+a[i])+(i+1)×(c[1]+c[2]+..+c[i])−(1×c[1]+2×c[1]+...+i×c[i])
不妨设f[],其中f[i]=i×c[i];
现在整理一下:
(1)用b[]预处理原数组a[]的前缀和
(2)c[]的前缀和用树状数组维护,对于每次[L,R]修改,我们只修改 c [ l ] + = v , c [ r + 1 ] − = v c[l]+=v,c[r+1]-=v c[l]+=v,c[r+1]−=v;
(3)f[]的前缀和再用一个树状数组来维护,对于每次[L,R]修改,我们只修改 f [ l ] + = l ∗ v , f [ r + 1 ] − = ( r + 1 ) ∗ v f[l]+=l*v,f[r+1]-=(r+1)*v f[l]+=l∗v,f[r+1]−=(r+1)∗v;
这是一维条件下的,二维类比一维也可以用一个数组c[][]来使区间加变成单点加,单点查询变成矩形区间查询。
五.应用
1.二维数点问题(线段树/树状数组)
二维数点问题一般用线段树或树状数组来解,分两种形式。
(1)离线型: 树状数组/线段树+一维排序(消除一维限制)
(2)在线型: 主席树
传送门(大佬博客):https://www.cnblogs.com/uid001/p/10718937.html
例题: http://poj.org/problem?id=2352
题意: 二维平面n个点,求出每个点左下角的点的个数。
解析:对于点(x[i],y[i])实际让求之前满足x<=x[i]&&y<=y[i]的点,所以可以先把一维限制去掉,怎么去,题上说过给出的点先按y排序,再按x排序,所以只要统计x<=x[i]的点就行了,可以用树状数组来维护这个点集合。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <map>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=32011;
int n,x,y;
int sum[maxn],ans[maxn];
int lowbit(int x){
return x&(-x);
}
void update(int x,int value){
while(x<=32001){
sum[x]+=value;
x+=lowbit(x);
}
}
int getsum(int x){
int ans=0;
while(x>0){
ans+=sum[x];
x-=lowbit(x);
}
return ans;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&x,&y);
x++;//将x范围调至[1,32001]
update(x,1);
ans[getsum(x)-1]++;//由于不包括本身,所以水平为getsum(x)-1的数加1
}
for(int i=0;i<n;i++){
printf("%d\n",ans[i]);
}
return 0;
}
2.查询第k大
直接上例题:
http://acm.hdu.edu.cn/showproblem.php?pid=2852
题意:q次操作
如果op为0,向数组中增加一个数x;
op为1,向数组中删除一个数x;
op为2,查询比x大的第k个数
解析:
可以开一个a数组存第x个数的个数,op为0则a[x]++;op位为1,先判断a[x]是否大于0,大于则a[x]–,否则输出No Elment!
对于查询操作,我们可以二分i,找到满足:
a[x+1]+a[x+2]+…+a[i-1]<k
a[x+1]+a[x+2]+…+a[i]>=k
i即为答案 两边同时加上a[1]+a[2]+…+a[x]
转化一下,也即是找第一个满足:a[1]+a[2]+…+a[i]>=a[1]+a[2]+…+a[x]+k的i
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <map>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=1e5+10;
int q;
int a[maxn],sum[maxn];
int lowbit(int x){
return x&(-x);
}
void update(int x,int value){
while(x<=maxn-10){
sum[x]+=value;
x+=lowbit(x);
}
}
int getsum(int x){
int ans=0;
while(x>0){
ans+=sum[x];
x-=lowbit(x);
}
return ans;
}
int get_index(int v){
int l=0,r=100000,ans=inf;
while(l<=r){
int m=(l+r)>>1;
int mid=getsum(m);
if(mid>=v){
ans=m;
r=m-1;
}
else l=m+1;
}
return ans;
}
int main()
{
while(~scanf("%d",&q)){
memset(sum,0,sizeof(sum));
memset(a,0,sizeof(a));
for(int i=1;i<=q;i++){
int op,x,k;
scanf("%d",&op);
if(op==0){
scanf("%d",&x);
a[x]++;
update(x,1);
}
else if(op==1){
scanf("%d",&x);
if(a[x]>0)a[x]--,update(x,-1);
else puts("No Elment!");
}
else {
scanf("%d%d",&x,&k);
int p=getsum(x)+k;
int ans=get_index(p);
if(ans==inf)puts("Not Find!");
else printf("%d\n",ans);
}
}
}
return 0;
}
3.优化dp
假设要我们维护一个序列 a [ ] a[] a[]的最长上升子序列;
我们知道 d p [ i ] dp[i] dp[i]为以a[i]为结尾的最长上升子序列
则状态转移方程: d p [ i ] = m a x ( d p [ j ] ) + 1 ( j < i 且 a [ j ] < a [ i ] ) dp[i]=max(dp[j])+1(j<i且a[j]<a[i]) dp[i]=max(dp[j])+1(j<i且a[j]<a[i])
很容易看出每次求 d p [ i ] dp[i] dp[i]都要去前面遍历去找满足的最大 d p [ j ] dp[j] dp[j],所以普通写法复杂度为 O ( n 2 ) O(n^2) O(n2),如果我们用数据结构去维护 d p [ i ] dp[i] dp[i],每次去前面查询时间复杂度会大大减少。
根据状态转移方程可以看出,我们需要一种支持单点更新,区间查询的数据结构,线段树/树状数组都可胜任,用树状数组更简便,总时间复杂度为 O ( n l o g n ) O(nlog_n) O(nlogn)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
const int inf=0x3f3f3f3f;
const int maxn=1e3+7;
int n,m,t;
int a[maxn],b[maxn];
int dp[maxn];
void discretization(){//对原数组离散化
sort(b+1,b+1+n);
int len=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+len,a[i])-b;
}
}
struct BIT{
int bit[maxn];
inline void init(){ for(int i=1;i<=n;i++)bit[i]=0; }
inline int lowbit(int x){ return x&(-x); }
inline void update(int x,int val){ for(int i=x;i<=n;i+=lowbit(i)) bit[i]=max(bit[i],val); }
inline int query(int x,int res=0){ for(int i=x;i;i-=lowbit(i))res=max(bit[i],res); return res; }
}s;
int main()
{
while(~scanf("%d",&n)){
for(int i=1;i<=n;i++){
scanf("%d",a+i);
b[i]=a[i];
}
discretization();
s.init();
int ans=0;
for(int i=1;i<=n;i++){
dp[i]=s.query(a[i]-1)+1;
s.update(a[i],dp[i]);
ans=max(ans,dp[i]);
}
cout<<ans<<'\n';
}
return 0;
}