青训营X豆包MarsCode 技术训练营刷题第19题字典序最小的01字符串题目解析 | 豆包MarsCode AI 刷题

90 阅读6分钟

问题描述

小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'

思路分析

  1. 目标: 我们希望以最少的操作次数,将'0'移动到靠近字符串左侧的位置,使得字符串尽可能字典序最小。

  2. 操作限制: 每次操作只能交换相邻字符,并且最多可以进行 kkk 次操作。

  3. 字典序的本质

    • 字典序的大小本质上是字符串在排序中的位置。例如,00101 < 01010,因为第一个字符开始的比较结果已经决定了顺序。
    • 因此,为了使字典序最小,我们需要尽量将更多的'0'尽早地移动到左边。
  4. 局部贪心策略

    • 我们通过每次操作将某个靠右的'0'向左“挪动”,这个挪动是局部最优的,能够在当前位置尽快摆正一个'0'。
    • 这个局部贪心在实际操作中会导致一个逐渐逼近全局最优解的过程,随着 kkk 次操作逐渐消耗,结果逐步接近最终目标字符串。
  5. 贪心有效性证明

    • 假设贪心未能使结果字典序最小,存在另一种更优解。那么这种解必然是某些字符在更靠左的位置。
    • 但贪心策略每次都选择将最近的'0'尽可能早地移动到当前可用最左位置,所以其行为实际上等效于直接寻找全局最优解。
  6. 实现细节

    • 遍历字符串,寻找每个'0'的最佳位置。
    • 计算需要的操作次数:如果当前'0'在索引 iii,它想移动到位置 jjj,需要 j−ij - ij−i 次操作。
    • 如果 j−i≤kj - i \leq kj−i≤k,将该'0'直接移动到目标位置,并更新 kkk 和字符串;否则停止移动。
  7. 复杂度

    • 时间复杂度:O(n2)O(n^2)O(n2),最坏情况下每个字符需要与所有其他字符交换。
    • 空间复杂度:O(n)O(n)O(n),用于存储结果。

思路拓展

1. 贪心策略与堆优化

当前的解法是逐字符贪心,这在最坏情况下可能达到 O(n2)O(n^2)O(n2) 的复杂度。如果我们希望进一步优化,可以用 堆(Heap) 或其他更高效的数据结构:

  • 用最小堆存储所有的'0'的位置,优先将堆顶(最靠近目标位置的'0')移动到目标位置。
  • 每次移动后更新堆中剩余'0'的位置,复杂度可以降低到 O(nlog⁡n)O(n \log n)O(nlogn)。

2. 动态规划解法

另一种解法是用 动态规划 思路。我们可以将问题转化为:

  • 状态定义dp[i][j] 表示前 iii 个字符中,用 jjj 次操作可以得到的字典序最小字符串。

  • 状态转移

    • 如果当前不移动字符:dp[i][j] = dp[i-1][j] + s[i]
    • 如果移动字符:找到所有可能将字符移动到位置 iii 的方案,选择操作次数最少的一种。
  • 结果获取dp[n][k] 即为最终结果。

这种方法在 kkk 较小时表现优越,但对于 k≈n2k \approx n^2k≈n2 的情况复杂度会显得较高。


3. 与最小交换问题的关系

该问题与“最小交换次数使数组有序”问题有一定联系,区别在于这里的操作仅允许交换相邻字符。可以尝试使用以下方法分析:

  • 构造目标字符串(将所有'0'移到左边)。
  • 比较初始字符串与目标字符串,逐步减少两者之间的偏差。
  • 利用 BFS 或 DFS 搜索所有可能的操作路径,找到操作次数最小的解。

优化与变种问题

1. 操作次数有限的其他限制

如果限制允许交换的字符种类(如只允许'1'交换到更后的位置),则可以引入更复杂的规则判断策略。

2. 多次执行问题

如果问题要求多次重复类似操作,可以考虑提前缓存结果或者优化重复子问题的解决方案。

3. 多字符串比较

如果有多个字符串和不同的 kkk 值,如何找到最优解组合?这可能涉及排序与多维动态规划的组合分析。

代码详解

def solution(n: int, k: int, s: str) -> str:
   s = list(s)  # 转为列表以便修改
   i = 0        # 指向当前处理的字符位置
   
   for j in range(n):
        if s[j] == '0':
            # 计算将当前 0 移动到位置 i 所需的交换次数
            swaps_needed = j - i
            # 如果剩余的 k 足够进行移动
            if k >= swaps_needed:
                # 移动该 0 到位置 i
                s.pop(j)
                s.insert(i, '0')
                # 更新 k 和 i
                k -= swaps_needed
                i += 1
            else:
                # 如果 k 不足以移动到位置 i,则移动到 k 所允许的最远位置
                s.pop(j)
                s.insert(j - k, '0')
                break
    
    return ''.join(s)

if __name__ == '__main__':
    print(solution(5, 2, "01010") == '00101')
    print(solution(7, 3, "1101001") == '0110101')
    print(solution(4, 1, "1001") == '0101')

总结

这类题目本质上是一个关于局部最优到全局最优的动态调整过程。贪心算法因为其直接和高效的特性,在这类邻近交换、字典序调整的问题中非常适用。而通过动态规划、堆优化或其他高级算法,我们还能进一步提升复杂问题的求解能力。

同时,这道题还可以引申到许多更复杂的问题(如部分排序、字符串匹配等),使得其学习与理解价值进一步提升!