Facebook Pixel

2434. Using a Robot to Print the Lexicographically Smallest String

MediumStackGreedyHash TableString
Leetcode Link

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:

  1. Operation 1: Take the first character from string s and append it to the end of string t
  2. Operation 2: Take the last character from string t and write it on paper (removing it from t)

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 (from s) 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 to t (now t = "b", s = "ac")
  • Take 'a' from s to t (now t = "ba", s = "c")
  • Write 'a' from t to paper (now t = "b", paper has "a")
  • Write 'b' from t to paper (now t = "", paper has "ab")
  • Take 'c' from s to t (now t = "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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 from s 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:

  1. Keep track of the count of each character remaining in s
  2. Maintain the current minimum character that hasn't been exhausted in s
  3. For each character we take from s, push it to the stack t
  4. 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
  5. 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.

Learn more about Stack and Greedy patterns.

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 string s
  • stk: A list acting as a stack to represent the robot's string t
  • ans: A list to collect characters written to paper
  • mi: A variable tracking the smallest character that still exists in the unprocessed part of s

Algorithm Steps:

  1. Initialize the frequency counter: Create cnt = Counter(s) to know how many of each character we have in total.

  2. Set initial minimum: Start with mi = 'a' as the smallest possible character.

  3. 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 character
      while 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 answer
      while stk and stk[-1] <= mi:
          ans.append(stk.pop())
  4. 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 from s
  • 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 Evaluator

Example 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'] -= 1cnt = {'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'] -= 1cnt = {'b': 1, 'd': 0, 'c': 1}
  • Update mi: 'b' still has count 1, so mi = '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'] -= 1cnt = {'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'] -= 1cnt = {'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 is O(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 contributes O(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
  • 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 to n characters, so O(n)
  • Stack stk: Can contain up to n characters in the worst case, so O(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())
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More