给定一个字符串,对该字符串进行删除操作,保留 k 个字符且相对位置不变,使字典序最小

这是一个经典的编程问题,可以用 单调栈 的方法高效解决。以下是解题步骤和代码实现:


问题描述

给定一个字符串 s 和一个整数 k,要求删除字符串中的一些字符,最终保留 k 个字符,且相对顺序不变,使得结果字符串字典序最小。


解题思路

  1. 单调栈维护最小字典序:

    • 使用一个栈来维护当前最小的字典序字符。
    • 遍历字符串 s,尝试将每个字符压入栈。
    • 如果栈顶字符大于当前字符,并且后面还有足够的字符可以填满栈,则弹出栈顶字符。
    • 最终栈中保留的就是字典序最小的字符。
  2. 边界条件:

    • 栈的大小不能超过 k。
    • 遍历时要确保剩下的字符足够填满栈(栈中已保留字符 + 剩余未处理字符 >= k)。

Python代码实现

python 复制代码
def removeToKeepKMin(s: str, k: int) -> str:
    stack = []  # 用于存放最终结果字符的栈
    n = len(s)  # 字符串长度

    for i, char in enumerate(s):
        # 当栈非空,且栈顶字符比当前字符大,且后面还有足够字符时,可以弹出栈顶
        while stack and stack[-1] > char and len(stack) + (n - i) > k:
            stack.pop()
        # 如果栈长度小于k,则可以压入当前字符
        if len(stack) < k:
            stack.append(char)
    
    # 最终栈中的字符就是结果
    return ''.join(stack)

示例

示例 1
python 复制代码
s = "bcabc"
k = 3
print(removeToKeepKMin(s, k))  # 输出: "abc"

解释:

  • 删除字符串中的第一个 'b' 和第二个 'c',保留 "abc",使得字典序最小。
示例 2
python 复制代码
s = "cbacdcbc"
k = 4
print(removeToKeepKMin(s, k))  # 输出: "acdb"

解释:

  • 删除字符 'c' 和 'b' 后,保留 "acdb",使得字典序最小。

复杂度分析

  1. 时间复杂度:O(n),每个字符最多入栈一次,出栈一次。
  2. 空间复杂度 :O(k),栈的最大大小为 k。

这个问题也可以通过动态规划(DP)来解决。不过相较于单调栈,动态规划的时间复杂度和实现相对更复杂一些。

以下是动态规划的解法思路:


动态规划解法思路

状态定义

我们定义一个二维数组 dp[i][j] 表示从字符串的前 i 个字符中,选择 j 个字符所能获得的字典序最小的字符串。

  • i 是字符串前缀长度;
  • j 是要保留的字符个数;
  • dp[i][j] 表示从前 i 个字符中选 j 个字符的最优解(字典序最小)。
状态转移

在遍历字符串的过程中,对于每个字符,我们有两种选择:

  1. 不选当前字符 :如果不选当前字符,那么问题退化为从前 i-1 个字符中选择 j 个字符,即 dp[i][j] = dp[i-1][j]。
  2. 选当前字符 :如果选当前字符,那么我们需要从前 i-1 个字符中选择 j-1 个字符,再加上当前字符,即 dp[i][j] = dp[i-1][j-1] + s[i-1]。

状态转移方程如下:

dp\[i\]\[j\] = \\min(dp\[i-1\]\[j\], dp\[i-1\]\[j-1\] + s\[i-1\])

边界条件
  1. 当 j == 0(不保留字符时),结果为空字符串:dp[i][0] = "";
  2. 当 i == 0 且 j > 0(字符串为空时,无法选择任何字符):dp[0][j] 不存在,为无穷大(不可达)。
最终结果

最后的答案是 dp[n][k],其中 n 是字符串长度,k 是要保留的字符数。


动态规划代码实现

以下是基于上述思路的 Python 实现:

python 复制代码
def removeToKeepKMinDP(s: str, k: int) -> str:
    n = len(s)
    # dp[i][j] 表示从前 i 个字符中选择 j 个字符的字典序最小字符串
    dp = [["" for _ in range(k + 1)] for _ in range(n + 1)]

    # 初始化边界条件
    for i in range(n + 1):
        dp[i][0] = ""  # 选择 0 个字符时为空字符串
    
    for j in range(1, k + 1):
        dp[0][j] = "~"  # 不可能从空字符串中选择字符,用 "~" 表示无穷大(字典序最大字符)

    # 动态规划填表
    for i in range(1, n + 1):
        for j in range(1, k + 1):
            # 如果不选当前字符
            option1 = dp[i-1][j]
            # 如果选当前字符
            option2 = dp[i-1][j-1] + s[i-1] if j <= i else "~"  # 保证 j <= i
            # 取字典序最小的结果
            dp[i][j] = min(option1, option2)
    
    return dp[n][k]

示例

示例 1
python 复制代码
s = "bcabc"
k = 3
print(removeToKeepKMinDP(s, k))  # 输出: "abc"

过程:

  1. 初始化 dp 表:
    • dp[i][0] 初始化为 "";
    • dp[0][j] 初始化为 "~"。
  2. 填表,逐步从前缀中选择字符并更新最优解。
  3. 最终 dp[5][3] = "abc"。

示例 2
python 复制代码
s = "cbacdcbc"
k = 4
print(removeToKeepKMinDP(s, k))  # 输出: "acdb"

时间复杂度

  1. 时间复杂度 :O(n * k),n 是字符串长度,k 是需要保留的字符数。
    • 每个 dp[i][j] 都取决于上一步的状态,因此需要遍历整个 dp 表。
  2. 空间复杂度 :O(n * k),用于存储 dp 表。

相比单调栈方法,动态规划的复杂度更高,但它提供了更通用的思路,能够很好地解决其他类似问题。


总结

  • 如果问题的输入规模较小,可以使用动态规划方法。
  • 如果需要更高效的实现,单调栈是更优的选择,时间复杂度为 O(n),空间复杂度为 O(k)。
相关推荐
【心态好不摆烂】16 分钟前
数据结构基础精讲3
数据结构·算法
2601_967979121 小时前
pip在Python中的9个必须知道的用法
python·pip·版本控制·包管理工具·requirements.txt
漂流瓶jz1 小时前
UVA-1613 K度图的着色 题解答案代码 算法竞赛入门经典第二版
算法·深度优先·图论·贪心·aoapc·算法竞赛入门经典·uva
曲径海盗1 小时前
【Python 学习第 58 天】列表方法
python·学习笔记·编程入门
DeepVisionary1 小时前
Runway开源机器人大模型Praxis-1,推演延迟压到62毫秒
python·自动化
Pniubi2 小时前
动态规划入门 ( - 力扣509斐波那契数)
算法·动态规划
Cooper253 小时前
DDC 命令成功但屏幕不动:用 gamma 表重做亮度映射
python·mac
吴长建先生重名了3 小时前
GitHub Actions部署报表系统踩坑记:幂等性与限
算法
师夷长技以自愈3 小时前
手搓 AI Agent 第 7 篇:菜做砸了别硬炒——错误分类 + 双账本熔断
人工智能·python
用户956256254293 小时前
15 分钟社媒简报 Agent:我只用了搜索和模型两步
python·api