树状数组,又称二进制索引树,英文名Binary Indexed Tree
之前遇到一个求逆序对的题,看了很多题解都只说了这个树状数组,关于怎么实现的全都避而不谈,我研究了一下午,总算搞出个头绪了
一般用来求前缀和,可以把时间复杂度从O(n)降到O(log10 n)非常恐怖,举个例子,假如我们要求从1~1000的前缀和,普通方法需要遍历1000次,而树状数组只需要遍历5次,
先上图;
又称二进制索引树的几个特点:
图中每一个非叶子节点都代表一个区间的和,
a[4]代表的是a[1]+a[2]+a[3]+a[4],
a[5]就是自己a[5]
例如我们需要求1~11的和,我们就只需要求出a[11] ,a[10] ,a[8]的和,
同样的,假如求1~1000的和,那么只需要求a[1000] ,a[992] ,a[960], a[896], a[768],a[512]的和,
可以看出每次最后的一个结点必定是2的次方
因为只需要跳到最外面的一个结点就不需要在往下求了,而最外面的结点一定是2的n次方,不懂得可以根据那个图模拟一下。
lowbit:
int lowbit(int x)
{
return x&-x;
}
相信很多童鞋看到这个东西都是一脸懵逼,而我就不一样了,我TM是十脸懵逼,那让我们来推测一下:
首先根据英语意思理解,low就是低,bit就是位,连起来就是最低位,其实就是求的一个数的二进制的第一个1的位置,
比如6,它的二进制:0110,从右往左第一个1的位置是1(从零开始),那么lowbit(6)就等于2的1次方就等于2,
再比如9,它的二进制:1001,从右往左第一个1的位置是0,那么lowbit(6)就等于2的0次方就等于1,
我们再来看一下实现原理:
x&-x到底是个什么玩意
要知道,二进制数的负数是正数取反加一,那么-6就是1010,-6&6 就是 1010 & 0110==0010,换算成十进制就是2,所以lowbit(6)==2
现在实现原理知道了,那么这玩意跟树状数组有什么关系,请听下回讲解:
lowbit(x)是用来跳到x的下一个结点或者上一个结点的的介质,
比如lowbit(6)==2,那么上一个结点就是6-lowbit(6)==4,下一个结点是是6+lowbit(6)==8,可以看一下上面的那个图
这里说的上一个结点不是父节点,而是你要求的前缀和的管辖区间的结点,比如我们要求1~6的和:
此时sum==[1]+a[2]+a[3]+a[4]+a[5]+a[6],是不是很完美
如果我们要更新某一个点的值,那么所有把这个点包含在内的区间结点都需要更新,比如我们要使a[6]+1:
由于这个树是没有上界的,题目一般都会给出一个序列的长度为n
是不是感觉很强大,我只能这样说明它,不懂怎么证明它,感觉发明这些算法的人简直是个天才,正所谓那句话:社会是由百分之一的天才创造的,百分之九十九的人推动的,
关于为什么只能求前缀和,不能求区间和,可以仔细看一下上图很快就会明白,如果一旦到最外面的结点(2的n次方),那么x必定等于lowbit(x),x-lowbit(x)必定等于0,就会跳出累加,而最外面的结点x,一定代表的是1~x的和,就是前缀和,如果求区间和只能拿右边界的前缀和减去左边界的前缀和了
但是这个结构还有一个缺点,就是a[8]代表的是a[1~8]的和,所以我们并不知道a[8]具体是多少,要避免这种情况,就需要另外开一个与原序列长度相同的数组 t,用来存放树,原数组不动,
根据上图,a数组是最下面的一排叶子结点,用 t 数组来存储除叶子结点的树,
细心的同学可以发现,树结点的序号都是偶数,奇数的叶子结点都是一个单独的区间
如果要查询区间和的话,只需要判断t中的这个结点是不是偶数或者是不是等于0,这里就不再bb了
逆序对的定义:i
要求一个数组的逆序对,可以用归并排序的概念加一个累加就行,具体看这个例子https://blog.csdn.net/qq_41431457/article/details/88944840,我们这里主要介绍的是树状数组求逆序对
首先明白一个概念叫做离散化(Discretization)
在上面介绍的树状数组中,只需要开一个与原序列中最大元素相等的长度数组就行,那么如果我的序列是1,5,3,8,999,本来5个元素,却需要开到999这么大,造成了巨大的空间浪费,
离散化就是另开一个数组,d, d[i]用来存放第i大的数在原序列的什么位置,比如原序列a={5,3,4,2,1},第一大就是5,他在a中的位是1,所以d[1]=1,同理d[2]=3,········所以d数组为{1,3,2,4,5},
转换之后,空间复杂度就没这么高了,但不是求d中的逆序对了,而是求d中的正序对,来看一下怎么求的:
最后算出来,总共有9个逆序对,可以手算一下原序列a,也是9个逆序对,
具体实现:
实现代码:
#include
#define M 500005
using namespace std;
int a[M],d[M],t[M],n;
//原数组/ 离散化后的数组/ 树状数组
bool cmp(int x,int y)
{
if(a[x]==a[y]) return x>y;//避免元素相同
return a[x]>a[y];//按照原序列第几大排列
}
int mian()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i],d[i]=i;//初始化
sort(d+1,d+n+1,cmp);
//排序时候d就是离散化的数组了
return 0;
}
离散化之后,就是求和了,
根据上面的步骤每一次把一个新的数x放进去之后,都要求比他小的元素有几个,而比他小的元素个数一定是1到x中存在数的个数,也就是[1 , x-1]中有几个数,是不是很耳熟,有点像之前讲的前缀和了,只不过树状数组t表是的不是前缀和了,t[x]表示的是[1,x]中有几个数已经存在,这样我们每次把一个新的数x放进去的时候,都需要把包含这个数的结点更新,然后查询[1,x-1]有几个数已经存在。
还是拿上一个例子:
最后答案就出来了,关键是要理解那句标了红色的那句话,不是前缀和,而是有几个数已经存在,假如a[8]等于4,那么就表示[1,8]中只有4个数存在。
完整代码:
#include
#define M 500005
using namespace std;
int a[M],d[M],t[M],n;
int lowbit(int x)
{
return x&-x;
}
int add(int x)//把包含这个数的结点都更新
{
while(x<=n)//范围
{
t[x]++;
x+=lowbit(x);
}
}
int sum(int x)//查询1~X有几个数加进去了
{
int res=0;
while(x>=1)
{
res+=t[x];
x-=lowbit(x);
}
return res;
}
bool cmp(int x,int y)//离散化比较函数
{
if(a[x]==a[y]) return x>y;//避免元素相同
return a[x]>a[y];//按照原序列第几大排列
}
int main()//402002139
{
//freopen("in.txt","r",stdin);
long long ans=0;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i],d[i]=i;
sort(d+1,d+n+1,cmp);//离散化
for(int i=1;i<=n;i++)
{
add(d[i]);//把这个数放进去
ans+=sum(d[i]-1);//累加
}
cout<
求前缀和版本:
#include
#define lowbit(x) (x&-x)
using namespace std;
const int N = 1000;
int t[N<<2], n;
void add(int x, int id)
{
for(int i = id; i <= n; i += lowbit(i))
t[i] += x;
}
int sum(int id)
{
int res = 0;
for(int i = id; i > 0; i-= lowbit(i))
res += t[i];
return res;
}
int main()
{
n = 10;
for(int i = 1; i <= 10; i++)
{
int x;
cin >> x;
add(x, i);
}
while(1)
{
int x;
cin >> x;
cout << sum(x) << endl;
}
return 0;
}
不要以为那个while循环会执行很多次,就算x==1000,也只会遍历5次!!!,
天才创造了世界~
码字不易,点个赞再走呗