一文解决逆序数


目录:

预计阅读时间:8 分钟

一文解决逆序数

数组里满足 i < j 且 a[i] > a[j] 的一对下标叫一个逆序对,逆序数就是这种对的总数。一道逆序数,四种写法,从面试到竞赛一网打尽。计算逆序数有两种思路:

  1. 归并排序:合并左右两个已排好序的半段时,每当右半段的元素先出列,答案就加上左半段还没出列的元素个数(面试版代码里的 n - p1)。两边相等时让左边先出列,所以比较用严格的 >:写成 >= 会把相等的数也算成逆序,[2,2,2,2] 会得到 6 而不是 0;
  2. 值域计数:先把值离散化成名次 rank(值在去重排序后的位置,取 1 到 m,m 是不同值的个数),从左往右逐个把元素 rank 位置的计数加一;当前元素的逆序贡献(即它左边比它大的元素个数)等于加一之前值域区间 $[\text{rank}+1, m]$ 上的计数和——需要单点修改 + 区间求和的数据结构。

复杂度:归并排序时间 $O(n \log n)$、额外空间 $O(n)$,下面切片版和索引版的复杂度相同,只是常数不同;值域计数先排序离散化,再做 n 次单点修改和区间查询,时间也是 $O(n \log n)$,空间上树状数组开长度 m + 1 的数组,即 $O(m)$,线段树开长度 4m 的数组。

leetcode 上有一道数据规模较小的模板题(长度不超过 $5 \times 10^4$,洛谷 P1908 的 n 到 $5 \times 10^5$),下面四份代码都以它为载体。

思路一:归并排序

面试版(切片归并)

一般来说面试写出这版就够了:

class Solution:
    def reversePairs(self, record: List[int]) -> int:
        a = record
        if a == []:
            return 0
        ans = 0
        def change(nums1, nums2):
            nonlocal ans
            p1 = 0
            p2 = 0
            n = len(nums1)
            m = len(nums2)
            res = []
            while p1 < n and p2 < m:
                if nums1[p1] > nums2[p2]:
                    res.append(nums2[p2])
                    p2 += 1
                    ans += (n - p1)
                else:
                    res.append(nums1[p1])
                    p1 += 1
            res.extend(nums1[p1: ])
            res.extend(nums2[p2: ])
            return res
        def merge(nums):
            n = len(nums)
            if n == 1:
                return nums
            left = 0
            right = n - 1
            mid = (left + right) // 2
            l = merge(nums[: mid + 1])
            r = merge(nums[mid + 1: ])
            return change(l, r)
        merge(a)
        return ans

这版的函数名起反了:merge 实际是递归拆分、排序,change 才是合并两个有序段。

索引版(下标归并)

这版不切片,按下标区间 [l, r] 递归,辅助数组 res 只开一次,合并时先写进 res,再回写到原数组的这一段。它的用处是结构干净:不反复切片建新列表,辅助数组只开一次,几乎可以逐行翻成 C++ / Java。它在 CPython 里并不比切片版快:$5 \times 10^5$ 个随机数实测,Python 3.12 上切片版 0.50 秒、这版 0.71 秒,Python 3.9 上 0.90 秒对 1.17 秒。

class Solution:
    def reversePairs(self, record: List[int]) -> int:
        a = record
        if a == []:
            return 0
        ans = 0
        res = [0] * len(a)
        def change(l, mid, r):
            nonlocal ans
            p1 = l
            p2 = mid + 1
            n = mid - l + 1
            k = l
            while p1 <= mid and p2 <= r:
                if a[p1] > a[p2]:
                    res[k] = a[p2]
                    p2 += 1
                    ans += (n - p1 + l)
                else:
                    res[k] = a[p1]
                    p1 += 1
                k += 1
            while p1 != mid + 1:
                res[k] = a[p1]
                k += 1
                p1 += 1
            while p2 != r + 1:
                res[k] = a[p2]
                k += 1
                p2 += 1
            for i in range(l, r + 1):
                a[i] = res[i]
        def merge(left, right):
            if left >= right:
                return
            mid = (left + right) // 2
            merge(left, mid)
            merge(mid + 1, right)
            change(left, mid, right)
        merge(0, len(a) - 1)
        return ans

思路二:值域计数

第二种思路需要「单点修改 + 区间求和」,线段树、树状数组都能胜任,两种都写一遍。值先离散化成 rank。

线段树

class Solution:
    def reversePairs(self, record: List[int]) -> int:
        a = record
        class segment_tree:
            def __init__(self, nums):
                n = len(nums)
                self.nums = nums
                self.tree = [0] * 4 * n

            def update(self, root):
                self.tree[root] = self.tree[root * 2] + self.tree[root * 2 + 1]

            def build(self, left, right, root):
                if left > right:
                    return
                if left == right:
                    self.tree[root] = self.nums[left - 1]
                    return
                mid = (left + right) // 2
                self.build(left, mid, root * 2)
                self.build(mid + 1, right, root * 2 + 1)
                self.update(root)

            def change(self, A, v, left, right, root):
                if left > right:
                    return
                if left == right:
                    self.tree[root] = v
                    return
                mid = (left + right) // 2
                if left <= A <= mid:
                    self.change(A, v, left, mid, root * 2)
                if mid + 1 <= A <= right:
                    self.change(A, v, mid + 1, right, root * 2 + 1)
                self.update(root)

            def query(self, A, B, left, right, root):
                if A <= left <= right <= B:
                    return self.tree[root]
                mid = (left + right) // 2
                res = 0
                if A <= mid:
                    res += self.query(A, B, left, mid, root * 2)
                if B >= mid + 1:
                    res += self.query(A, B, mid + 1, right, root * 2 + 1)
                return res
        def get_ranks(nums):
            new_nums = sorted(list(set(nums)))
            hp = {v: r + 1 for r, v in enumerate(new_nums)}
            return hp
        hp = get_ranks(a)
        new_a = [0] * len(hp)
        tree = segment_tree(new_a)
        tree.build(1, len(hp), 1)
        ans = 0
        for i in a:
            p = hp[i]
            if p + 1 <= len(hp):
                ans += tree.query(p + 1, len(hp), 1, len(hp), 1)
            temp = tree.query(p, p, 1, len(hp), 1)
            tree.change(p, temp + 1, 1, len(hp), 1)
        return ans

树状数组

同样的思路,树状数组写起来短得多:

class Solution:
    def reversePairs(self, record: List[int]) -> int:
        a = record
        class binary_indexed_tree:
            def __init__(self, nums):
                self.n = len(nums)
                self.nums = nums
                self.tree = [0] * (self.n + 1)

            def lowbit(self, x):
                return x & -x

            def change(self, pos, v):
                while pos <= self.n:
                    self.tree[pos] += v
                    pos += self.lowbit(pos)

            def query(self, pos):
                res = 0
                while pos > 0:
                    res += self.tree[pos]
                    pos -= self.lowbit(pos)
                return res

        def get_ranks(nums):
            new_nums = sorted(list(set(nums)))
            hp = {v: r + 1 for r, v in enumerate(new_nums)}
            return hp

        hp = get_ranks(a)
        new_a = [0] * len(hp)
        tree = binary_indexed_tree(new_a)
        ans = 0
        for i in a:
            p = hp[i]
            ans += tree.query(len(hp)) - tree.query(p)
            tree.change(p, 1)
        return ans

区间和用两次前缀查询相减:query(len(hp)) - query(p)。其中 query(len(hp)) 恒等于已经插入的元素个数,也可以直接用循环下标代替,省掉一次查询。


本文由 aboom 原创,转载请注明出处。

📖相关推荐