Lexicographically Smallest String After Adjacent Removals
HardPrompt
You are given a string s consisting of lowercase English letters.
You can perform the following operation any number of times (including zero):
- Remove any pair of adjacent characters in the string that are consecutive in the alphabet, in either order (e.g.,
'a'and'b', or'b'and'a'). - Shift the remaining characters to the left to fill the gap.
Return the lexicographically smallest string that can be obtained after performing the operations optimally.
Note: Consider the alphabet as circular, thus 'a' and 'z' are consecutive.
Example 1:
Input: s = "abc"
Output: "a"
Explanation:
- Remove
"bc"from the string, leaving"a"as the remaining string. - No further operations are possible. Thus, the lexicographically smallest string after all possible removals is
"a".
Example 2:
Input: s = "bcda"
Output: ""
Explanation:
- Remove
"cd"from the string, leaving"ba"as the remaining string. - Remove
"ba"from the string, leaving""as the remaining string. - No further operations are possible. Thus, the lexicographically smallest string after all possible removals is
"".
Example 3:
Input: s = "zdce"
Output: "zdce"
Explanation:
- Remove
"dc"from the string, leaving"ze"as the remaining string. - No further operations are possible on
"ze". - However, since
"zdce"is lexicographically smaller than"ze", the smallest string after all possible removals is"zdce".
Constraints:
1 <= s.length <= 250sconsists only of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a straightforward brute-force recursion to explore every possible sequence of character removals. It defines a recursive function that takes a string, tries every possible removal, and then calls itself on the resulting strings. A global variable is used to keep track of the lexicographically smallest string encountered across all states (both intermediate and final).
Algorithm
- Define a global or reference variable
minStringand initialize it with the input strings. - Create a recursive function, say
findSmallest(String currentString). - Inside the function, first, update
minStringifcurrentStringis lexicographically smaller. - Iterate through the
currentStringfrom the first character to the second-to-last character. - At each position
i, check if the characterscurrentString.charAt(i)andcurrentString.charAt(i+1)are consecutive. - The consecutive check must handle the circular nature of the alphabet (e.g., 'a' and 'z'). This can be done by checking if the absolute difference of their character codes is 1 or 25.
- If they are consecutive, create a
newStringby removing this pair. - Make a recursive call
findSmallest(newString). - The base case for the recursion is implicit: if no pairs can be removed, the loop finishes and the function returns.
Walkthrough
The core idea is to model the problem as a search through all possible removal paths. We start with the initial string and, at each step, if there are multiple removable pairs, we branch out and explore each possibility recursively.
For example, if we have the string "cba", we can remove "cb" to get "a" or remove "ba" to get "c". The recursive function would explore both paths. It would call itself with "a" and with "c".
This method doesn't keep track of strings it has already processed. If an intermediate string like "da" can be reached via multiple removal sequences, this algorithm will re-process it completely for each path, leading to exponential time complexity.
class Solution { String minString; private boolean areConsecutive(char c1, char c2) { int diff = Math.abs(c1 - c2); return diff == 1 || diff == 25; } private void solve(String current) { if (current.compareTo(minString) < 0) { minString = current; } boolean removed = false; for (int i = 0; i < current.length() - 1; i++) { if (areConsecutive(current.charAt(i), current.charAt(i + 1))) { String nextString = current.substring(0, i) + current.substring(i + 2); solve(nextString); removed = true; } } } public String smallestString(String s) { minString = s; solve(s); return minString; }}Complexity
Time
Exponential, O(k^N), where N is the string length and k is the branching factor. This is a loose upper bound, but the complexity comes from exploring all possible removal paths, which can be numerous.
Space
O(N^2), where N is the length of the string. The maximum depth of the recursion can be N/2, and each call's stack frame stores a string of up to length N.
Trade-offs
Pros
Conceptually simple and a direct translation of the problem statement.
Cons
Extremely inefficient due to massive redundant computations.
The number of recursive calls can grow exponentially with the number of possible removal sequences, not just the number of unique strings.
Likely to cause a StackOverflowError for moderately long strings due to deep recursion.
Will not pass the time limits for the given constraints.
This approach improves upon the brute-force method by treating the problem as a state-space search on a graph. Each unique string is a node (state), and a removal operation is a directed edge. By using a visited set, we ensure that each state is processed only once, avoiding the redundant computations of the naive recursive approach. A Breadth-First Search (BFS) is a natural way to explore this state space.
Algorithm
- Initialize a queue and add the initial string
s. - Initialize a
HashSetcalledvisitedto store strings that have already been processed. Addstovisited. - Initialize a string variable
resulttos, which will hold the lexicographically smallest string found so far. - Start a loop that continues as long as the queue is not empty.
- In each iteration, dequeue a string, let's call it
current. - Iterate through
currentfromi = 0tocurrent.length() - 2. - Check if
current.charAt(i)andcurrent.charAt(i+1)are consecutive (handling the 'a'/'z' wrap-around). - If they are, form a
nextStringby removing this pair. - If
nextStringhas not been visited (i.e., is not in thevisitedset): a. AddnextStringto thevisitedset. b. EnqueuenextStringfor future processing. c. ComparenextStringwithresultand updateresultifnextStringis lexicographically smaller. - After the queue is empty,
resultwill hold the answer.
Walkthrough
The key insight is that different sequences of removals can lead to the same intermediate string. The brute-force approach would wastefully re-explore all possibilities from that string again. This optimized approach uses a visited set (a HashSet for efficient lookups) to keep track of strings we've already queued for processing.
We use a queue to perform a BFS. We start with the initial string. In each step, we take a string from the queue, generate all possible strings that can be formed by one removal, and for any newly generated string that we haven't seen before, we add it to our visited set, our queue, and compare it with our best-so-far result. This guarantees that we explore all reachable strings while processing each unique string only once.
This method is equivalent to a top-down dynamic programming approach with memoization, where the state is the string itself.
import java.util.HashSet;import java.util.LinkedList;import java.util.Queue;import java.util.Set; class Solution { private boolean areConsecutive(char c1, char c2) { int diff = Math.abs(c1 - c2); return diff == 1 || diff == 25; } public String smallestString(String s) { Queue<String> queue = new LinkedList<>(); Set<String> visited = new HashSet<>(); queue.add(s); visited.add(s); String result = s; while (!queue.isEmpty()) { String current = queue.poll(); if (current.compareTo(result) < 0) { result = current; } for (int i = 0; i < current.length() - 1; i++) { if (areConsecutive(current.charAt(i), current.charAt(i + 1))) { String nextString = current.substring(0, i) + current.substring(i + 2); if (!visited.contains(nextString)) { visited.add(nextString); queue.add(nextString); } } } } return result; }}Complexity
Time
O(|V| * N^2), where `|V|` is the number of unique reachable strings and `N` is the maximum string length. For each of the `|V|` states, we iterate through the string (O(N)) and perform string slicing/concatenation (O(N)), leading to O(N^2) work per state.
Space
O(|V| * N), where `|V|` is the number of unique reachable strings and `N` is the maximum string length. This space is used to store the strings in the `queue` and `visited` set.
Trade-offs
Pros
Guaranteed to find the correct lexicographically smallest string.
Significantly more efficient than the brute-force recursion by eliminating redundant computations.
Feasible for the given constraints on typical test cases where the number of reachable states is manageable.
Cons
The number of unique reachable strings (
|V|) can still be large for some inputs, potentially leading to high memory usage and long execution times.String manipulation and hashing can be costly, contributing to the
N^2factor in the time complexity.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.