【算法与数据结构】—— 树状数组

树状数组



概述

树状数组是一个查询和修改复杂度都为 O ( log ⁡ n ) O(\log{n}) O(logn) 的数据结构,主要用于数组的单点修改以及区间求和。比如说对于某个数组 a r y = a 1 , a 2 , … , a n ary={a_1,a_2,\ldots,a_n} ary=a1,a2,,an ,若现在要求动态地对区间 [ a , b ] \left[a,b\right] [a,b] 进行求和,则在每次给出 a 、 b a、b ab 取值后都需要计算 ∑ i = a b a i \sum_{i=a}^ba_i i=abai,这个过程是线性的(即时间复杂度为 O ( n ) O\left(n\right) O(n)):

sum = 0;
for(int i=a; i<=b; i++)
	sum += ary[i];

m m m 次询问,动态求解区间和的时间复杂度即为 O ( m n ) O\left(mn\right) O(mn)
对于区间求和,可能你会想到“前缀和”这一数据结构,这确实是一个很好的办法,但对于含单点修改操作的区间求和而言,前缀和与差分数组都将难以完成任务。此时,树状数组将尽其所长(求动态前缀和)。当采用树状数组存储序列信息时,其求区间和的时间复杂度将变为 O ( log ⁡ n ) O(\log{n}) O(logn)。显然,当 n n n 的范围较大时,树状数组带来的收益将是非常明显的。

另外一个拥有类似功能的是线段树,两者间的具体区别和联系如下:

  • 两者在复杂度上同级,但是树状数组的常数明显优于线段树,其编程复杂度也远小于线段树。
  • 树状数组的作用被线段树完全涵盖。凡是可以用树状数组解决的问题,使用线段树一定可以解决;但是线段树能够解决的问题树状数组未必能解决。
  • 树状数组的突出特点是编程的极端简洁性。使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树(后面会详细阐述lowbit的原理)。

“树状数组” 名称的由来:对于一般的二叉树,其形状大致如下:

在这里插入图片描述

若将各结点的位置稍微移动,便得到树状数组的画法:

在这里插入图片描述

上图展示的是树状数组本身的结构,而实际上树状数组和初始数组的对应关系如下图所示(其中 A 数组是原数组,C 数组是求和后的数组(即树状数组),C[i] 代表子树的叶子结点的权值之和):

在这里插入图片描述

从上图可以看到:

  • C [ 1 ] = C [ 0001 ] = A [ 1 ] C[1] = C[0001] = A[1] C[1]=C[0001]=A[1]
  • C [ 2 ] = C [ 0010 ] = A [ 1 ] + A [ 2 ] C[2] = C[0010] = A[1]+A[2] C[2]=C[0010]=A[1]+A[2]
  • C [ 3 ] = C [ 0011 ] = A [ 3 ] C[3] = C[0011] = A[3] C[3]=C[0011]=A[3]
  • C [ 4 ] = C [ 0100 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] C[4] = C[0100] = A[1]+A[2]+A[3]+A[4] C[4]=C[0100]=A[1]+A[2]+A[3]+A[4]
  • C [ 5 ] = C [ 0101 ] = A [ 5 ] C[5] = C[0101] = A[5] C[5]=C[0101]=A[5]
  • C [ 6 ] = C [ 0110 ] = A [ 5 ] + A [ 6 ] C[6] = C[0110] = A[5]+A[6] C[6]=C[0110]=A[5]+A[6]
  • C [ 7 ] = C [ 0111 ] = A [ 7 ] C[7] = C[0111] = A[7] C[7]=C[0111]=A[7]
  • C [ 8 ] = C [ 1000 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] + A [ 5 ] + A [ 6 ] + A [ 7 ] + A [ 8 ] C[8] = C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8] C[8]=C[1000]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]

