2434. Using a Robot to Print the Lexicographically Smallest String
Problem Description
You have a string s
and a robot that holds an empty string t
. The robot can perform two operations repeatedly until both strings are empty:
- Operation 1: Take the first character from string
s
and append it to the end of stringt
- Operation 2: Take the last character from string
t
and write it on paper (removing it fromt
)
Your goal is to determine the lexicographically smallest string that can be written on the paper by choosing the operations strategically.
The process works like this:
- String
s
acts as an input queue where you can only remove from the front - String
t
acts as a stack where the robot can append characters to the end (froms
) or remove from the end (to write on paper) - Characters written on paper form the final result string in the order they are written
For example, if s = "bac"
:
- You could take 'b' from
s
tot
(nowt = "b"
,s = "ac"
) - Take 'a' from
s
tot
(nowt = "ba"
,s = "c"
) - Write 'a' from
t
to paper (nowt = "b"
, paper has "a") - Write 'b' from
t
to paper (nowt = ""
, paper has "ab") - Take 'c' from
s
tot
(nowt = "c"
,s = ""
) - Write 'c' from
t
to paper (final result: "abc")
The challenge is to find the optimal sequence of operations that produces the lexicographically smallest possible result string.
Intuition
The key insight is that string t
acts like a stack, and we want to write characters to paper in lexicographically smallest order. To achieve this, we should write a character from t
to paper when it's advantageous to do so.
When should we write a character from t
to paper? We should write it when:
- The character at the top of
t
is smaller than or equal to any character we could potentially get froms
in the future
This is because if we wait and take more characters from s
first, we might end up having to write larger characters before we can access the smaller one that's currently at the top of t
.
To make this decision, we need to know what's the smallest character remaining in s
at any point. If the top of our stack t
has a character that's less than or equal to the smallest remaining character in s
, we should write it immediately. This ensures we're always writing the smallest possible character at each step.
For example, if t
has 'b' at the top and the smallest remaining character in s
is 'c', we should write 'b' now. If we don't, we'd have to stack 'c' on top of 'b', and then we'd be forced to write 'c' before 'b', resulting in a lexicographically larger string.
The strategy becomes:
- Keep track of the count of each character remaining in
s
- Maintain the current minimum character that hasn't been exhausted in
s
- For each character we take from
s
, push it to the stackt
- After pushing, check if the top of the stack is less than or equal to the minimum remaining character - if so, pop and write it
- Keep popping and writing as long as this condition holds
This greedy approach ensures that at each step, we're making the locally optimal choice that leads to the globally optimal (lexicographically smallest) result.
Solution Approach
The implementation uses a greedy approach with a stack to build the lexicographically smallest result string.
Data Structures Used:
cnt
: A Counter (frequency map) to track the count of each character remaining in strings
stk
: A list acting as a stack to represent the robot's stringt
ans
: A list to collect characters written to papermi
: A variable tracking the smallest character that still exists in the unprocessed part ofs
Algorithm Steps:
-
Initialize the frequency counter: Create
cnt = Counter(s)
to know how many of each character we have in total. -
Set initial minimum: Start with
mi = 'a'
as the smallest possible character. -
Process each character in
s
:- Decrement the count:
cnt[c] -= 1
(we're removing this character from the remaining pool) - Update the minimum character: While the current
mi
has count 0, increment it to the next characterwhile mi < 'z' and cnt[mi] == 0: mi = chr(ord(mi) + 1)
- Push the current character onto the stack:
stk.append(c)
- Pop and write characters when beneficial: While the stack is not empty and its top element is less than or equal to
mi
, pop it and add to the answerwhile stk and stk[-1] <= mi: ans.append(stk.pop())
- Decrement the count:
-
Return the result: Join all characters in
ans
to form the final string.
Why this works:
The critical insight is in the condition stk[-1] <= mi
. When the top of the stack is less than or equal to the smallest remaining character in s
, we should write it immediately. This is because:
- If
stk[-1] < mi
: Writing it now gives us a smaller character in our result than any future character froms
- If
stk[-1] == mi
: Writing it now is at least as good as waiting, and it might allow us to access even smaller characters buried deeper in the stack
By continuously updating mi
and making greedy decisions to pop from the stack whenever possible, we ensure that we're always writing the smallest available character at each position, resulting in the lexicographically smallest final string.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with s = "bdbc"
.
Initial Setup:
cnt = {'b': 2, 'd': 1, 'c': 1}
(frequency of each character)stk = []
(empty stack)ans = []
(empty result)mi = 'a'
(starting minimum)
Step 1: Process 'b' (index 0)
cnt['b'] -= 1
→cnt = {'b': 1, 'd': 1, 'c': 1}
- Update
mi
: 'a' has count 0, so increment →mi = 'b'
- Push 'b' to stack →
stk = ['b']
- Check if we should pop: Is 'b' ≤ 'b'? Yes! Pop 'b' and write it →
ans = ['b']
,stk = []
Step 2: Process 'd' (index 1)
cnt['d'] -= 1
→cnt = {'b': 1, 'd': 0, 'c': 1}
- Update
mi
: 'b' still has count 1, somi = 'b'
- Push 'd' to stack →
stk = ['d']
- Check if we should pop: Is 'd' ≤ 'b'? No! Keep 'd' in stack
Step 3: Process 'b' (index 2)
cnt['b'] -= 1
→cnt = {'b': 0, 'd': 0, 'c': 1}
- Update
mi
: 'b' now has count 0, increment →mi = 'c'
- Push 'b' to stack →
stk = ['d', 'b']
- Check if we should pop: Is 'b' ≤ 'c'? Yes! Pop 'b' →
ans = ['b', 'b']
,stk = ['d']
- Continue checking: Is 'd' ≤ 'c'? No! Stop popping
Step 4: Process 'c' (index 3)
cnt['c'] -= 1
→cnt = {'b': 0, 'd': 0, 'c': 0}
- Update
mi
: All counts are 0,mi
becomes 'z' (no more characters) - Push 'c' to stack →
stk = ['d', 'c']
- Check if we should pop: Is 'c' ≤ 'z'? Yes! Pop 'c' →
ans = ['b', 'b', 'c']
,stk = ['d']
- Continue checking: Is 'd' ≤ 'z'? Yes! Pop 'd' →
ans = ['b', 'b', 'c', 'd']
,stk = []
Final Result: "bbcd"
The key decisions were:
- Writing 'b' immediately when we first saw it (since 'b' was the minimum remaining)
- Holding 'd' in the stack when we knew a 'b' was still coming
- Writing the second 'b' before 'd' to maintain lexicographic order
- Finally writing 'c' and 'd' in that order
This gives us the lexicographically smallest possible result "bbcd" instead of alternatives like "bdbc" or "dbbc".
Solution Implementation
1from collections import Counter
2from typing import List
3
4class Solution:
5 def robotWithString(self, s: str) -> str:
6 # Count frequency of each character in the string
7 char_count = Counter(s)
8
9 # Result list to store the final answer
10 result = []
11
12 # Stack to temporarily hold characters
13 stack = []
14
15 # Track the minimum character still remaining in the unprocessed part
16 min_remaining_char = 'a'
17
18 # Process each character in the input string
19 for current_char in s:
20 # Decrease count as we process this character
21 char_count[current_char] -= 1
22
23 # Update minimum remaining character by skipping exhausted characters
24 while min_remaining_char < 'z' and char_count[min_remaining_char] == 0:
25 min_remaining_char = chr(ord(min_remaining_char) + 1)
26
27 # Push current character onto stack
28 stack.append(current_char)
29
30 # Pop from stack while top element is <= minimum remaining character
31 # This ensures we get lexicographically smallest result
32 while stack and stack[-1] <= min_remaining_char:
33 result.append(stack.pop())
34
35 # Join the result list into a string and return
36 return ''.join(result)
37
1class Solution {
2 public String robotWithString(String s) {
3 // Count frequency of each character in the string
4 int[] charFrequency = new int[26];
5 for (char currentChar : s.toCharArray()) {
6 charFrequency[currentChar - 'a']++;
7 }
8
9 // StringBuilder to construct the result string
10 StringBuilder result = new StringBuilder();
11
12 // Stack to temporarily hold characters
13 Deque<Character> stack = new ArrayDeque<>();
14
15 // Track the minimum character still available in remaining string
16 char minRemainingChar = 'a';
17
18 // Process each character in the input string
19 for (char currentChar : s.toCharArray()) {
20 // Decrease frequency as we process this character
21 charFrequency[currentChar - 'a']--;
22
23 // Update minimum remaining character by skipping exhausted characters
24 while (minRemainingChar < 'z' && charFrequency[minRemainingChar - 'a'] == 0) {
25 minRemainingChar++;
26 }
27
28 // Push current character onto stack
29 stack.push(currentChar);
30
31 // Pop from stack to result while top of stack is <= minimum remaining character
32 // This ensures we get the lexicographically smallest result
33 while (!stack.isEmpty() && stack.peek() <= minRemainingChar) {
34 result.append(stack.pop());
35 }
36 }
37
38 return result.toString();
39 }
40}
41
1class Solution {
2public:
3 string robotWithString(string s) {
4 // Count frequency of each character in the string
5 int charFrequency[26] = {0};
6 for (char& c : s) {
7 ++charFrequency[c - 'a'];
8 }
9
10 // Track the minimum character that still exists in remaining string
11 char minRemainingChar = 'a';
12
13 // Stack to temporarily store characters
14 string stack;
15
16 // Result string to build the answer
17 string result;
18
19 // Process each character in the input string
20 for (char& c : s) {
21 // Decrease frequency as we process this character
22 --charFrequency[c - 'a'];
23
24 // Update minimum remaining character by skipping exhausted characters
25 while (minRemainingChar < 'z' && charFrequency[minRemainingChar - 'a'] == 0) {
26 ++minRemainingChar;
27 }
28
29 // Push current character to stack
30 stack += c;
31
32 // Pop from stack while top element is less than or equal to
33 // the minimum character still available in remaining string
34 while (!stack.empty() && stack.back() <= minRemainingChar) {
35 result += stack.back();
36 stack.pop_back();
37 }
38 }
39
40 return result;
41 }
42};
43
1/**
2 * Constructs the lexicographically smallest string by processing characters
3 * through a stack-based approach
4 * @param s - The input string to process
5 * @returns The lexicographically smallest possible result string
6 */
7function robotWithString(s: string): string {
8 // Count frequency of each character in the input string
9 const characterFrequency = new Map<string, number>();
10 for (const character of s) {
11 characterFrequency.set(character, (characterFrequency.get(character) || 0) + 1);
12 }
13
14 // Result array to build the final string
15 const result: string[] = [];
16
17 // Stack to temporarily hold characters
18 const stack: string[] = [];
19
20 // Track the current minimum character still available in remaining string
21 let minimumAvailableChar = 'a';
22
23 // Process each character in the input string
24 for (const currentChar of s) {
25 // Decrement frequency as we process this character
26 characterFrequency.set(currentChar, (characterFrequency.get(currentChar) || 0) - 1);
27
28 // Update minimum available character by skipping exhausted characters
29 while (minimumAvailableChar < 'z' && (characterFrequency.get(minimumAvailableChar) || 0) === 0) {
30 minimumAvailableChar = String.fromCharCode(minimumAvailableChar.charCodeAt(0) + 1);
31 }
32
33 // Push current character to stack
34 stack.push(currentChar);
35
36 // Pop from stack to result while top of stack is <= minimum available character
37 // This ensures we get the lexicographically smallest arrangement
38 while (stack.length > 0 && stack[stack.length - 1] <= minimumAvailableChar) {
39 result.push(stack.pop()!);
40 }
41 }
42
43 // Join result array into final string
44 return result.join('');
45}
46
Time and Space Complexity
Time Complexity: O(n + |Σ|)
where n
is the length of string s
and |Σ|
is the size of the character set (26 for lowercase English letters).
- Creating the Counter takes
O(n)
time to count all characters in the string - The main loop iterates through each character in
s
once, which isO(n)
- Inside the main loop:
- Decrementing the counter:
O(1)
- Finding the next minimum character: The inner while loop advances
mi
from 'a' to 'z' at most once throughout the entire execution, not per iteration. This contributesO(26)
=O(|Σ|)
total - Appending to stack:
O(1)
- The inner while loop for popping from stack: Each element is pushed once and popped at most once, contributing
O(n)
total across all iterations
- Decrementing the counter:
- Joining the answer list:
O(n)
Total: O(n) + O(n) + O(|Σ|) + O(n) = O(n + |Σ|)
Space Complexity: O(n)
- Counter storage:
O(|Σ|)
=O(26)
=O(1)
for storing counts of at most 26 distinct characters - Answer list
ans
: Can contain up ton
characters, soO(n)
- Stack
stk
: Can contain up ton
characters in the worst case, soO(n)
- Variable
mi
:O(1)
Total: O(n) + O(n) + O(1) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Minimum Character Update Logic
The Problem:
A common mistake is updating the minimum remaining character (min_remaining_char
) at the wrong time or incorrectly. Some implementations might:
- Update it before decrementing the count, causing off-by-one errors
- Forget to update it at all after processing a character
- Use incorrect bounds checking (forgetting the
< 'z'
condition)
Incorrect Example:
# WRONG: Updating before decrementing count
for current_char in s:
while min_remaining_char < 'z' and char_count[min_remaining_char] == 0:
min_remaining_char = chr(ord(min_remaining_char) + 1)
char_count[current_char] -= 1 # This should come first!
stack.append(current_char)
# ...
Solution: Always decrement the count first, then update the minimum:
for current_char in s:
char_count[current_char] -= 1 # Decrement first
while min_remaining_char < 'z' and char_count[min_remaining_char] == 0:
min_remaining_char = chr(ord(min_remaining_char) + 1)
# ...
Pitfall 2: Forgetting to Empty the Stack After Processing
The Problem:
After processing all characters from string s
, there might still be characters left in the stack. The algorithm shown correctly handles this by using <=
in the comparison, which naturally empties the stack at the end. However, some implementations might use <
instead or have additional logic that prevents complete stack emptying.
Incorrect Example:
# WRONG: Using strict less than while stack and stack[-1] < min_remaining_char: # Should be <= result.append(stack.pop())
This would leave characters in the stack when min_remaining_char
reaches 'z' or beyond.
Solution:
Use <=
comparison or explicitly empty the stack after the main loop:
# Option 1: Use <= (as in the correct solution) while stack and stack[-1] <= min_remaining_char: result.append(stack.pop()) # Option 2: Explicitly empty stack after main loop for current_char in s: # ... main processing ... # Empty any remaining characters while stack: result.append(stack.pop())
Pitfall 3: Misunderstanding the Greedy Strategy
The Problem: Some might think we should always pop characters immediately if they're small, without considering what's coming next. This leads to incorrect logic like popping whenever we see a character smaller than the current one.
Incorrect Example:
# WRONG: Popping based on current character instead of minimum remaining for current_char in s: stack.append(current_char) while stack and stack[-1] <= current_char: # Wrong comparison! result.append(stack.pop())
Solution:
The key insight is comparing against the minimum character still available in the unprocessed part of s
, not the current character. This ensures we make globally optimal decisions rather than locally greedy ones.
# Correct: Compare against minimum remaining character while stack and stack[-1] <= min_remaining_char: result.append(stack.pop())
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!