分治实例(三)求解逆序数问题

问题

逆序:在序列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。

逆序数:序列中逆序的总数量。

例如,在数列{3,6,7,8,2}中,{3,2},{6,2},{7,2},{8,2}都是逆序。

思路

暴力法

暴力求解就是一个个地去遍历,时间复杂度O(N^2)。

分治法

应用归并的思想去求解,时间复杂度O(NlogN)。

对于一个数组arr来说,例如,arr=[32, 36, 3, 9, 29, 16, 35, 73, 34, 82],拆分成两半之后:A=[32, 36, 3, 9, 29],B=[16, 35, 73, 34, 82] —— 显然,A和B都是原问题的子问题,我们可以假设通过递归可以求解出A和B子问题的结果。 

那么问题来了,我们怎么通过A、B子问题的结果来构建arr的结果呢?也就是说,我们怎么通过子问题分治来获取原问题的答案呢?在回答之前,我们先来分析一下当前的情况。当我们将arr数组拆分成了AB两个部分之后,整个arr的逆序数就变成了三个部分:

  1. A数组之间的逆序数;
  2. B数组之间的逆序数;
  3. AB两个数组之间的逆序数 —— 也就是一个元素在A中,一个元素在B中的逆序数对。我们再来分析一下,会发现A数组中的元素交换位置,只会影响A数组之间的逆序数,并不会影响B以及AB之间构成的逆序数。因为A中的元素即使交换位置,也在B数组所有元素之前。 我们举个例子: 假设arr=[3, 5, 1, 4],那么A=[3, 5], B=[1, 4]。对于arr而言,它的逆序数是3分别是(3, 1), (5, 1)和(5, 4)。对于A而言,它的逆序数是0,B的逆序数也是0。我们试着交换一下B当中的位置,交换之后的B=(4, 1),此时arr=[3, 5, 4, 1]。那么B的逆序数变成1,A的逆序数依然还是0。而整体arr的逆序数变成了4,分别是:(3, 1),(5, 1),(5, 4)和(4,1),很明显整体的逆序数新增的就是B交换元素带来的。通过观察,我们也能发现,对于A中的3和5而言,B中的1和4的顺序并不影响它们构成逆序数的数量。想明白了这一层,剩下的就简单了。既然A和B当中的元素无论怎么交换顺序也不会影响对方的结果,那么我们就可以放心地使用分治算法来解决了。我们先假设,我们可以通过递归获取A和B各自的逆序数。那么剩下的问题就是找出所有A和B个占一个元素的逆序数对的情况了。 

我们先对A和B中的元素进行排序,我们之前已经验证过了,我们调整A或者B当中的元素顺序,并不会改变横跨AB逆序数的数量,而我们通过递归已经求到了A和B中各自逆序数对的数量,所以我们存下来之后,就可以对A和B中的元素进行排序了。A和B中元素有序了之后,我们可以用插入排序的方法,将A中的元素依次插入B当中。

 

从上图我们可以看出来,假设我们把[公式]这个元素插入B数组当中j的位置。由于之前[公式]排在B这j个元素之前,所以构成了j个逆序数对。我们对于所有A中的元素[公式]求出它在B数组所有插入的位置j,然后对j求和即可。 

比较容易想到,由于B元素有序,我们可以通过二分的方法来查找A当中元素的位置。但其实还有更好的办法,我们一个步骤就可以完成AB的排序以及插入,就是将AB两个有序的数组进行归并。在归并的过程当中,我们既可以知道插入的B数组中的位置,又可以完成数组的排序,也就顺带解决了A和B排序的问题。所以整个步骤其实就是归并排序的延伸,虽然整个代码和归并排序差别非常小,但是,这个过程当中的推导和思考非常重要。 

Python 3的实现 

分治法

# 求解逆序数问题
import sys

def inverse_ordinal_numbers(arr):
    n = len(arr)
    if n <= 1:
        return 0, arr

    # 整除
    mid = n // 2

    # 将数组拆分为二往下递归
    inverse_l, arr_l = inverse_ordinal_numbers(arr[:mid])
    inverse_r, arr_r = inverse_ordinal_numbers(arr[mid:])

    nl, nr = len(arr_l), len(arr_r)

    # 插入最大的int作为标兵,可以简化判断超界的代码
    arr_l.append(sys.maxsize)
    arr_r.append(sys.maxsize)

    i, j = 0, 0
    new_arr = []

    # 存储array对应的逆序数
    inverse = inverse_l + inverse_r

    while i < nl or j < nr:
        if arr_l[i] <= arr_r[j]:
            # 插入A[i]的时候逆序数增加j,因为A[i]之前放在这j个元素之前
            inverse += j
            new_arr.append(arr_l[i])
            i += 1
        else:
            new_arr.append(arr_r[j])
            j += 1
    return inverse, new_arr

上面这段代码实现了归并排序的同时也算出了逆序数。所以这就是为什么很多人会将两者相提并论的原因,也是我个人非常喜欢这个问题的原因。看起来完全不相关的两个问题,竟然能用几乎同一套代码来解决,不得不感叹算法的神奇。也正是因此,我们这些算法的研究和学习者,才能获取到源源不断的乐趣。

引自:归并算法的经典用法——求解逆序数 - 知乎 (zhihu.com)

参考:归并排序

posted @ 2022-03-20 16:47  vicky2021  阅读(753)  评论(0编辑  收藏  举报