由此可见,树状数组的存在,使得某些原本需要成段计算的元素被集中放在了某个单元素中。例如,利用原始数组 A [   ] A[\ ] A[ ]计算 A [ 1 ] + A [ 2 ] + … + A [ 8 ] A[1]+A[2]+…+A[8] A[1]+A[2]++A[8] 时需要计算 8 次加法运算,而利用树状数组 C [   ] C[\ ] C[ ] 计算 A [ 1 ] + A [ 2 ] + … + A [ 8 ] A[1]+A[2]+…+A[8] A[1]+A[2]++A[8] 仅需计算 1 次(取出 C [ 8 ] C[8] C[8] )。同时,利用新的树状数组也能得到所有区间和,例如:区间和 [ 2 , 4 ] = C [ 4 ] − C [ 1 ] [2,4]=C[4]-C[1] [2,4]=C[4]C[1]、区间和 [ 3 , 7 ] = C [ 7 ] + C [ 4 ] − C [ 2 ] [3,7]=C[7]+C[4]-C[2] [3,7]=C[7]+C[4]C[2]……其内部是如何实现自动计算待累加元素的原理将在后续进行讲解。现在我们只关心两个问题:

  • 新的数据结构是否合理(不会造成数据丢失)?
  • 是否能在访问时间上带来明显优化?

问题一:新的数据结构是否会造成数据丢失?

当然不会。拿上面的例图来说,可以很明显地看出树状数组对原始数组进行了分区与覆盖:比如 C [ 1 ] C[1] C[1] 分到的区域是 A [ 1 ] A[1] A[1] C [ 2 ] C[2] C[2] 分到的区域是 A [ 1 ] A[1] A[1] A [ 2 ] A[2] A[2] C [ 8 ] C[8] C[8] 分到的区域则是 A [ 1 ] , A [ 2 ] , … … , A [ 8 ] A[1],A[2],……,A[8] A[1],A[2],……,A[8]。其特点是:每个 C [ i ] C[i] C[i] 必定会包括对应 A [ i ] A[i] A[i],此外其还会额外包括一些其他项 A [ x ] A[x] A[x],而这些项将由 i i i 本身确定(从 i i i 的二进制最低位出发含有的连续零个数,后续会进行详细解释)。因此这样的存储并不会导致数据丢失,反而带来了便于访问的好处:在对某段区间进行求和以及更新时能够快速地跳表,从而避免了对区间中的每一项都进行访问,加快了访问速度。

问题二:新的数据结构能在访问时间上带来明显优化?
当然会。且降低程度随着 n n n 的增加其效果会越明显。关于这一点的证明将在后面给出 lowbit 函数的定义后加以说明。



原理与实现

前面讨论了树状数组的基本结构,现在我们来思考如何构建和使用树状数组?

实际上,树状数组的 “快速跳表” 既应用在访问过程,也用在了构建过程,我们来看以下例子(其中, k k k i i i 的二进制中从最低位到高位连续零的长度):

  • i = 1 i=1 i=1(二进制:0001)时, k = 0 k=0 k=0,则 C [ 1 ] = C [ 0001 ] = A [ 1 ] C[1] = C[0001] = A[1] C[1]=C[0001]=A[1]
  • i = 6 i=6 i=6(二进制:0110)时, k = 1 k=1 k=1,则 C [ 6 ] = C [ 0010 ] = A [ 5 ] + A [ 6 ] C[6] = C[0010] = A[5]+A[6] C[6]=C[0010]=A[5]+A[6]
  • i = 4 i=4 i=4(二进制:0100)时, k = 2 k=2 k=2,则 C [ 4 ] = C [ 0100 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] C[4] = C[0100] = A[1]+A[2]+A[3]+A[4] C[4]=C[0100]=A[1]+A[2]+A[3]+A[4]
  • i = 8 i=8 i=8(二进制:1000)时, k = 3 k=3 k=3,则 C [ 8 ] = C [ 1000 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] + A [ 5 ] + A [ 6 ] + A [ 7 ] + A [ 8 ] C[8] = C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8] C[8]=C[1000]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]

由此可见,树状数组C的构建存在如下规则:

