点击获取AI摘要

2434. 使用机器人打印字典序最小的字符串 M

给你一个字符串 s 和一个机器人,机器人当前有一个空字符串 t 。执行以下操作之一,直到 st 都变成空字符串:

  • 删除字符串 s第一个 字符,并将该字符给机器人。机器人把这个字符添加到 t 的尾部。
  • 删除字符串 t最后一个 字符,并将该字符给机器人。机器人将该字符写到纸上。

请你返回纸上能写出的字典序最小的字符串。

示例 1:

输入:s = “zza”
输出:“azz”
解释:用 p 表示写出来的字符串。
一开始,p=“” ,s=“zza” ,t=“” 。
执行第一个操作三次,得到 p=“” ,s=“” ,t=“zza” 。
执行第二个操作三次,得到 p=“azz” ,s=“” ,t=“” 。

示例 2:

输入:s = “bac”
输出:“abc”
解释:用 p 表示写出来的字符串。
执行第一个操作两次,得到 p=“” ,s=“c” ,t=“ba” 。
执行第二个操作两次,得到 p=“ab” ,s=“c” ,t=“” 。
执行第一个操作,得到 p=“ab” ,s=“” ,t=“c” 。
执行第二个操作,得到 p=“abc” ,s=“” ,t=“” 。

示例 3:

输入:s = “bdda”
输出:“addb”
解释:用 p 表示写出来的字符串。
一开始,p=“” ,s=“bdda” ,t=“” 。
执行第一个操作四次,得到 p=“” ,s=“” ,t=“bdda” 。
执行第二个操作四次,得到 p=“addb” ,s=“” ,t=“” 。

提示:

  • 1 <= s.length <= 10⁵
  • s 只包含小写英文字母。

问题分析

如何通过将字符串 ss 中的字符依次移入一个栈 tt,然后从栈 tt 中弹出字符构建最终字符串 pp,使得 pp 的字典序最小。操作包括:

  1. ss 的头部取出字符追加到 tt 的尾部(栈顶)。
  2. tt 的尾部(栈顶)取出字符追加到 pp 的尾部。

目标是最小化最终的字符串 pp。我们希望尽早地将较小的字符放入 pp

算法思路

这是一个贪心问题。在任何时候,我们都可以选择从 ss 中取下一个字符放入栈 tt,或者从栈 tt 中取出栈顶字符放入 pp(如果 tt 不为空)。为了使 pp 字典序最小,我们应该优先将当前可用的最小字符放入 pp。当前可用的字符有两个来源:栈顶的字符 t[1]t[-1]ss 中剩余的所有字符。

ss 中取出的字符必须先经过栈 tt 才能进入 pp。因此,我们能直接放入 pp 的只有栈顶字符 t[1]t[-1]

关键的决策点在于:当前栈顶字符 t[1]t[-1] 是否应该立即放入 pp
如果 t[1]t[-1] 大于等于 ss 中剩余字符的最小者,那么如果现在放入 t[1]t[-1],可能会导致后面更小的字符被压在栈底,或者不得不将这个相对较大的字符提前放入 pp。在这种情况下,更好的策略是先将 ss 中更小的字符移入栈 tt,希望它们能在某个时候被弹出。
如果 t[1]t[-1] 小于等于 ss 中剩余字符的最小者,那么 t[1]t[-1] 是当前所有可用的字符(栈顶和 ss 中剩余的)中最小的(或之一)。此时,应该立即将 t[1]t[-1] 放入 pp,因为它是当前最优的选择。

基于此,贪心策略如下:

  1. 预先计算出 ss 的每一个后缀的最小字符。设 min_suffix[i]min\_suffix[i]s[i:]s[i:] 的最小字符。
  2. 遍历 ss 的字符,依次将其推入栈 tt。在每次推入之前(或之后),检查栈顶字符 t[1]t[-1]
  3. 如果栈 tt 不为空,并且栈顶字符 t[1]t[-1] 小于等于当前 ss 中剩余字符的最小者(即 min_suffix[s_ptr]min\_suffix[s\_ptr],其中 s_ptrs\_ptrss 中下一个要处理的字符索引),则重复将栈顶字符弹出并追加到 pp 中,直到栈为空或栈顶字符大于 min_suffix[s_ptr]min\_suffix[s\_ptr]
  4. ss 的当前字符推入栈 tt
  5. ss 中的所有字符都已处理完毕后,将栈 tt 中剩余的所有字符依次弹出并追加到 pp 中。

