引入
树状数组,顾名思义,像树一样结构的数。那它的作用是什么呢?
我们知道前缀和,是一种非常高效且巧妙的思想,查询一个序列中的某一区间和[l,r],那么我们将可以预处理出[1,l-1],[1,r]的和,那么一相减便就得到[l,r]之间的和了。but,这只支持离线操作,如果我要不断的询问和修改的话,那么时间复杂度每次修改最坏就是O(n),所以就不行了,所以我们要引入了新的支持在线查询的东东——树状数组。它能以O(logn)的复杂度完成更新和查询。
PS:树状数组能解决的问题,线段树都能解决,但有些问题线段树才能解决,但是但是树状数组代码量是真的小,写起来真爽:D,但是线段树理解起来比树状数组简单一点。
原理
树状数组
是不是很像一棵快乐的树啊.我们先不管为什么是这样的储存。
来看看它是如何求出区间和的:
假如我要查询a[5~7]区间和,那么我们要求出前缀和sum[4],sum[7],那么到底哪里是sum[4],sum[7]呢?
我们已知c[4]是存的a[1]+a[2]+a[3]的和,那么前缀和已经求出了,我们在看看sum[7],c[7]只存储了a[7],c[6]储存了a[5],a[6],c[4]储存了a[1~4],所以sum[7]=c[7]+c[6]+c[4],也就是它的左边兄弟的和。
所以最后区间[5~7]已经非常明显了。
深入了解
我们要知道是如何储存的这些数字呢?
还是上面的图,我们发现c[1,3,5,7,9]高度看做0,而a[2,6]为1,a[4]为2,a[8]为3。正好就去连了多少个除它以外的直接的儿子个数。
我们将这些数转为2进制,你品,你细品。
1=1,2=10,3=11,4=100,5=101,6=110,7=111,8=1000,9=1001。
你看它们末尾0的个数就是不是对应了树的高度。
显然这和二进制有关,那末尾0的个数如何求呢?
这里直接给出答案,感兴趣也可了解包括(取反,按位与,补码)
inline int lowbit(int i)
{
return i&(-i);
}
通常我们用lowbit表示。
所以建立就是从该点开始,不断的累加lowbilt。而且到了2的倍数后,增长的飞快。
查询也差不多,不断的减去lowbit,并累加答案,而且如果到了2的倍数,计算发现直接跳到0.
所以说效率是极高的。
我们已经了解了建立和查询的原理了。
来,上代码.
代码
传送门luoguP3374
一道板子
#include<bits/stdc++.h>
using namespace std;
int n,m;
int tree[500005];
int lowbit(int i)
{
return i&(-i);
}
void Updata(int i,int x)
{
while(i<=n)
{
tree[i]+=x;
i+=lowbit(i);
}
}
int Search(int x)
{
int sum=0;
while(x>0)
{
sum+=tree[x]; //累加
x-=lowbit(x);//到它的左兄弟
}
return sum;
}
int main()
{
scanf("%d%d",&n,&m);
int x;
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
Updata(i,x); //更新
}
int k,y;
for(int i=1;i<=m;i++)
{
scanf("%d",&k);
if(k==1)
{
scanf("%d%d",&x,&y);
Updata(x,y); //更新
}
if(k==2)
{
scanf("%d%d",&x,&y);
cout<<Search(y)-Search(x-1)<<endl;//查询,前缀和思想
}
}
return 0;
}
就这样完成了在线更新和查询,代码量要比线段树少多了。
求逆序对
逆序对,一个序列中的位置i<j,对于的数字ai>aj,我们就称为一组逆序对,
求逆序对可以暴力的冒泡排序的方法,统计交换次数就是逆序对数,O(n方)
还可以用归并排序,O(nlogn)
下面,让我们了解树状数组求逆序对的原理。
我们知道在上面的题目中,树状数组所储存的是一个区间的和。
那么我们是否可以统计读入每一个数,查询比它小的数,然后去减一共加入的目前加入的数的个数最后把每次询问的结果一累加,不就是逆序对数了吗?
带着这个想法,我们来实现这个操作。
假设我们输入一串数字3,1,5,4
黑色数字就是代表这里出现了一次3
inline void update(int i)
{
while(i<=n)
{
tree[i]++;
i+=lowbit(i);
}
}
然后每次来一个数我们就查询一次比它小的数
inline int query(int i)
{
int sum=0;
while(i>0)
{
sum+=tree[i];
i-=lowbit(i);
}
return sum;
}
//int main中
ans+=i-query(x);
//i是目前一共加入了多少数,
x是当前更新的
好了这些我们就能求出逆序对了。
下面是图解:
这就是大部分树状数组的基础内容了,
下一次将会介绍一些较有意思的题目。