我们知道要是一个数组的和C[i]=A[1]+A[2]+.......+A[i],那么我更改其中任意一项值A[i],那么C[i],C[i+1],C[i+2]....C[n]都会随之变化,所以我们要调整C[]当n非常大时就需要很长的时间,必然会导致超时。这样就可以引入树状数组,它的修改和求和的复杂度为nlogn.
树状数组是一种数组结构,能够高效地获取数组中连续n个数的和。
下面这张图是基本上大多数博客都有的一张图,主要为了让我们更容易理解树状数组。
红色的C[i]表示树状数组,且C[i] = A[i–2^k+ 1] +A[i-2^k+2] … + A[i] (i>=1)
其中,k为i在二进制下末尾0的个数,(可以利用位运算2^k=i&(i^(i-1))=i&(-i)),则我们称C为树状数组。由树状数组的定义可知:
C[1]=A[1]; C[2]=A[1]+A[2]=C[1]+A[2]; C[3]=A[3]; C[4]=A[1]+A[2]+A[3]+A[4]=C[2]+C[3]+A[4];
C[5]=A[5]; C[6]=A[5]+A[6]=C[5]+C[6]; C[7]=A[7]; C[8]=...=C[4]+C[6]+C[7]+A[8];
由图可知,k为这颗树的高度,且最高不会超过logn,这个时候我们改变A[i]的值后可以由C[i]往根节点上溯,调整这条路上的C[]即可。比方说改变A[1],即pos=1的值,需要更新的值为C[1],C[2],C[4],C[8],位置变化的规律为:pos+=LowBit(pos),复杂度为logn。模板为:
#define LowBit(num) num&(-num)
void Add(int pos,int value)
{ for(int i=pos;i<MAX;i+=LowBit(i))
C[i]+=value;
}
要求统计某一个位置num的前num项和时:只要找到num前的所有的最大子树,然后把其根节点的C[]加起来即可。如:要求前5项的和,用Sum(5)表示,则Sum(5)=C[5]+C[4],需要加起来的C[i]中的i也有规律可循,i-=LowBit(i),模板为:
int Add(int num)
{ int sum=0;
for(int i=num;i>0;i-=LowBit(i))
sum+=C[i];
return sum;
}
树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。
用法1:树状数组---插点问线,当只有某一点的数值变化,而查找的区间为一段时用这个方法。
直接上题目吧,例题1:HDU 1166(敌兵布阵)直接用暴力的话肯定会超时。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX=1000010;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,total,value,C[MAX];
char Find[10];
int LowBit(int num)//二进制后面为0的个数
{ return num&(-num);
}
void Add(int pos,int val)//将pos位置的值+val
{ while(pos<=n)
{ C[pos]+=val;//则每个位置的C[]都要改变,看下图有哪些C[]改变了,比方我把A[4]+val,变化的C[]有C[4],C[8]..变化,C[5],C[6]..并未变化
pos+=LowBit(pos);//求出下一个要改变的位置,回溯到根节点
}
}
int Sum(int num)//求前num项的和
{ int sum=0;
while(num>0)
{ sum+=C[num];
num-=LowBit(num);//找到num前的所有最大子树
}
return sum;
}
int main()
{ int sum=1;
scanf("%d",&total);
while(total--)
{ CLR(C,0);//要记得初始化
printf("Case %d:\n",sum++);
scanf("%d",&n);
for(int i=1;i<=n;i++)//要从1开始!!!
{ scanf("%d",&value);
Add(i,value);
}
while(scanf("%s",Find))
{ int End,Start;
if(Find[0]=='E') break;
scanf("%d%d",&Start,&End);
if(Find[0]=='A') Add(Start,End);
else if(Find[0]=='S') Add(Start,-End);
else printf("%d\n",Sum(End)-Sum(Start-1));
}
}
return 0;
}
和这个题目一样的是:NYOJ 116(士兵杀敌)~~~~~~
用法2:插线问点,当有某一段的数值都有变化时,而查询只有一个位置时用插线问点。NYOJ 123(士兵杀敌4)
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=1000010;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,m,num,C[MAX];
char Find[10];
int LowBit(int num)
{ return num&(-num);
}
void Add(int pos,int val)//前n项每项增加val
{ while(pos>0)
{ C[pos]+=val;
pos-=LowBit(pos);
}
}
int Sum(int pos)
{ int sum=0;
while(pos<=n)
{ sum+=C[pos];
pos+=LowBit(pos);
}
return sum;
}
int main()
{ CLR(C,0);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{ scanf("%s",Find);
if(Find[0]=='A')
{ int Start,End;
scanf("%d%d%d",&Start,&End,&num);
/****下面两行Nice****/
Add(Start-1,-num);
Add(End,num);
/********************/
}
else
{ scanf("%d",&num);
printf("%d\n",Sum(num));
}
}
return 0;
}
用法3:多维树状数组,增加模板:
#define LowBit(num) num&(-num)
void Add(int x,int y,....,int val)
{ for(int i=x;i<MAX;i+=LowBit(i))
for(int j=y;j<MAX;j+=LowBit(j))
.......//是几维的就几层
C[i][j][...]+=val;
}
void Sum(int x,int y,.....)
{ int sum=0;
for(int i=x;i>0;i-=LowBit(i))
for(int j=y;j>0;j-=LowBit(j))
......
sum+=C[i][j][...];
return sum;
}
例题3:HDU 1892(See you~~),代码:
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=1010;
#define min(a,b) a<b?a:b
#define LowBit(num) num&(-num)
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int total,n,Fx,Fy,Tx,Ty,value,sum,Count=1,C[MAX][MAX];
void Add(int x,int y,int val)
{ for(int i=x;i<MAX;i+=LowBit(i))
for(int j=y;j<MAX;j+=LowBit(j))
C[i][j]+=val;
}
int Sum(int x,int y)//求从位置(1,1)到(x,y)的元素之和
{ int sum=0;
for(int i=x;i>0;i-=LowBit(i))
for(int j=y;j>0;j-=LowBit(j))
sum+=C[i][j];
return sum;
}
int main()
{ scanf("%d",&total);
while(total--)
{ CLR(C,0);
for(int i=1;i<MAX;i++)//记得要初始化为1
for(int j=1;j<MAX;j++)
Add(i,j,1);
printf("Case %d:\n",Count++);
scanf("%d",&n);
for(int i=0;i<n;i++)
{ char c[5];
scanf("%s",c);
switch(c[0])
{ case 'A':
scanf("%d%d%d",&Fx,&Fy,&value);//在(Fx,Fy)处增加value本书
Add(Fx+1,Fy+1,value);
break;
case 'D':
scanf("%d%d%d",&Fx,&Fy,&value);//在(Fx,Fy)处移除value本书,如果这里的书不足,则只移除原有的书
sum=Sum(Fx+1,Fy+1)+Sum(Fx,Fy)-Sum(Fx+1,Fy)-Sum(Fx,Fy+1);//求出(Fx+1,Fy+1)位置的书的数目,可以用容斥定理解决
value=min(value,sum);
Add(Fx+1,Fy+1,-value);
break;
case 'M':
scanf("%d%d%d%d%d",&Fx,&Fy,&Tx,&Ty,&value);//从(Fx,Fy)处移value本书到(Tx,Ty)处,同上面不足的话移除所有的书
sum=Sum(Fx+1,Fy+1)+Sum(Fx,Fy)-Sum(Fx+1,Fy)-Sum(Fx,Fy+1);
value=min(sum,value);
Add(Fx+1,Fy+1,-value);
Add(Tx+1,Ty+1,value);
break;
default:
scanf("%d%d%d%d",&Fx,&Fy,&Tx,&Ty);
if(Fx>Tx) swap(Fx,Tx);
if(Fy>Ty) swap(Fy,Ty);
sum=Sum(Tx+1,Ty+1)+Sum(Fx,Fy)-Sum(Fx,Ty+1)-Sum(Tx+1,Fy);
printf("%d\n",sum);
}
}
}
return 0;
}
求点(Fx+1,Fy+1)的书的数目:
可知:由面积关系可知,B点的值为:Sum(Fx+1,Fy+1)+Sum(Fx,Fy)-Sum(Fx+1,Fy)-Sum(Fx,Fy+1);
最后的求(Fx,Fy)到(Tx+1,Ty+1)的元素之和同样可求,即求ABCD的面积,只是C(Fx,Fy),B(Tx+1,Ty+1),A(Fx,Ty+1),D(,Tx+1,Fy),这个时候面积为:
Sum(Tx+1,Ty+1)+Sum(Fx,Fy)-Sum(Fx,Ty+1)-Sum(Tx+1,Fy)。
三维树状数组:关键是点的值的变化,如下图,只标了几个点的坐标,O不一定指原点,只是为了立体感强点。
例题4:HDU 3584(Cube)
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=110;
#define LowBit(num) num&(-num)
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,m,C[MAX][MAX][MAX];
void Add(int x,int y,int z,int val)
{ for(int i=x;i<=n;i+=LowBit(i))
for(int j=y;j<=n;j+=LowBit(j))
for(int k=z;k<=n;k+=LowBit(k))
C[i][j][k]+=val;
}
int Sum(int x,int y,int z)
{ int sum=0;
for(int i=x;i>0;i-=LowBit(i))
for(int j=y;j>0;j-=LowBit(j))
for(int k=z;k>0;k-=LowBit(k))
sum+=C[i][j][k];
return sum;
}
int main()
{ while(scanf("%d%d",&n,&m)!=EOF)
{ CLR(C,0);
for(int i=0;i<m;i++)
{ int comp,Sx,Sy,Sz,Ex,Ey,Ez;
scanf("%d",&comp);
switch(comp)
{ case 0:
scanf("%d%d%d",&Sx,&Sy,&Sz);
printf("%d\n",Sum(Sx,Sy,Sz)&1);
break;
default:
scanf("%d%d%d%d%d%d",&Sx,&Sy,&Sz,&Ex,&Ey,&Ez);
//变成立方体了~~~~
Add(Sx,Sy,Sz,1);
Add(Sx,Ey+1,Ez+1,1);
Add(Ex+1,Sy,Ez+1,1);
Add(Ex+1,Ey+1,Sz,1);
Add(Ex+1,Sy,Sz,1);
Add(Sx,Ey+1,Sz,1);
Add(Sx,Sy,Ez+1,1);
Add(Ex+1,Ey+1,Ez+1,1);
}
}
}
return 0;
}
涉及到树状数组的题目有:
HDOJ 1116,1394,1541,1556,1640,1867,2227,,2275,2430,2492,2642,2668,2689,2838,2852,3047,3450,3743,3887,4031,
POJ1195,1990,2029,2155,2299,2352,2464,2481,3067,3321,3468,