[知识点]树状数组(附逆序对)
【20210903 谢谢大家的指正,博主在忙着考研暂时没时间打理陈年旧事,待尘埃落定后再来补缺勘误】
1、前言
这么简单的东西一直没有来看一眼。。。因为最初学数据结构的时候就曾从各方各面了解到线段树的各种优越性,各种比树状数组好,于是就看了线段树就没管了。。。但是树状数组的常数小,代码短这些隐性优势也许当时并不清楚吧。
2、概念
树状数组,依旧是一个线性数组构成,但是其性质却如同树结构一样是立体的。如图所示,我们首先给出一个最基本的a数组的一部分,大家可以观察一下上方的c数组有什么规律?
我们将箭头看做该节点的儿子节点,每一个节点的权值为所有儿子节点的权值和,易得:
c[1]=a[1];c[2]=a[1]+a[2];c[3]=a[3];c[4]=c[2]+a[3]+a[4]=a[1]+a[2]+a[3]+a[4];
c[5]=a[5];c[6]=a[5]+a[6];c[7]=a[7];c[8]=c[4]+c[6]+a[7]=a[1]+a[2]+...+a[8]。
没错,上方的c数组就是树状数组。分析数据之后,我们可以得到树状数组的一些性质:对于c[i],他的儿子节点取决于i的所有因子中最多有2^j次幂,则向前取2^j个数作为儿子,即[i-2^j+1,i]。例如,6的最大2次方因子为2,即2^1,则向前取2个数,则c[6]=a[5]+a[6];8的最大2次方因子为8,即2^3,则向前取8个数,则c[8]=a[1]+a[2]+...+a[8]。
3、构建&询问&修改
三个步骤,一起讲了吧,都很简单。
<1>构建:应该并不存在什么难度,唯一要考虑的就是如何得到为该数因子中最大的2^x是多少。我自己在想的时候只能想到很复杂的还要预处理的方式,其实巧妙地利用位运算符就可以以O(1)的速度直接得到,至于原因,没有去考虑,记着就行吧。代码:
------------------------------------------------------------------------------------------------------
int lowbit(int x) { return x&(-x); }
------------------------------------------------------------------------------------------------------
<2>求数组的和:我们注意到,树状数组在求和的时候,相对于普通数组的优势就像树链剖分和普通LCA一样。每次我们不需要一个一个相加,直接利用当前位置的lowbit值跳转即可,如代码:
------------------------------------------------------------------------------------------------------
int getSum(int now)
{
int sum=0;
while (now>0) sum+=c[now],now-=lowbit(now);
return sum;
}
------------------------------------------------------------------------------------------------------
<3>单点修改权值:同样地,修改也是非常快的,O(log n),假设当前为节点i加上val,如代码:
------------------------------------------------------------------------------------------------------
int update(int i,int val) { while (i<=n) c[i]+=x,i+=lowbit(i); }
------------------------------------------------------------------------------------------------------
4、总结
显然可以看出来了,树状数组空间复杂度小些,代码短些,常数也小些,这些平时可能不是很注意的优势都要注意,虽然它的功能缺失逊于线段树,但是在能够使用树状数组的情况下,我觉得还是可以多用用。
UPDATE(20151009):
因为个人觉得逆序对一个东西不需要单独放出来,又其可以用树状数组去求解,故就在这里写写吧。见下。
1、概念
逆序对仅在数组中有意义,对于一个包含n个非负整数的数组a,如果有i<j,且a[i]>a[j],则称(a[i],A[j])为数组a中的一个逆序对。其实我好几次看这个的时候都觉得它用途应该很小,也不知道能在什么情况下用。直到看过了NOIP的火柴排队之后就能够理解了,而后今天又做了一道类似的题目。
2、求法
逆序对有两种求法,O(n^2)的做法应该谁都想得到吧?直接暴力查找就行了。而O(n log n)算法是利用树状数组或者归并排序来维护。从代码复杂度而言,树状数组显然是首选。为什么?因为在求逆序对的过程中,我们唯一需要知道的数组内元素之间的关系就是谁大谁小,那么我们将元素一一放入树状数组中,可以在O(log n)的时间内求出比它小(大)的数。
代码:
-----------------------------------------------------------------------------------------------------
int n,a[MAXN],b[MAXN],ans=INF,o;
int lowbit(int o) { return o&-o; }
int query(int o) { int res=0; while (o) res+=b[o],o-=lowbit(o); return res; }
void insert(int o) { while (o<=n) b[o]++,o+=lowbit(o); }
for (int i=1;i<=n;i++) scanf("%d",&a[i]);
for (int i=n;i>=1;i--) o+=query(a[i]-1),insert(a[i]);
-----------------------------------------------------------------------------------------------------