前言:早在知乎看到某位巨巨回答最优美的数据结构:树状数组,
原因是:实现简单,代码优雅,效果拔群。
这么优美的数据结构,怎么能不来一个总结?
树状数组的作用:频繁对于单点和区间修改和查询操作,时间复杂度都是log(n).
关于原理神马,各种神牛博客都有,我就不多加解释.
这里就发一些题解,作为新手入门,大牛可以忽略.
主要操作:
1.追溯其父节点或下辖第一个没有关系的点:
(所有操作都是围绕这个)
int lowbit (int x)
{
return x&(-x);
}
2.修改元素的值
void update(int i,int x)
{
while(i<=n){
tree[i]+=x;
i+=lowbit(i);
}
}
3.求数组前n项和
int Querty(int x){
int sum=0;
while(x>0){
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
常见题目类型:
1.单点更新与区间求和
hdu1166 敌兵布阵
这题典型的树状数组,各种操作都是模板。
#include<bits/stdc++.h>
using namespace std;
int c[50010],n;
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int num)
{
while(x<=n)
{
c[x]+=num;
x+=lowbit(x);
}
}
int getsum(int x)
{
int s=0;
while(x>0)
{
s+=c[x];
x-=lowbit(x);
}
return s;
}
int main()
{
int T;
scanf("%d",&T);
for(int cur=1;cur<=T;cur++)
{
memset(c,0,sizeof(c));
printf("Case %d:\n",cur);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
update(i,x);
}
char s[10];int a,b;
//scanf("%s",s);
while(scanf("%s",s)!=EOF&&strcmp(s,"End"))
{
scanf("%d %d",&a,&b);
if(s[0]=='Q')
printf("%d\n",getsum(b)-getsum(a-1));
else if(s[0]=='A')
update(a,b);
else
update(a,-b);
}
}
return 0;
}
2.区间更新,单点求值。
hdu1556 color the ball
在单点更新里面,树状数组求和操作代表区间的和,而在区间更新中,树状数组求和操作代表单个元素的变化量。
所以这题比如对(4,6)区间进行更新,就是update(4,1),update(6+1,-1),
Query(i)代表查询第i个数变化量。
#include<bits/stdc++.h>
using namespace std;
int tree[100010],n;
void init()
{
memset(tree,0,sizeof(tree));
}
int lowbit(int x)
{
return x&(-x);
}
void update(int x,int num)
{
while(x<=n)
{
tree[x]+=num;
x+=lowbit(x);
}
}
int getsum(int x)
{
int sum=0;
while(x>0)
{
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
while(~scanf("%d",&n)&&n)
{
init();
for(int i=1;i<=n;i++)
{
int a,b;
scanf("%d %d",&a,&b);
update(a,1);
update(b+1,-1);
}
for(int i=1;i<=n-1;i++)
printf("%d ",getsum(i));
printf("%d\n",getsum(n));
for(int i=1;i<=n;i++) cout<<tree[i]<<endl;
}
}
3.逆序数
在一个排列里,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么我们就称它为一对逆序数。树状数组可以求一个排列里逆序数的对数。
hdu 2689 Sort it
题意:把一个具有n个不同元素的序列,只能交换两个相邻的位置的元素,问最少交换多少次变成升序序列。
思路:对于当前大小为a的第i个元素,他需要交换的次数为前面比它大的数的个数,那么整个序列需要交换次数为逆序对数。
逆序数求法:tree[i]值为0或1表示i这个数出现的是否出现,
#include<bits/stdc++.h>
using namespace std;
int a[1010],n;
int lowbit(int x)
{
return x&(-x);
}
void update(int i,int x)
{
while(i<=n){
a[i]+=x;
i+=lowbit(i);
}
}
int Query(int x)
{
int sum=0;
while(x>0){
sum+=a[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
while(cin>>n){
memset(a,0,sizeof(a));
int ans=0;
for(int i=1;i<=n;i++){
int x;cin>>x;
update(x,1);
ans+=i-Query(x);
}
cout<<ans<<endl;
}
}
变形一下的逆序数:
hdu2838 Cow Sorting
求一个序列里面所有逆序数对之和,对于序列大小为a的第i个数,除了知道他前面有多少个比他小的数,还要知道他们的和,所以这里需要结构体记录个数,和之和两个信息。
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=100000+10;
int n;
struct node
{
int cnt;
long long sum;
}tree[N];
int lowbit(int x) {return x&(-x);}
void update(int x,int v,int cnt){
while(x<=n){
tree[x].cnt+=1;
tree[x].sum+=v;
x+=lowbit(x);
}
}
long long get_cnt(int x){
long long sum=0;
while(x>0){
sum+=tree[x].cnt;
x-=lowbit(x);
}
return sum;
}
long long get_sum(int x){
long long sum=0;
while(x>0){
sum+=tree[x].sum;
x-=lowbit(x);
}
return sum;
}
int main()
{
while(~scanf("%d",&n)){
memset(tree,0,sizeof(tree));
long long sum=0;
for(int i=1;i<=n;i++){
int a;scanf("%d",&a);
update(a,a,1);
int t=i-get_cnt(a);
//cout<<"t="<<t<<endl;
if(t!=0){
sum+=((long long)t*a+get_sum(n)-get_sum(a));
}
}
cout<<sum<<endl;
}
}
4.多维树状数组。
二维树状数组:hdu2642 Stars
题意:在一个平面区域里统计出现星星个数。
思路:多维树状数组在统计区域值得时候,通常需要容斥原理求得特定区域的信息,这题也是.
注意这题星星可能多次点亮操作,所有需要一个标记数组。
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1000;
int tree[N+10][N+10];
bool mark[N+10][N+10];
int lowbit(int x){return x&(-x);}
void update(int x,int y,int num){
for(int i=x;i<=N;i+=lowbit(i)){
for(int j=y;j<=N;j+=lowbit(j)){
tree[i][j]+=num;
}
}
}
int Query(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+=tree[i][j];
}
}
return sum;
}
int main()
{
int n;
while(~scanf("%d",&n)){
memset(tree,0,sizeof(tree));
memset(mark,false,sizeof(mark));
for(int i=0;i<n;i++){
char ch;scanf(" %c",&ch);
if(ch=='B'){
int x,y;scanf("%d%d",&x,&y);
x++,y++;
if(!mark[x][y]) update(x,y,1),mark[x][y]=true;
}
else if(ch=='Q'){
int x1,y1,x2,y2;scanf("%d%d%d%d",&x1,&x2,&y1,&y2);
x1++;y1++;x2++;y2++;
if(x1<x2) swap(x1,x2);
if(y1<y2) swap(y1,y2);
printf("%d\n",Query(x1,y1)-Query(x1,y2-1)-Query(x2-1,y1)+Query(x2-1,y2-1));
}
else if(ch=='D'){
int x,y;scanf("%d%d",&x,&y);
x++;y++;
if(mark[x][y]) update(x,y,-1),mark[x][y]=false;
}
}
}
}
三维树状数组:hdu3584
这题在三维空间,并且是区域更新,点查询。
然后区域更新的时候运用容斥原理(找规律的排列一下参数)。
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int tree[N][N][N];
int lowbit(int x){return x&(-x);}
void update(int x,int y,int z){
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)){
tree[i][j][k]=(tree[i][j][k]== 0 ? 1 : 0);
}
}
}
}
int Query(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+=tree[i][j][k];
}
}
}
return sum;
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m)){
memset(tree,0,sizeof(tree));
for(int i=0;i<m;i++){
int a[4];cin>>a[3]>>a[0]>>a[1]>>a[2];
if(a[3]==1){
int b[3];cin>>b[0]>>b[1]>>b[2];
update(a[0],a[1],a[2]);
update(a[0],a[1],b[2]+1);
update(a[0],b[1]+1,a[2]);
update(b[0]+1,a[1],a[2]);
update(a[0],b[1]+1,b[2]+1);
update(b[0]+1,a[1],b[2]+1);
update(b[0]+1,b[1]+1,a[2]);
update(b[0]+1,b[1]+1,b[2]+1);
}
else{
printf("%d\n",Query(a[0],a[1],a[2])%2);
}
}
}
}
另外常见出题形式:
1.离散化化树状数:
poj2299
思路:常见 题目数据多大,树状数组就开多大,然而有的题单个数据很大,但总共个数不大,我们就可以对其离散化。比如这题:数据范围999999999,n范围十万.求逆序数对数。
离散化步骤:存最初每个数的顺序,按照大小排序,后,按照排序后的顺序和原来位置重新赋值。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define N 500001
struct node
{
int v,pos;
}a[N];
int tree[N],c[N];
bool cmp(node a,node b){ return a.v<b.v;}
int lowbit(int x) {return x&(-x);}
void update(int x){
while(x<=N){
tree[x]+=1;
x+=lowbit(x);
}
}
long long get_sum(int x)
{
long long sum=0;
while(x>0){
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
int main()
{
int n;
while(scanf("%d",&n)&&n){
for(int i=1;i<=n;i++){
scanf("%d",&a[i].v);
a[i].pos=i;
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++) c[a[i].pos]=i;
memset(tree,0,sizeof(tree));
long long ans=0;
for(int i=1;i<=n;i++){
update(c[i]);
ans+=(i-get_sum(c[i]));
}
printf("%lld\n",ans);
}
}
2.求区间最值问题:
当然区间最值问题,最优做法是线段树,灵活,信息量大。但是树状数组代码更少不是吗?
常见借用树状数组解决区间最值问题有三种方法:
1.用二进制表示:
转自:http://www.cnblogs.com/ambition/archive/2011/04/06/bit_rmq.html
#include <iostream>
using namespace std;
#define maxn 1<<20
int n,k;
int c[maxn];
int lowbit(int x){
return x&-x;
}
void insert(int x,int t){
while(x<maxn){
c[x]+=t;
x+=lowbit(x);
}
}
int find(int k){
int cnt=0,ans=0;
for(int i=20;i>=0;i--){
ans+=(1<<i);
if(ans>=maxn || cnt+c[ans]>=k)ans-=(1<<i);
else cnt+=c[ans];
}
return ans+1;
}
void input(){
memset(c,0,sizeof(c));
int t;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++){
scanf("%d",&t);
insert(t,1);
}
printf("%d\n",find(k));
}
int main(){
int cases;
scanf("%d",&cases);
while(cases--){
input();
}
return 0;
}
2.区间划分
转自:http://www.cnblogs.com/ambition/archive/2011/04/06/bit_rmq.html
3.二分枚举:
hdu2852
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100005;
int tree[N+10];
int lowbit(int x) {return x&(-x);}
void add(int i,int x)
{
while(i<=N){
tree[i]+=x;
i+=lowbit(i);
}
}
int get_sum(int i)
{
int sum=0;
while(i>0){
sum+=tree[i];
i-=lowbit(i);
}
return sum;
}
int binary(int pos,int k)
{
int l=pos+1,r=N,x=get_sum(pos),mid=N,ans=N;
while(l<=r){
mid=(l+r)*0.5;
if(get_sum(mid)-x>=k){
r=mid-1;
ans=mid;//这里注意,(l+r)/0.5取得是下限,比如正确结果是4,l=3,r=4时,虽然进不来,mid会变成3.所以结果保存能满足的mid.
}
else l=mid+1;
}
return ans;
}
int main()
{
int n;
while(~scanf("%d",&n)){
memset(tree,0,sizeof(tree));
for(int i=0;i<n;i++){
int p;scanf("%d",&p);
switch(p){
case 0:{
int e;scanf("%d",&e);
add(e,1);
break;
}
case 1:{
int e;scanf("%d",&e);
if(get_sum(e)-get_sum(e-1)>0) add(e,-1);
else printf("No Elment!\n");
break;
}
case 2:{
int a,k;scanf("%d%d",&a,&k);
int ans=binary(a,k);
if(ans==N) printf("Not Find!\n");
else printf("%d\n",ans);
break;
}
}
}
}
}