树状数组
模板代码:
#define lowbit(x) ((x)&-(x))
void add(int x,int d){//更新数组tree[],修改所有和a[x]有关的tree[]
while(x<=n){
tree[x]+=d;
x+=lowbit(x);
}
}
int sum(int x){//求和:返回a[1]+a[2]+a[3]...a[x]
int sum=0;
while(x>0){
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
模板讲解:
树状数组是一种用数的二进制特征进行检索的树状结构。代码简洁,高效。
树状数组的核心为lowbit(x)操作。他的功能就是找到x得二进制数的最后一个1。原理是利用负数的补码表示,补码是原码取反加1。比如x=6=000001102,-x=x补=111110102,那么lowbit(6)=102=2。这里给出1-9 lowbit()结果:
x | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
x的二进制 | 1 | 10 | 11 | 100 | 101 | 110 | 111 | 1000 | 1001 |
lowbit(x) | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 8 | 1 |
tree[x]等于 | a1 | a1+a2 | a3 | a1+a2+a3+a4 | a5 | a5+a6 | a7 | a1+a2+a3+a4+a5+a6+a7+a8 | a9 |
注意:tree数组下标是从1开始的,有的题需要转换。
其实可以 看到lowbit的操作就是从右到左截取x的二进制数截取到第一个1所得的数。
然后tree[x]实际上就等于a[x]+a[x-1]+a[x-2]+…a[x-lowbit(x)]的和。
sum操作:
sum(x)操作是求a1+a2+a3+a4+a5+a6…ax,求和。比如我们想求sum(7),也就是前7项的和,那么可以由表中观察到sum(7)=tree[7]+tree[6]+tree[4[。同时可以看到关系就是tree[7]+tree[7-lowbit(7)=6]+tree[6-lowbit(6)=4],
最后4-lowbit(4)=0结束。
add操作:
add就是把ax加上d。比如我们想给a3加上2,那么tree[3]、tree[4]、tree[8]都会相应的加上2。这个操作也用到了lowbit(x)。
首先给tree[3]加上2。
3+lowbit(3)=4,tree[4]加上2。
4+lowbit(8),tree[8]加上2。
继续直到最后的tree[n]。
时间复杂度
sum、add操作都非常快。时间复杂度都是log(n)。
模板使用:
以Lost Cows POJ - 2182 为例子,讲解模板使用。
题目链接
题目大意:
有编号1-n的n个数字,2<=n<=8000,乱序排列,顺序是未知的。对于每个位置的数
字,我们只知道每个位置前面有几个比他小的数。请你根据给出的信息计算出每个
位置上的数是多少。
样例输入:
5 1 2 1 0
输出:2 4 5 3 1
讲解
这里在n个位置上,每个位置都有一头牛。即a1=a2=a3=a4=a6=a7…an=1。这道题有点特殊,不需要使用a[]也可以做。
tree[]的初始化。tree[x]的初始化就是lowbit (x)。因为a[]数组的每个数都为1。
从后往前。对每个pre[i]+1,找出sum(x)=pre[i]+1所对应的x,就是第x头牛。在找到x头牛之后,让ax=0,也就是add(x,-1)。
下面代码完全套用了上面模板:
#include<iostream>
#include<algorithm>
#include<math.h>
#include<cstdio>
#include<string>
#include<string.h>
#include<list>
#include<queue>
#include<sstream>
#include<vector>
#include<set>
#include<map>
#include<deque>
#include<stack>
using namespace std;
#define debug(x) cout<<"###"<<x<<"###"<<endl;
const int INF=0x3f3f3f3f,mod=1e9,Maxn=1e4+10;
typedef long long ll;
int n,a[Maxn],b[Maxn],tree[Maxn];
#define lowbit(x) ((x)&-(x))
void add(int x,int d){
while(x<=n){
tree[x]+=d;
x+=lowbit(x);
}
}
int sum(int x){
int sum=0;
while(x>0){
sum+=tree[x];
x-=lowbit(x);
}
return sum;
}
int findpos(int x){//二分查找找到的那个位置就是所应该的位置
int l=1,r=n;
while(l<r){
int m=l+(r-l)/2;
if(sum(m)<x){
l=m+1;
}
else{
r=m;
}
}
return l;
}
int main(){
scanf("%d",&n);
a[1]=0;
for(int i=2;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=n;i++){
tree[i]=lowbit(i);
}//初始化
for(int i=n;i>=1;i--){
int x=findpos(a[i]+1);
add(x,-1);
b[i]=x;
}
for(int i=1;i<=n;i++){
printf("%d\n",b[i]);
}
return 0;
}
树状数组和线段树的对比
树状数组和线段数比起来树状数组代码更简洁,所需要的空间有时也比线段树少(线段树需要4倍空间)。但是可以用树状数组做的题,用线段树一定可以做。