时间复杂度

  1. 预计算后缀最小值:需要遍历字符串 ss 一次,时间复杂度为 O(N)O(N),其中 NN 是字符串 ss 的长度。
  2. 模拟机器人操作:
    • 每个字符从 ss 被推入栈 tt 一次。
    • 每个字符最多从栈 tt 被弹到 pp 一次。
    • 栈的推入和弹出操作总次数是 O(N)O(N)
    • 追加到 pp 的操作总次数是 O(N)O(N)
    • 内层 while 循环的总执行次数(所有外层循环迭代加起来)不会超过栈的总弹出次数,即 O(N)O(N)
      总的时间复杂度为 O(N)+O(N)=O(N)O(N) + O(N) = O(N)

O(N)O(N)

代码分解

  1. 预计算后缀最小值: 创建一个与 ss 同等长度的列表 min_suffix。从后向前填充,min_suffix[i] 存储 s[i:]s[i:] 的最小字符。
    1
    2
    3
    4
    min_suffix = [None] * n
    min_suffix[n - 1] = s[n - 1]
    for i in range(n - 2, -1, -1):
    min_suffix[i] = min(s[i], min_suffix[i + 1])
  2. 初始化: 创建一个空列表 t 作为栈,一个空列表 p 用于存储结果字符,以及一个指针 s_ptr 初始化为 0 指向 ss 的头部。
    1
    2
    3
    t = []
    p = []
    s_ptr = 0
  3. 主循环:s_ptr 小于 nn 时,表示 ss 中还有字符未处理。
    1
    2
    3
    while s_ptr < n:
    # ... 处理逻辑 ...
    s_ptr += 1 # 移动指针到 s 的下一个字符
  4. 贪心弹出: 在主循环内部,获取当前 ss 中剩余字符的最小者 current_min_in_s = min_suffix[s_ptr]。只要栈 t 不为空且栈顶字符小于等于 current_min_in_s,就将栈顶字符弹出并追加到 p 中。
    1
    2
    3
    current_min_in_s = min_suffix[s_ptr]
    while t and t[-1] <= current_min_in_s:
    p.append(t.pop())
  5. 推入栈:s[s_ptr]s[s\_ptr] 推入栈 t
    1
    t.append(s[s_ptr])
  6. 处理剩余栈: 主循环结束后,ss 中的所有字符都已进入或经过栈 tt。将栈 tt 中剩余的所有字符依次弹出并追加到 pp 中。
    1
    2
    while t:
    p.append(t.pop())
  7. 构建结果:p 中的字符连接成最终字符串。
    1
    return "".join(p)

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution:
def robotWithString(self, s: str) -> str:
n = len(s)
if n == 0:
return ""

# 1. 预计算后缀最小值
# min_suffix[i] 存储 s[i:] 中的最小字符
min_suffix = [None] * n
min_suffix[n - 1] = s[n - 1]
for i in range(n - 2, -1, -1):
min_suffix[i] = min(s[i], min_suffix[i + 1])

# 2. 模拟机器人操作
t = [] # 使用列表作为栈
p = [] # 使用列表构建结果字符串
s_ptr = 0 # 指向 s 的当前头部

while s_ptr < n:
# 当前 s 中剩余字符的最小者
# 如果 s_ptr >= n,表示 s 已空,此时 min_suffix[s_ptr] 不存在
# 但我们在循环条件中保证了 s_ptr < n
current_min_in_s = min_suffix[s_ptr]

# 贪心判断并弹出:如果栈不为空且栈顶 <= s 中剩余的最小字符
# 注意:这里需要循环弹出,直到不满足条件
while t and t[-1] <= current_min_in_s:
p.append(t.pop())

# 将 s 的当前字符推入栈 t
t.append(s[s_ptr])
s_ptr += 1

# 3. s 处理完毕后,将栈 t 中剩余的字符全部弹出
# 此时 s 中剩余的最小字符可视作无穷大,栈顶字符总是小于等于它
while t:
p.append(t.pop())

# 4. 构建最终结果字符串
return "".join(p)