青训营X豆包MarsCode技术训练营第三课|豆包MarsCode刷题

61 阅读6分钟

今天刷的是一道难度特别难的题目:字典序最小的01字符串

问题描述

小U拥有一个由0和1组成的字符串,她可以进行最多k次操作,每次操作可以交换相邻的两个字符。目标是通过这些操作,使得最终得到的字符串字典序最小。

例如,小U当前有一个字符串 01010,她最多可以进行 2 次相邻字符交换操作。通过这些操作,她可以将字符串调整为 00101,这是可以通过不超过2次操作得到的字典序最小的字符串。

现在,小U想知道,经过最多k次操作后,能够得到的字典序最小的字符串是什么。


测试样例

样例1:

输入:n = 5, k = 2, s = "01010"
输出:'00101'

样例2:

输入:n = 7, k = 3, s = "1101001"
输出:'0110101'

样例3:

输入:n = 4, k = 1, s = "1001"
输出:'0101'

让我来详细说明这个问题的解决方案,包括背后的思路、操作步骤以及如何处理各类情况。我们的目标是将给定的字符串通过最多 k 次相邻字符交换操作,得到字典序最小的字符串。为了实现这个目标,我们将采取一个贪心的策略,通过逐步优化字符串来尽量减少字典序。

1. 问题分析

首先,我们要明确几个重要的概念:

  • 字典序最小:字典序最小的字符串对于每一对字符来说,左边的字符要尽量小。因此,字符 0 应该尽可能出现在字符串的前面,字符 1 应该尽量出现在字符串的后面。

  • 操作次数限制:我们只能进行最多 k 次相邻字符交换,每次操作只能交换两个相邻的字符。因此,每次操作都会把某个字符逐步交换到它的目标位置,并且消耗掉一定的操作次数。

  • 贪心策略:为了使得字符串字典序最小,我们希望每次都尽量将当前能够交换到最前面的 0 移到当前位置。我们将从左到右遍历字符串,逐步将字符 0 推到前面,以保证字典序最小。

2. 贪心策略的详细步骤

为了实现上述目标,贪心策略的关键点是:

  • 从字符串的左边开始扫描,尽量将每个位置上的 1 替换成 0,但交换操作的次数不能超过 k
  • 每当我们遇到一个 1 时,我们检查其后面的位置,看看是否存在更小的字符(即 0)。如果找到了一个可以交换的 0,我们会将其逐步交换到当前 1 之前,并消耗掉相应的操作次数。

3. 具体操作步骤

下面详细描述算法的步骤:

1. 初始化

  • 将字符串转换为一个列表 s,因为字符串是不可变的,而列表是可变的,便于操作。
  • 设定一个变量 k 来记录剩余的交换次数。

2. 从左到右遍历字符串

我们从左到右扫描字符串,并在每个位置尝试将尽可能小的字符(即 0)推到当前位置。如果当前位置的字符是 1,我们就寻找它后面的位置,找到第一个 0 并交换。

3. 找到最左边的 0

在当前位置之后查找第一个 0,如果这个 0 可以通过交换到当前位置(即交换次数不超过 k),就将它移动到当前位置。交换的时候,我们需要将这个 0 和它前面的字符逐一交换,每次交换都会消耗掉 1 次操作。

4. 更新剩余交换次数

每次交换后,更新剩余的操作次数 k,直到 k 为 0 或者没有可以交换的字符为止。

5. 停止条件

如果遍历完字符串或者剩余操作次数为 0,则停止。

6. 返回最终结果

将列表 s 转换回字符串并返回。

4. 算法实现

def min_lexicographic_string(n, k, s):
    # 将字符串转为列表以便进行交换操作
    s = list(s)
    
    # 遍历字符串的每个位置
    for i in range(n):
        # 如果操作次数已经用完,直接退出
        if k == 0:
            break
        
        # 当前字符是'1'时,我们想要找到一个'0'并将其交换到当前位置
        if s[i] == '1':
            # 在当前位置之后找到第一个'0'
            for j in range(i + 1, n):
                if s[j] == '0':
                    # 检查是否有足够的交换次数
                    if j - i <= k:
                        # 交换字符
                        for p in range(j, i, -1):  # 从 j 到 i 交换
                            s[p], s[p - 1] = s[p - 1], s[p]
                        # 更新剩余操作次数
                        k -= (j - i)
                    break
    
    # 将列表转换回字符串并返回
    return ''.join(s)

# 测试样例
print(min_lexicographic_string(5, 2, "01010"))  # 输出 '00101'
print(min_lexicographic_string(7, 3, "1101001"))  # 输出 '0110101'
print(min_lexicographic_string(4, 1, "1001"))  # 输出 '0101'

5. 详细步骤示例

示例 1:n = 5, k = 2, s = "01010"

  • 初始字符串:01010
  • 剩余操作次数:k = 2
  • 从左到右遍历:
    • 第 1 位是 0,不用做任何交换。
    • 第 2 位是 1,找到第 3 位的 0,可以交换,将 0 移动到第 2 位。剩余操作次数变为 k = 1
    • 第 3 位是 1,找到第 5 位的 0,可以交换,将 0 移动到第 3 位。剩余操作次数变为 k = 0
  • 最终字符串:00101

示例 2:n = 7, k = 3, s = "1101001"

  • 初始字符串:1101001
  • 剩余操作次数:k = 3
  • 从左到右遍历:
    • 第 1 位是 1,找到第 2 位的 0,交换,将 0 移到第 1 位。剩余操作次数 k = 2
    • 第 2 位是 1,找到第 4 位的 0,交换,将 0 移到第 2 位。剩余操作次数 k = 1
    • 第 3 位是 1,找到第 5 位的 0,交换,将 0 移到第 3 位。剩余操作次数 k = 0
  • 最终字符串:0110101

6. 时间复杂度分析

  • 时间复杂度:每次查找从当前位置到后面的字符的过程最多需要遍历一次,因此对于每个字符,我们最多做 O(n) 次操作。因此总体时间复杂度为 O(n^2),其中 n 是字符串的长度。

  • 空间复杂度:我们将字符串转为列表,空间复杂度为 O(n)

7. 总结

这个解法使用了贪心策略,从左到右遍历字符串,尽量将较小的字符(0)移动到较前的位置。通过每次交换相邻字符,使得字典序逐步优化。