交易逆序对的总数

标签: 树状数组 线段树 数组 二分查找 分治 有序集合 归并排序

难度: Hard

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。

示例 1:

输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

限制:

0 <= record.length <= 50000

Submission

运行时间: 1776 ms

内存: 20 MB

from typing import List


class Solution:
    result = 0

    def reversePairs(self, nums: List[int]) -> int:
        if not nums:
            return 0
        N = len(nums)
        if N == 1:
            return 0
        self.merge_count([0]*N, nums, 0, N-1)
        return self.result

    def merge_count(self, aux, a, lo, hi):
        if lo >= hi:
            return
        mid = (lo + hi) // 2
        self.merge_count(aux, a, lo, mid)
        self.merge_count(aux, a, mid+1, hi)
        self.merge(aux, a, lo, mid, hi)

    def merge(self, aux, a, lo, mid, hi):
        i = lo
        j = mid + 1
        for k in range(lo, hi + 1):
            aux[k] = a[k]
        for k in range(lo, hi + 1):
            if i > mid:
                a[k] = aux[j]
                j += 1
            elif j > hi:
                a[k] = aux[i]
                i += 1
            elif aux[j] < aux[i]:
                a[k] = aux[j]
                j += 1
                self.result += mid-i+1
            else:
                a[k] = aux[i]
                i += 1

if __name__ == '__main__':
    s = Solution()
    print(s.reversePairs([7,5,6,4]))

Explain

该题解使用了分治算法结合归并排序来寻找逆序对。首先,将数组分成左右两部分,递归地在每部分中找逆序对。在合并两部分的过程中,通过比较左右两部分的元素,统计跨越中点的逆序对的数量。如果左边元素大于右边元素,表明存在逆序对,并且逆序对的数量等于左边数组中从当前元素到中点的元素个数。

时间复杂度: O(n log n)

空间复杂度: O(n)

from typing import List


class Solution:
    result = 0  # 用于存储全局的逆序对个数

    def reversePairs(self, nums: List[int]) -> int:
        if not nums:
            return 0
        N = len(nums)
        if N == 1:
            return 0
        self.merge_count([0]*N, nums, 0, N-1)  # 初始化辅助数组并开始递归
        return self.result

    def merge_count(self, aux, a, lo, hi):
        if lo >= hi:
            return
        mid = (lo + hi) // 2
        self.merge_count(aux, a, lo, mid)  # 递归处理左半部分
        self.merge_count(aux, a, mid+1, hi)  # 递归处理右半部分
        self.merge(aux, a, lo, mid, hi)  # 合并两部分

    def merge(self, aux, a, lo, mid, hi):
        i = lo
        j = mid + 1
        for k in range(lo, hi + 1):
            aux[k] = a[k]  # 复制元素到辅助数组
        for k in range(lo, hi + 1):
            if i > mid:
                a[k] = aux[j]
                j += 1
            elif j > hi:
                a[k] = aux[i]
                i += 1
            elif aux[j] < aux[i]:
                a[k] = aux[j]
                j += 1
                self.result += mid-i+1  # 计算逆序对
            else:
                a[k] = aux[i]
                i += 1

if __name__ == '__main__':
    s = Solution()
    print(s.reversePairs([7,5,6,4]))

Explore

在归并排序中,左右两部分各自已经是排序好的。当合并时,如果某个右侧元素小于左侧的元素,说明这个右侧元素小于左侧当前元素及所有该元素右侧的元素(因为左侧已排序)。因此,每当右侧的元素小于左侧的元素时,逆序对的数量就是左侧数组中从该元素到中点的元素个数,即`mid - i + 1`。这是因为左侧数组的这部分元素都大于右侧的当前元素。

归并排序中递归的基本终止条件是子数组长度为1或0,即没有更多的分割可以进行。在题解中,`if lo >= hi`这个条件检查是为了确定子数组是否可以继续分割。如果`lo`大于等于`hi`,意味着子数组长度小于或等于1,因此不需要进一步分割或合并,确保了递归过程能够在正确的时刻结束。

在归并排序中,辅助数组`aux`用于暂存主数组的元素,以便在合并过程中可以正确地比较和重新排列元素。在`merge`函数中,首先将主数组`a`的元素复制到`aux`中,然后使用`aux`来比较元素并更新主数组`a`。使用辅助数组是为了避免在原数组上直接操作导致数据覆盖,确保在合并时原始元素顺序被保持,从而正确地合并两个有序子数组。

在`merge`函数中,`if i > mid`检查是否所有左侧部分的元素都已经被合并到主数组中,如果是,则剩下的元素都是右侧部分的,直接将这些元素复制回主数组。类似地,`elif j > hi`检查是否所有右侧部分的元素都已经被合并,如果是,则剩下的元素都是左侧部分的,也直接复制回主数组。这两个条件保证了不管合并过程中元素如何比较,所有元素最终都被正确地合并回主数组中,无论是从左侧还是右侧。