树状数组的概念:
树状数组(Fenwick Tree,又称二叉索引树)是一个查询和修改复杂度都为log(n)的数据结构,它是利用二进制的一些特点来实现。它的功能有局限性,主要是用来动态查询连续和(或者是前缀和)的问题。它利用O(n)的附加空间复杂度,将线性的数列结构转化成树状结构从而进行跨越扫描,高效完成查询连续和。
原数组为a[ ],用树状数组储存后的数组为 C[ ]
来一张百度百科里的图片先找一下规律体会一下
模板函数:
int lowbit(int x)//函数作用:获取x二进制中从右往左数第一个1所代表的的十进制数。//比如1110,变为0010,值为2
{
return x&(-x);//x&(x^(x-1))
}
上述函数作用:x节点最近的父亲的编号=x+lowbit(x)。x最近前驱的编号=x-lowbit(x);//当我们求1->x的和时,C[x]如果包含的不是1-->x的全部和时(客观规律是只有x为2^k(k=0,2,3.....)时,C[x]才包含前x项的和),比如求1到6的和sum(6),肯定有C[6],C[6]=a[6]+C[5],还需要谁?由上图易得还需要C[4]。4怎么来的呢?4=6-lowbit(6);而4-lowbit(4)=0表示已经得到所有和了(4为2^2,C[4]表示之前所有和)……这种点不妨称之为x的前驱节点(可能有若干个)
固得sum(6)=C[6]+C[4].于是就延伸出下述模板
模板函数
int ask(int x)//函数作用:获取1->x的和
{
int sum=0;
for(;x>0;x-=lowbit(x))
{
sum+=C[x];
}
return sum;
}
模板函数
//函数作用:由于某个值改变,需要更新树状数组
void add(int x,int num)//在x处增加num
{
for(;x<=N;x+=lowbit(x))//N为树状数组的上界
C[x]+=num;
}
模板函数
//树状数组初始化
//1、
for(int i=1;i<=n;++i)//函数作用,将a[]-->C[]
{
scanf("%d",&a[i]);
if(i&1) C[i]=a[i];//客观规律吧,写写就明白了
else {
C[i]=a[i];
int f=lowbit(i);
f>>=1;
while(f){
C[i]+=C[i-f];
f>>=1;
}
}
}
//上述是我发现的一个规律,其实是麻烦了,但感觉上面比下面的高效吧
2、
//其实只要add(i,a[i])就是把a[]->C[],是我理解的不够
for(int i=1;i<=n;++i)
{
add(i,a[i]);
}
//3、
void init(){//线性构造
for (int i = 1; i <= n; i++){
pre[i] = pre[i - 1] + a[i];//前缀和
c[i] = pre[i] - pre[i - lowbit(i)];
}
}
最后是一道模板题体会一下 树状数组模板题
PLUS
求区间最大值
a[ ]原数组的值,C[ ]区间最大值
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int num)
{
a[x]=num;
int idx;
for(;x<=n;x+=lowbit(x))
{
C[x] = a[x];
idx = lowbit(x);
for (int i=1; i<idx; i<<=1)
h[x] = max(h[x], h[x-i]);
}
}
int getmax(int x, int y)
{
int ans = 0;
while (y >= x)
{
ans = max(a[y], ans);
y --;
for (; y-lowbit(y) >= x; y -= lowbit(y))
ans = max(h[y], ans);
}
return ans;
}
离散化:
关于离散化,这篇文章讲的很好——>传送
模板:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100000;
int a[maxn];
int b[maxn];
int n;
int main()
{
int m=0;
cin>>n;//元素个数
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]);
b[i]=a[i];//b[]作为离散化的数组
}
sort(b+1,b+1+n);//将b数组排序,因为是从b[1]开始存储的,所以要b+1
m=unique(b+1,b+1+n)-b-1;//去重操作,返回不同元素的个数
for(int i=1;i<=n;++i)
a[i] = lower_bound(b+1,b+1+m,a[i]) - b;
return 0;
}
根据原值查询(映射)
//原arr数组存值
memcpy(num,arr,sizeof(arr));
sort(num+1,num+cnt+1);
int Size= unique(num+1,num+cnt+1) - num-1;
int query(int x,int Size)
{
return lower_bound(num+1,num+Size+1,x) - num;
}
树状数组的本质操作就是维护区间的前缀和
1、区间修改,单点查询————树状数组维护差分数组
树状数组只能查询前缀和 和 单点修改,我们可以通过设立一个差分数组d[ ],并用树状数组维护,那么区间修改就变为了单点修改。如在[l,r]的区间内增加一个k,那么add(l,k),add(r+1,-k)。这就完成了差分数组的维护,若单点查询x处 的值,只需a[x] + ask(x)。
2、区间修改,区间查询————两个树状数组维护(可以用来完成动态区间 加 等差数列的维护)
由上面我们得知,用树状数组进行区间修改就是维护一个差分数组d,对于x位置处增加的值就是ask(x),即,那么对于原数组a[ ],数组a的前缀和S(x) 总体增加的值就是
即我们可以用两个树状数组维护上述两个前缀和。。(其中前一个是差分数组,后面是i*差分数组)
具体的说,我们建立两个树状数组c0,c1,起初全部赋值为0.对于每条指令 C l r d 执行四个操作
1、在树状数组c0中把位置 l 上的加上d
2、在树状数组c0中,把位置r+1 上的位置减去d
3、在树状数组c1中,在位置l 上加上l * d
4、在树状数组c1中,在位置r+1上的数减去(r+1)*d
另外,我们建立数组S存储a的原始前缀和。所以对于每条指令 Q l r为:
虽然这样区间修改区间维护这个做法比较麻烦,不如直接用线段树,不过,有时候动态区间加等差数列会产生比较巧妙的用处。
附上算法竞赛进阶指南上的代码
const int SIZE = 100010;
int a[SIZE],n,m;
ll c[2][SIZE],S[SIZE];
int lowbit(int x){return x&(-x);}
ll ask(int k,int x){
ll ans = 0;
for(;x > 0;x -= lowbit(x)) ans += c[k][x];
return ans;
}
void add(int k,int x,int y){
for(;x <= n;x += lowbit(x)) c[k][x] += y;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= n;++i) scanf("%d",&a[i]),S[i] = S[i-1] + a[i];
while(m --){
char op[3];int l,r,d;
scanf("%s%d%d",op,&l,&r);
if(op[0] == 'C'){
scanf("%d",&d);
add(0,l,d);add(0,r+1,-d);
add(1,l,l*d);add(1,r+1,-(r+1)*d);
}
else {
ll ans = S[r] + (r+1)*ask(0,r) - ask(1,r);
ans -= S[l-1] + l *ask(0,l-1) - ask(1,l-1);
printf("%lld\n",ans);
}
}
}