C [ i ] = A [ i − 2 k + 1 ] + A [ i − 2 k + 2 ] + … + A [ i ] C\left[i\right]=A\left[i-2^k+1\right]+A\left[i-2^k+2\right]+\ldots+A\left[i\right] C[i]=A[i2k+1]+A[i2k+2]++A[i]

这便是树状数组的更新(构建)过程,这个过程实际上是通过快速跳表得到。

那访问过程呢?假设现在要计算 A [ 1 ] , A [ 2 ] , … … , A [ 9 ] A[1],A[2],……,A[9] A[1],A[2],……,A[9] 的总和 s u m sum sum,用树状数组该如何计算?

  • i = 9 i = 9 i=9 时,其对应二进制:1001,从最低位出发找到的连续零个数为0,因此 i i i 更新为(即进行跳表): i = i – 20 = 9 − 1 = 8 i = i–20 = 9-1 = 8 i=i–20=91=8,并记录 s u m + = C [ 9 ] sum += C[9] sum+=C[9](注: C [ 9 ] = A [ 9 ] C[9] = A[9] C[9]=A[9]
  • i = 8 i = 8 i=8 时,其对应二进制:1000,从最低位出发找到的连续零个数为3,因此 i i i 更新为(即进行跳表): i = i – 23 = 8 − 8 = 0 i = i–23 = 8-8 = 0 i=i–23=88=0,并记录 s u m + = C [ 8 ] sum += C[8] sum+=C[8](注: C [ 8 ] = A [ 1 ] + A [ 2 ] + … … + A [ 8 ] C[8] = A[1]+A[2]+……+A[8] C[8]=A[1]+A[2]+……+A[8]
  • i = 0 i = 0 i=0 表示到达临界点,故停止更新。故得到 s u m = C [ 9 ] + C [ 8 ] sum = C[9]+C[8] sum=C[9]+C[8]

上面过程仅用 2 次加法运算便得到了 A [ 1 ] , A [ 2 ] , … … , A [ 9 ] A[1],A[2],……,A[9] A[1],A[2],……,A[9] 的总和,这比传统求解方式快了太多。这便是树状数组在计算上带来的优化。同时,我们也能从这里看出一点:树状数组的核心操作在于快速跳表。因此,接下来的问题便是,如何实现上面的快速跳表?

首先要肯定一件事,跳表操作必须足够快,否则会影响树状数组的访问速度。另一方面,从前面的分析可知,跳表操作涉及到了数的二进制表达和运算,所以我们必须深入了解计算机中的数值表示方式,并从中设计合适的算法来达到跳表目的。需要注意,计算机中的数值表示方式也是一项颇有难度的点,此处仅介绍与求解当前问题相关的知识。

  • 原码:用二进制表示的数字,其中最高位为符号位(0表示正数、1表示负数)。如十进制数6的原码为(用1字节表示,下同):0000 0110;十进制数-6的原码为1000 0110。
  • 反码:正数的反码和原码一样,负数的反码除最高位符号位外,其他位都取反(即0变1,1变0,符号位不变)。如十进制数6的反码为:0000 0110;十进制数-6的反码为1111 1001。
  • 补码:在反码的基础上加1(这样可以方便计算机进行计算)。如十进制数6的补码为:0000 0110;十进制数-6的补码为1111 1010。
  • 注:在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。说简单点:实际上计算机中是没有减法运算的,减法实际上就是补码直接相加。例如,上面例子中的 6-6 在计算机中执行的是:(0000 0110) + (1111 1010)= 1 0000 0000,由于寄存器共 1 字节(8位),因此得到结果的最高位 1 会被舍弃,最终得到的结果即为 0000 0000,于是最后计算得到的结果是6-6=0。

对于问题 “如何实现快速跳表?” 我们只需要知道一点:在计算机系统中,负数的补码等于原码除符号位外的所有位按位取反后再加1。此时,从数字的补码里,会出现一个有趣的现象,例如:

十进制数 12,其对应补码为:0000 1100;其相反数 -12 对应的补码为:1111 0100。如果将这两个数按位进行 “与运算” 则有:12 & -12 = (0000 1100) + (1111 0100) = 0000 0100。

接下来从跳表的角度出发(假设现在要计算 A [ 1 ] , A [ 2 ] , … … , A [ 12 ] A[1],A[2],……,A[12] A[1],A[2],……,A[12] 的总和 s u m sum sum),对于 i = 12 i=12 i=12(记录 s u m + = C [ 12 ] sum += C[12] sum+=C[12]),其二进制数为1100,从最低位出发找到的连续零个数为 2,因此更新 i = i – 2 2 = 12 − 4 = 8 i = i–2^2 = 12 - 4 = 8 i=i22=124=8。重点在于减数 2 2 = 4 2^2 = 4 22=4,其对应的二进制数正好是 0000 0100。下面继续跳表(更新 i i i)。

十进制数 8,其对应补码为:0000 1000;其相反数 -8 对应的补码为:1111 1000。将这两个数按位进行 “与运算” 则有:8 & -8 = (0000 1000) + (1111 1000) = 0000 1000。

接下来从跳表的角度出发,对于 i = 8 i=8 i=8(记录 s u m + = C [ 8 ] sum += C[8] sum+=C[8]),其二进制数为 1000,从最低位出发找到的连续零个数为 3,因此需更新 i = i – 2 3 = 8 − 8 = 0 i = i–2^3 = 8 - 8 = 0 i=i23=88=0。重点在于减数 2 3 = 8 2^3 = 8 23=8,其对应的二进制数正好是 0000 1000。

此时由于 i = 0 i=0 i=0,故停止更新,最终得到 s u m = C [ 12 ] + C [ 8 ] sum = C[12]+C[8] sum=C[12]+C[8]

从上面的过程不难发现:跳表时需要转移的差值可以通过运算:n & (-n) 得到
而这,正是树状数组的核心函数 lowbit(),其定义如下:

int lowbit(int n)
{ return n & (-n); }

同时,从这里可以看出,树状数组的索引需要从 1 开始计数。



单点更新

树状数组的单点更新既可表达为“对原始数组中的某个元素进行修改”,也可引申为“创建一个树状数组”(此时可视作对原始数组中的所有元素进行更新)。前面提到,树状数组的构建存在规则:
C [ i ] = A [ i − 2 k + 1 ] + A [ i − 2 k + 2 ] + … + A [ i ] C\left[i\right]=A\left[i-2^k+1\right]+A\left[i-2^k+2\right]+\ldots+A\left[i\right] C[i]=A[i2k+1]+A[i2k+2]++A[i]

这就表明对在原始数组的某个元素进行修改时,其会连带修改树状数组上层中的若干个节点。例如,假设当前存在一个含 8 个节点的树状数组(初始时 C [ i ] C[i] C[i] 均取 0),则在插入(更新)第 1 个元素 A [ 1 ] A[1] A[1] 时,其涉及到待修改的上层节点有: C [ 1 ] , C [ 2 ] , C [ 4 ] , C [ 8 ] C[1],C[2],C[4],C[8] C[1],C[2],C[4],C[8](如下图所示)。

在这里插入图片描述

用 lowbit 函数对这个过程进行模拟,如下:

  • 最初 i = 1 i=1 i=1,执行 C [ i ] + = A [ 1 ] C[i]+=A[1] C[i]+=A[1](完成了C[1]的更新),由于 l o w b i t ( 1 ) = 1 lowbit(1)=1 lowbit(1)=1,则更新 i = i + l o w b i t ( i ) = 1 + 1 = 2 i=i+lowbit(i)=1+1=2 i=i+lowbit(i)=1+1=2
  • 接着 i = 2 i=2 i=2,执行 C [ i ] + = A [ 1 ] C[i]+=A[1] C[i]+=A[1](完成了C[2]的更新),由于 l o w b i t ( 2 ) = 2 lowbit(2)=2 lowbit(2)=2,则更新 i = i + l o w b i t ( i ) = 2 + 2 = 4 i=i+lowbit(i)=2+2=4 i=i+lowbit(i)=2+2=4
  • 然后 i = 4 i=4 i=4,执行 C [ i ] + = A [ 1 ] C[i]+=A[1] C[i]+=A[1](完成了C[4]的更新),由于 l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4,则更新 i = i + l o w b i t ( i ) = 4 + 4 = 8 i=i+lowbit(i)=4+4=8 i=i+lowbit(i)=4+4=8
  • 最后 i = 8 i=8 i=8,执行 C [ i ] + = A [ 1 ] C[i]+=A[1] C[i]+=A[1](完成了C[8]的更新),由于当前 i i i 已经到达了树状数组的节点上限,故停止更新。此时,得到的树状数组内容如下:
i i i12345678
C [ i ] C[i] C[i]11010001

继续,接下来插入 A [ 2 ] = 2 A[2]=2 A[2]=2,其涉及到待修改的上层节点有 C [ 2 ] , C [ 4 ] , C [ 8 ] C[2],C[4],C[8] C[2],C[4],C[8],如下图所示:

在这里插入图片描述

再用 lowbit 函数对这个过程进行模拟,如下:

  • 最初 i = 2 i=2 i=2,故 C [ i ] + = A [ 2 ] C[i]+=A[2] C[i]+=A[2](完成了C[2]的更新),由于 l o w b i t ( 2 ) = 2 lowbit(2)=2 lowbit(2)=2,则更新 i = i + l o w b i t ( i ) = 2 + 2 = 4 i=i+lowbit(i)=2+2=4 i=i+lowbit(i)=2+2=4
  • 接着 i = 4 i=4 i=4,故 C [ i ] + = A [ 2 ] C[i]+=A[2] C[i]+=A[2](完成了C[4]的更新),由于 l o w b i t ( 4 ) = 4 lowbit(4)=4 lowbit(4)=4,则更新 i = i + l o w b i t ( i ) = 4 + 4 = 8 i=i+lowbit(i)=4+4=8 i=i+lowbit(i)=4+4=8
  • 最后 i = 8 i=8 i=8,故 C [ i ] + = A [ 2 ] C[i]+=A[2] C[i]+=A[2](完成了C[8]的更新),由于当前 i i i 已经到达了树状数组的节点上限,故停止更新。此时,得到的树状数组内容如下::
i i i12345678
C [ i ] C[i] C[i]13030003

……

如此执行下去,就通过 lowbit 函数构建好了一个树状数组,将上述过程的每步抽象出来便得到了树状数组的更新函数:

void update(int x,int y,int n)	//单点更新:x为更新的位置,y为更新后的数,n为数组的规格
{ 
	for(int i=x; i<=n; i+=lowbit(i))
		C[i] += y;
}


区间求和

前面提到,树状数组的跳表操作即可用于更新也可用于访问,它们都是基于 lowbit() 函数实现的。但 lowbit() 函数在跳表时,其范围总是从某个位置到树状数组的最底层(或最上层),取决于调用 lowbit() 时采用累加还是逐减。因此,lowbit() 函数在求树状数组的区间和时,其总是先得到 “前n项和” :

int getSum(int n)	//求前n项之和
{
	int ans = 0;
	for(int i=n; i; i-=lowbit(i))
		ans += C[i];
	return ans;
}

然后再通过调用此函数,来得到某一段区间的区间和:

int getSumOfSection(int l,int r)	//求区间[l,r]之间的元素之和
{ return getSum(r) - getSum(l-1); }


树状数组的典型用例:求逆序数

逆序数是指一个序列中满足 j < i j<i j<i a r y [ j ] > a r y [ i ] ary\left[j\right]>ary\left[i\right] ary[j]>ary[i] 的二元组对数。例如,对于 {4, 2, 1, 5, 3} 这个序列,满足条件的二元组有 {<4,2>, <4,1>, <4,3>, <2,1>, <5,3> },故该序列的逆序数为 5。

可利用树状数组在 lowbit() 跳表时进行前向查询(计算前 n 项和),此时,树状数组维护的信息是某个区间中数字出现的个数。在这样的前提下,若将源数据按其原顺序插入树状数组(第 i i i 个数字插入时就将树状数组的 a r y [ i ] ary[i] ary[i] 元素置为 1,同时更新覆盖到它的父区间),那么之后在执行 getSum(ary[i]-1) 时,得到的就是数字 ary[i] 前小于它的个数,则大于该数的就有 (i-1)-getSum(ary[i]-1) 个(“当前输入数据总数-自身” - “前面更小数的个数”)。采用这样的方式,在不断插入数据的同时就能统计出目标序列含有的所有逆序对个数。下面用序列 {4, 2, 1, 5, 3} 进行推演:

初始时,前缀和数组 C [   ] = { 0 } C[\ ]=\{0\} C[ ]={0}(注:前缀和数组的索引都是从 1 开始计数的),如下:

i i i12345
C [ i ] C[i] C[i]00000
  1. 当前 i = 1 i = 1 i=1 输入第一个元素 a r y [ i ] = 4 ary[i] = 4 ary[i]=4,此时更新前缀和数组的内容如下:

    i i i12345
    C [ i ] C[i] C[i]00010

    此时 g e t S u m ( a r y [ i ] − 1 ) = g e t S u m ( 3 ) = 0 + 0 + 0 = 0 getSum(ary[i]-1) = getSum(3) = 0+0+0 = 0 getSum(ary[i]1)=getSum(3)=0+0+0=0,即认为元素 4 前没有比它更小的数,因此其产生的逆序对个数为: ( i − 1 ) − 0 = 0 − 0 = 0 (i-1)-0 = 0-0 = 0 (i1)0=00=0

  2. 当前 i = 2 i = 2 i=2 输入第二个元素 a r y [ i ] = 2 ary[i] = 2 ary[i]=2,此时前缀和数组的内容如下:

    i i i12345
    C [ i ] C[i] C[i]01010

    此时 g e t S u m ( a r y [ i ] − 1 ) = g e t S u m ( 1 ) = 0 getSum(ary[i]-1) = getSum(1) = 0 getSum(ary[i]1)=getSum(1)=0,即认为元素 2 前没有比它更小的数,因此其产生的逆序对个数为: ( i − 1 ) − 0 = 1 − 0 = 1 (i-1)-0 = 1-0 = 1 (i1)0=10=1

  3. 当前 i = 3 i = 3 i=3 输入第二个元素 a r y [ i ] = 1 ary[i] = 1 ary[i]=1,此时前缀和数组的内容如下:

    i i i12345
    C [ i ] C[i] C[i]11010

    此时 g e t S u m ( a r y [ i ] − 1 ) = g e t S u m ( 1 ) = 0 getSum(ary[i]-1) = getSum(1) = 0 getSum(ary[i]1)=getSum(1)=0,即认为元素 1 前没有比它更小的数,因此其产生的逆序对个数为: ( i − 1 ) − 0 = 2 − 0 = 2 (i-1)-0 = 2-0 = 2 (i1)0=20=2

  4. 当前 i = 4 i = 4 i=4 输入第二个元素 a r y [ i ] = 5 ary[i] = 5 ary[i]=5,此时前缀和数组的内容如下:

    i i i12345
    C [ i ] C[i] C[i]11011

    此时 g e t S u m ( a r y [ i ] − 1 ) = g e t S u m ( 1 ) = 0 getSum(ary[i]-1) = getSum(1) = 0 getSum(ary[i]1)=getSum(1)=0,即认为元素 5 前有 3 个比它更小的数,因此其产生的逆序对个数为: ( i − 1 ) − 3 = 3 − 3 = 0 (i-1)-3 = 3-3 = 0 (i1)3=33=0

  5. 当前 i = 5 i = 5 i=5 输入第二个元素 a r y [ i ] = 3 ary[i] = 3 ary[i]=3,此时前缀和数组的内容如下:

    i i i12345
    C [ i ] C[i] C[i]11111

    此时 g e t S u m ( a r y [ i ] − 1 ) = g e t S u m ( 1 ) = 0 getSum(ary[i]-1) = getSum(1) = 0 getSum(ary[i]1)=getSum(1)=0,即认为元素 5 前有 2 个比它更小的数,因此其产生的逆序对个数为: ( i − 1 ) − 2 = 4 − 2 = 2 (i-1)-2 = 4-2 = 2 (i1)2=42=2

综上,可得到所有逆序对的总数为: 0 + 1 + 2 + 0 + 2 = 5 0+1+2+0+2=5 0+1+2+0+2=5

基于这一思路,可得到利用树状数组求解逆序对的完整代码:

#include<bits/stdc++.h> 
using namespace std;

const int MAX = 1e5+5;
int C[MAX], n;

// 树状数组模板
int lowbit(int x){return x & -x;}
void update(int pos,int valve)
{ 
    for(int i=pos;i<=n;i+=lowbit(i))
        C[i] += valve;
}
int getSum(int pos)
{
    int ans = 0;
    while(pos){
        ans += C[pos];
        pos -= lowbit(pos);
    }
    return ans;
}

int main( )
{
    int x, ans = 0; 
    cin>>n;
    for(int i=0; i<n; i++){
        cin>>x;
        update(x, 1);
        ans += i-getSum(x-1);
    }
    cout<<ans<<endl;
    return 0;
}

但是,树状数组在进行区间查询时总会遍历整个数组,这在数据范围很大而实际数据有限的情况下无疑会浪费很多查询时间。例如,对于序列 {4, 2, 3, 1, 9999} 而言,其在查询元素 9999 前更小数的个数时,会从 9999 开始往前进行若干次 lowbit() 运算,但我们知道,对于求该序列的逆序对而言,它根本不需要从 9999 开始,其只需要从 5 开始即可。换言之,在用树状数组求逆序对时,我们仅关心原始序列中各个数的相对大小,而不需要用绝对大小进行求解。

所以,现实中用树状数组求序列逆序对时,我们通常会对输入数据进行数据压缩,如将序列 {4, 2, 3, 1, 9999} 转换为 {4, 2, 3, 1, 5} 进行求解。当然,也可以转换为 {2, 4, 3, 5, 1} 求解。这两种转换方式的区别在于,前者(按数据原始相对大小关系进行转换)得到的数组在求解时,依然是求序列的逆序对;后者(按数据相对大小的相反关系进行转换)得到的数组在求解时,要求的是序列的顺序对。由于求顺序对更容易理解,因此下面的代码将采取后一种方式:

#include <bits/stdc++.h>
using namespace std;

const int MAX = 1e5 + 5;
// a 数组存放数据序号、b 数组存放数据绝对取值 
int a[MAX], b[MAX], c[MAX], n;
long ans;

// 树状数组模板 
int lowbit(int x) { return x & -x; }
void add(int i, int x)
{
    while(i<=n){
        c[i] += x;
        i += lowbit(i);
    }   
}
int sum(int i)
{
    int ans = 0;
    while(i>0){
        ans += c[i];
        i -= lowbit(i);
    }
    return ans;
}

// 进行数据压缩时的比较函数 
bool cmp(const int x, const int y)
{
    if (b[x] == b[y])
        return x > y;
    return b[x] > b[y];
}

int main()
{
    // 录入原始序列 
    cin>>n;
    for(int i=1; i<=n; i++){
        cin>>b[i];
        a[i]=i;
    }
    
    // 通过排序实现数据压缩:按照 b 数组指定的大小关系对 a 数组进行排序 
    // 这一步会将原始数据的大小关系置反,因此现在要求的便是顺序对了 
    sort(a+1, a+n+1, cmp);
        
    // 利用树状数组求顺序对 
    for (int i=1; i<=n; i++){
        add(a[i], 1);
        ans += sum(a[i] - 1);
    }
    cout<<ans<<endl;
    return 0;
}


趁热打铁

练习题:【马蹄集】MT2141 快排变形、MT2142 逆序


END


  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theSerein

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值