邻位交换的最小次数

标签: 贪心 双指针 字符串

难度: Medium

给你一个表示大整数的字符串 num ,和一个整数 k

如果某个整数是 num 中各位数字的一个 排列 且它的 值大于 num ,则称这个整数为 妙数 。可能存在很多妙数,但是只需要关注 值最小 的那些。

  • 例如,num = "5489355142"
    • 第 1 个最小妙数是 "5489355214"
    • 第 2 个最小妙数是 "5489355241"
    • 第 3 个最小妙数是 "5489355412"
    • 第 4 个最小妙数是 "5489355421"

返回要得到第 k最小妙数 需要对 num 执行的 相邻位数字交换的最小次数

测试用例是按存在第 k 个最小妙数而生成的。

 

示例 1:

输入:num = "5489355142", k = 4
输出:2
解释:第 4 个最小妙数是 "5489355421" ,要想得到这个数字:
- 交换下标 7 和下标 8 对应的位:"5489355142" -> "5489355412"
- 交换下标 8 和下标 9 对应的位:"5489355412" -> "5489355421"

示例 2:

输入:num = "11112", k = 4
输出:4
解释:第 4 个最小妙数是 "21111" ,要想得到这个数字:
- 交换下标 3 和下标 4 对应的位:"11112" -> "11121"
- 交换下标 2 和下标 3 对应的位:"11121" -> "11211"
- 交换下标 1 和下标 2 对应的位:"11211" -> "12111"
- 交换下标 0 和下标 1 对应的位:"12111" -> "21111"

示例 3:

输入:num = "00123", k = 1
输出:1
解释:第 1 个最小妙数是 "00132" ,要想得到这个数字:
- 交换下标 3 和下标 4 对应的位:"00123" -> "00132"

 

提示:

  • 2 <= num.length <= 1000
  • 1 <= k <= 1000
  • num 仅由数字组成

Submission

运行时间: 116 ms

内存: 17.4 MB

from sortedcontainers import SortedList


def kthNextPermutation(
    nums: List[int], k: int, inplace=False, prev=False
) -> Optional[List[int]]:
    """下k个字典序的排列(可以存在重复元素)

    Args:
        nums: 原排列数组.
        k (int): 后续第k个(`本身算第0个`)字典序的排列.
        inplace (bool, optional): 是否原地修改. 默认为False.
        prev (bool, optional): 使用next还是prev. 默认使用next.

    Returns:
        Optional[List[int]]: 下k个字典序的排列,如果不存在,返回None.
    """
    if not nums:
        return
    counter = Counter([nums[-1]])
    sl = (
        SortedList([nums[-1]])
        if not prev
        else SortedList([nums[-1]], key=lambda x: -x)
    )

    fac = 1
    facPtr = 1
    curPerm = 0
    overlap = 1  # 重复元素的个数的乘积
    allPerm = 1  # 后缀里的所有排列个数
    for right in range(len(nums) - 2, -1, -1):
        if curPerm + k < allPerm:
            break
        num = nums[right]
        counter[num] += 1
        overlap *= counter[num]

        smaller = 0
        pos = sl.bisect_left(num)
        if pos == len(sl) or sl[pos] != num:  # set去重
            sl.add(num)
        for i, pre in enumerate(sl):
            if i >= pos:
                break
            # for pre in sl.islice(0, pos):
            smaller += (fac * counter[pre]) // overlap

        facPtr += 1
        fac *= facPtr
        curPerm += smaller
        allPerm = fac // overlap

    if curPerm + k >= allPerm:
        return

    res = []
    fac //= facPtr
    permCount = 0
    target = curPerm + k
    while permCount != target:
        for i, pre in enumerate(sl):
            curPerm = (fac * counter[pre]) // overlap  # 以当前元素开头的排列个数
            cand = permCount + curPerm
            if cand <= target:
                permCount = cand
                continue
            facPtr -= 1
            fac //= facPtr
            overlap //= counter[pre]
            res.append(pre)
            counter[pre] -= 1
            if not counter[pre]:
                sl.pop(i)
            break

    for pre in sl:
        res += [pre] * counter[pre]
    if inplace:
        nums[-len(res) :] = res
        return nums
    return nums[: len(nums) - len(res)] + res


