算法复习——分而治之篇之逆序对计数问题

算法复习——分而治之篇之逆序对计数问题

以下内容主要参考中国大学MOOC《算法设计与分析》,墙裂推荐希望入门算法的童鞋学习!

1. 问题背景

逆序对:当 i < j i<j i<j时, A [ i ] > A [ j ] A[i]>A[j] A[i]>A[j]的二元组 ( A [ i ] , A [ j ] ) (A[i], A[j]) (A[i],A[j]),例如下图中的 ( A [ 1 ] , A [ 4 ] ) (A[1], A[4]) (A[1],A[4]) ( A [ 2 ] , A [ 4 ] ) (A[2], A[4]) (A[2],A[4])等。

在这里插入图片描述

​ 那么,问题就变成了如何求解数组中逆序对的个数?

2. 问题定义

逆序对计数问题(Counting Inversions)

输入:

  • 长度为 n n n的数组 A [ 1.. n ] A[1..n] A[1..n]

输出:

  • 数组 A [ 1.. n ] A[1..n] A[1..n]逆序对的总数,即 ∑ 1 ≤ i < j ≤ n X i , j \sum_{1 \leq i < j \leq n}X_{i, j} 1i<jnXi,j,其中,
    X i , j = { 1 , A [ i ] > A [ j ] 0 , A [ i ] ≤ A [ j ] X_{i, j}=\left\{ \begin{array}{rcl} 1, & & {A[i] > A[j]}\\ 0, & & {A[i] \leq A[j]}\\ \end{array} \right. Xi,j={1,0,A[i]>A[j]A[i]A[j]

3. 分而治之

在这里插入图片描述

  • 分解原问题:把数组 A A A二分为两个子数组 A [ 1.. n 2 ] A[1..\frac{n}{2}] A[1..2n] A [ n 2 + 1.. n ] A[\frac{n}{2}+1..n] A[2n+1..n]
  • 递归求解子问题
    • 求解 S 1 S_{1} S1:仅在 A [ 1.. n 2 ] A[1..\frac{n}{2}] A[1..2n]中的逆序对数目
    • 求解 S 2 S_{2} S2:仅在 A [ n 2 + 1.. n ] A[\frac{n}{2}+1..n] A[2n+1..n]中的逆序对数目
  • 合并问题解:合并 A [ 1.. n 2 ] A[1..\frac{n}{2}] A[1..2n] A [ n 2 + 1.. n ] A[\frac{n}{2}+1..n] A[2n+1..n]的解:
    • 求解 S 3 S_{3} S3:跨越子数组的逆序对数目
    • S = S 1 + S 2 + S 3 S = S_{1} + S_{2} + S_{3} S=S1+S2+S3

​ 那么,问题变成了如何高效地求解 S 3 S_{3} S3

  • 策略一:直接求解
    • 对每个 A [ j ] ∈ A [ m + 1.. n ] A[j] \in A[m+1..n] A[j]A[m+1..n],枚举 A [ i ] ∈ A [ 1.. m ] A[i] \in A[1..m] A[i]A[1..m]并统计逆序对数目;
    • 这种策略求解 S 3 S_{3} S3的算法运行时间是 O ( n 2 ) O(n^{2}) O(n2)
    • 分而治之框架的算法运行时间: T ( n ) = 2 T ( n 2 ) + O ( n 2 ) T(n)=2T(\frac{n}{2})+O(n^{2}) T(n)=2T(2n)+O(n2),由主定理可得 T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2);这种策略并没有提高算法的效率,时间复杂度和暴力枚举一致。

​ 我们可以来思考策略一失败的原因。首先我们已经知道,运行时间受制于跨越子数组的逆序对计数方法。那么可以换种思路,我们实际上是找右边的一个数,在左边有几个数比它大。而这种定位查找,我们通常会想到二分查找,但是二分查找有前提条件,那就是被查找数组要排好顺序。所以,如果被查找数组能够排好顺序,利用排好顺序的有序性进行二分查找,可以很容易找到右边的数在左边的定位。数组的有序性通常有助于提高算法的运行时间

  • 策略二:排序求解:
    • 分别对数组 A [ 1.. m ] A[1..m] A[1..m] A [ m + 1.. n ] A[m+1..n] A[m+1..n]进行排序;对于每个 A [ j ] ∈ A [ m + 1.. n ] A[j] \in A[m+1..n] A[j]A[m+1..n],利用二分查找为其在 A [ 1.. m ] A[1..m] A[1..m]中定位;则 A [ j ] A[j] A[j] A [ 1.. m ] A[1..m] A[1..m]定位点右侧的元素均可与 A [ j ] A[j] A[j]构成逆序对。
    • 这种策略求解 S 3 S_{3} S3的算法运行时间是 O ( n l o g n ) O(nlogn) O(nlogn)(对两个子数组排序的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),对右边所有元素二分查找的时间复杂度也是 O ( n l o g n ) O(nlogn) O(nlogn))。
    • 分而治之框架的算法运行时间: T ( n ) = 2 T ( n 2 ) + O ( n l o g n ) T(n)=2T(\frac{n}{2})+O(nlogn) T(n)=2T(2n)+O(nlogn),由主定理可得 T ( n ) = O ( n l o g 2 n ) T(n)=O(nlog^{2}n) T(n)=O(nlog2n)

​ 排序求解 S 3 S_{3} S3的分而治之提高了算法运行时间,是否还有优化可能?看似排序和二分查找均为再优化空间了,其实不然,因为在这个策略中,没有将排序过程融入算法框架。我们以下图为例。

​ 我们对以上两个数组分别排序,如下图所示。

在这里插入图片描述

​ 然后,我们通过二分查找的过程后,合并两个数组。

在这里插入图片描述

​ 然而,这时我们又要对整个数组重新排序了,也就是受排序未利用子数组有序性质。看到这里,如果阅读了算法复习——分而治之篇之归并排序,一定可以想到杠铃增重的案例,因此我们在这里也可以使用归并排序来利用子数组的有序性质,即在合并的时候完成排序,那么子数组排序的运行时间就从原先的 O ( n l o g n ) O(nlogn) O(nlogn)降到了 O ( n ) O(n) O(n)

​ 那么如何降低二分查找的时间复杂度呢?如果 j > m i d j > mid j>mid i ≤ m i d i \leq mid imid,且 A [ i ] > A [ j ] A[i] > A[j] A[i]>A[j]时, A [ i . . m i d ] A[i..mid] A[i..mid]中的元素均与 A [ j ] A[j] A[j]构成逆序对,因此通过一次线性扫描 O ( n ) O(n) O(n),就可以所有右子数组元素在左数组中的定位。

  • 策略三:归并求解
    • 从左到右扫描有序子数组: A [ i ] ∈ A [ 1.. m ] A[i] \in A[1..m] A[i]A[1..m] A [ j ] ∈ A [ m + 1.. n ] A[j] \in A[m+1..n] A[j]A[m+1..n]
      • 如果 A [ i ] > A [ j ] A[i] > A[j] A[i]>A[j],统计逆序对, j j j向右移
      • 如果 A [ i ] ≤ A [ j ] A[i] \leq A[j] A[i]A[j] i i i向右移
    • 利用归并排序框架保证合并后数组的有序性
    • 求解 S 3 S_{3} S3时间复杂度降至 O ( n ) O(n) O(n)

4. 伪代码

初始调用:CountInver(A, 1, n)

CountInver(A, left, right)

输入:数组 A [ 1.. n ] A[1..n] A[1..n],数组下标 l e f t left left r i g h t right right

输出:数组 A [ l e f t . . r i g h t ] A[left..right] A[left..right]的逆序对数,递增数组 A [ l e f t . . r i g h t ] A[left..right] A[left..right]

if left >= right then
	return A[left..right]
end
mid ← (left + right) // 2
S_1 ← CountInver(A, left, mid)
S_2 ← CountInver(A, mid+1, right)
S_3 ← MergeCount(A, left, mid, right)
S ← S_1 + S_2 + S_3
return S, A[left..right]

归并求解:MergeCount(A, left, mid, right)

输入:数组 A [ 1.. n ] A[1..n] A[1..n],数组下标 l e f t left left m i d mid mid r i g h t right right

输出:跨越数组 A [ l e f t . . m i d ] A[left..mid] A[left..mid] A [ m i d + 1.. r i g h t ] A[mid+1..right] A[mid+1..right]的逆序对数, A [ l e f t . . r i g h t ] A[left..right] A[left..right]

A'[left..right] ← A[left..right], S_3 ← 0
i ← left, j ← mid + 1, k ← 0
while i <= mid and j <= right do
	if A'[i] <= A'[j] do
		A[left+k] ← A'[i]
		k ← k + 1, i ← i + 1
	end
	else
		A[left+k] ← A'[j]
		S_3 ← S_3 + (mid - i + 1)
		k ← k + 1, j ← j + 1
	end
end
if i <= mid then
	A[k..right] ← A'[i..mid]
end
else
	A[k..right] ← A'[j..right]
end
return S_3, A[left..right]

​ 基于以上的伪代码,可以进行时间复杂度分析,得到 T ( n ) T(n) T(n)的递归式。
T ( n ) = { 1 , n = 1 2 T ( n 2 ) + O ( n ) , n > 1 T(n)=\left\{ \begin{array}{rcl} 1, & & {n = 1}\\ 2T(\frac{n}{2})+O(n), & & {n > 1}\\ \end{array} \right. T(n)={1,2T(2n)+O(n),n=1n>1
​ 所以,使用主定理,可以直接求得 T ( n ) = n l o g n T(n)=nlogn T(n)=nlogn

  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值