class BIT1:
    """单点修改,区间和查询"""

    __slots__ = "size", "bit", "tree"

    def __init__(self, n: int):
        self.size = n
        self.bit = n.bit_length()
        self.tree = [0] * (n + 1)

    def add(self, index: int, delta: int) -> None:
        # index 必须大于0
        while index <= self.size:
            self.tree[index] += delta
            index += index & -index

    def _query(self, index: int) -> int:
        res = 0
        while index > 0:
            res += self.tree[index]
            index -= index & -index
        return res

    def query(self, left: int, right: int) -> int:
        return self._query(right) - self._query(left - 1)


class Solution:
    def getMinSwaps(self, num: str, k: int) -> int:
        n = len(num)
        arr = [*map(int, num)]
        brr = arr.copy()
        brr = kthNextPermutation(brr, k)
        idx = defaultdict(list)
        for i, b in enumerate(brr, 2):
            idx[b].append(i)
        cnt = defaultdict(int)
        al = []
        for a in arr:
            al.append(idx[a][cnt[a]])
            cnt[a] += 1
        bit = BIT1(n + 10)
        res = 0
        for i in al:
            res += bit.query(i + 1, n + 5)
            bit.add(i, 1)
        return res

Explain

解决这个问题涉及两个主要步骤:1. 找到给定数字字符串的第k个字典序更大的排列。2. 计算从原始字符串到该排列通过相邻交换得到的最小次数。首先,使用'kthNextPermutation'函数来获取所需的排列,此函数实现了对给定数组的下一个排列算法,并能够处理有重复数字的情况。之后,利用索引映射和树状数组(Binary Indexed Tree, BIT),来计算把原数组转换到目标排列需要的最小交换次数。树状数组用于高效计算前缀和,以此来快速得出每次交换的次数。

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

空间复杂度: O(n)

from sortedcontainers import SortedList
from collections import Counter, defaultdict
from typing import List, Optional

class BIT1:
    '''单点修改,区间和查询'''    
    def __init__(self, n: int):
        self.size = n
        self.bit = n.bit_length()
        self.tree = [0] * (n + 1)

    def add(self, index: int, delta: int) -> None:
        while index <= self.size:
            self.tree[index] += delta
            index += index & -index

    def _query(self, index: int) -> int:
        res = 0
        while index > 0:
            res += self.tree[index]
            index -= index & -index
        return res

    def query(self, left: int, right: int) -> int:
        return self._query(right) - self._query(left - 1)

class Solution:
    def getMinSwaps(self, num: str, k: int) -> int:
        n = len(num)
        arr = [*map(int, num)]
        brr = arr.copy()
        brr = kthNextPermutation(brr, k)
        idx = defaultdict(list)
        for i, b in enumerate(brr, 2):
            idx[b].append(i)
        cnt = defaultdict(int)
        al = []
        for a in arr:
            al.append(idx[a][cnt[a]])
            cnt[a] += 1
        bit = BIT1(n + 10)
        res = 0
        for i in al:
            res += bit.query(i + 1, n + 5)
            bit.add(i, 1)
        return res

Explore

在`kthNextPermutation`函数中,通过多次应用下一个字典序排列的算法来确保找到第k个字典序更大的排列。具体做法是,从给定排列开始,反复调用一次计算下一个排列的函数,重复这个过程k次。每次调用都是基于当前排列找到字典序中的下一个排列,通过连续执行k次,最终达到第k个更大的排列。

树状数组(BIT)被选用是因为它在处理频繁的单点更新和区间求和查询时提供了较为高效的性能,且实现相比线段树更为简洁。在计算最小交换次数过程中,需要频繁地更新单个元素并查询区间和,BIT能够以对数时间复杂度处理这些操作,而简单数组处理区间求和时会更慢。相对于线段树,BIT在空间上更加节省且足够应对此问题的需求。

计算最小交换次数时,使用索引映射和树状数组可以高效地确定每个数字应该移动到的位置并计算必要的交换次数。具体步骤如下:首先,通过索引映射确定每个数字在目标排列中的位置。然后,使用树状数组来维护和查询已经放置在最终位置上的数字的数量。对于每个数字,通过查询树状数组可以快速得到在当前数字前已经正确放置的数字数量,从而计算出将当前数字移动到正确位置需要的交换次数。每处理一个数字后,更新树状数组以反映这一变化。

`al`数组在解决方案中的作用是记录每个数字在目标排列中的位置。它是这样构建的:遍历原始数列的每个数字,对于每个数字,使用索引映射(由目标排列定义)找到该数字在目标排列中应出现的位置,并将这些位置添加到`al`数组中。这个位置数组随后用于树状数组中,以帮助计算和跟踪每个数字到达其目标位置需要进行的交换次数。