LeetCode 1544 - Make The String Great
The problem asks us to process a string s consisting of lowercase and uppercase English letters, removing pairs of adjac
Difficulty: 🟢 Easy
Topics: String, Stack
Solution
Problem Understanding
The problem asks us to process a string s consisting of lowercase and uppercase English letters, removing pairs of adjacent letters that are the same character but differ in case. Specifically, any pair where one character is lowercase and the adjacent character is its uppercase counterpart (or vice versa) is considered "bad." We must remove such pairs iteratively until no more bad pairs remain, producing a "good" string.
The input s is guaranteed to have a length between 1 and 100, which is small enough to allow simple iterative solutions without performance concerns. The output should be the final good string, and the problem guarantees uniqueness. An empty string is considered valid if all characters are removed.
Key points include recognizing that removal of a pair may create new bad pairs that need to be resolved in subsequent iterations. Edge cases include strings that are already good, strings with alternating upper/lower letters that cancel completely, and single-character strings.
Approaches
A brute-force approach would repeatedly scan the string for bad adjacent pairs, remove them, and repeat until no bad pairs remain. While correct, this approach is inefficient because repeated scanning and string slicing lead to O(n²) time complexity in the worst case.
The optimal solution leverages a stack to process the string in a single pass. The idea is to iterate through each character and compare it with the top of the stack. If the character forms a bad pair with the stack's top, pop the stack. Otherwise, push the character onto the stack. This method ensures that we always resolve adjacent conflicts immediately and process the string in linear time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Repeatedly scan and remove bad pairs until string is good |
| Optimal | O(n) | O(n) | Single-pass stack solution, resolving conflicts immediately |
Algorithm Walkthrough
- Initialize an empty stack to hold characters of the currently "good" string as we iterate.
- Iterate through each character
cin the input strings. - If the stack is not empty and the top character of the stack forms a bad pair with
c(i.e.,abs(ord(stack[-1]) - ord(c)) == 32), pop the stack to remove the conflicting pair. - Otherwise, push
conto the stack. - After processing all characters, convert the stack into a string by joining its elements.
- Return the resulting string as the final good string.
Why it works: The stack maintains the invariant that it contains a good string at all times. Any time a bad pair is found, it is immediately removed, which guarantees that at the end, no bad pairs remain. The process is complete because each character is either added or removed exactly once.
Python Solution
class Solution:
def makeGood(self, s: str) -> str:
stack = []
for c in s:
if stack and abs(ord(stack[-1]) - ord(c)) == 32:
stack.pop()
else:
stack.append(c)
return ''.join(stack)
The Python solution uses a list as a stack. For each character, it checks whether it forms a bad pair with the stack's top. The abs(ord(stack[-1]) - ord(c)) == 32 condition works because lowercase and uppercase ASCII letters differ by 32. Characters not forming a bad pair are appended to the stack, and finally, the stack contents are joined into the output string.
Go Solution
func makeGood(s string) string {
stack := []rune{}
for _, c := range s {
if len(stack) > 0 && abs(int(stack[len(stack)-1]-c)) == 32 {
stack = stack[:len(stack)-1]
} else {
stack = append(stack, c)
}
}
return string(stack)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
In Go, we use a slice of runes as the stack. Each character is compared to the stack's top using the same ASCII difference check. The slice is popped by reslicing, and characters are appended normally. The abs helper function is needed because Go does not have a built-in absolute value for integers.
Worked Examples
Example 1: s = "leEeetcode"
| Step | Character | Stack | Action |
|---|---|---|---|
| 1 | l | [l] | Push |
| 2 | e | [l,e] | Push |
| 3 | E | [l] | Pop (bad pair e-E) |
| 4 | e | [l,e] | Push |
| 5 | e | [l,e,e] | Push |
| 6 | t | [l,e,e,t] | Push |
| 7 | c | [l,e,e,t,c] | Push |
| 8 | o | [l,e,e,t,c,o] | Push |
| 9 | d | [l,e,e,t,c,o,d] | Push |
| 10 | e | [l,e,e,t,c,o,d,e] | Push |
Resulting string: "leetcode"
Example 2: s = "abBAcC"
| Step | Character | Stack | Action |
|---|---|---|---|
| 1 | a | [a] | Push |
| 2 | b | [a,b] | Push |
| 3 | B | [a] | Pop (b-B) |
| 4 | A | [] | Pop (a-A) |
| 5 | c | [c] | Push |
| 6 | C | [] | Pop (c-C) |
Resulting string: ""
Example 3: s = "s"
| Step | Character | Stack | Action |
|---|---|---|---|
| 1 | s | [s] | Push |
Resulting string: "s"
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each character is pushed and popped at most once, so linear in string length |
| Space | O(n) | Stack may store all characters in the worst case if no bad pairs exist |
The algorithm is efficient due to the single-pass stack processing, avoiding repeated scanning.
Test Cases
# Provided examples
assert Solution().makeGood("leEeetcode") == "leetcode" # mixed case removal
assert Solution().makeGood("abBAcC") == "" # all removed
assert Solution().makeGood("s") == "s" # single character
# Additional edge cases
assert Solution().makeGood("aA") == "" # two characters removed
assert Solution().makeGood("aa") == "aa" # same case, not removed
assert Solution().makeGood("Aa") == "" # two characters removed
assert Solution().makeGood("abAB") == "abAB" # no adjacent bad pair
assert Solution().makeGood("") == "" # empty string
| Test | Why |
|---|---|
| "leEeetcode" | Basic adjacent upper/lower removal |
| "abBAcC" | Multiple chained removals leading to empty string |
| "s" | Single character, already good |
| "aA" | Minimal removal case |
| "aa" | Same case, should not remove |
| "Aa" | Case reversal minimal removal |
| "abAB" | No adjacent bad pair, string remains unchanged |
| "" | Edge case, empty input |
Edge Cases
One important edge case is a string of length one. Since a single character cannot form a bad pair, it should be returned as is. Another edge case is when the string alternates perfectly between upper and lower letters, like "aAaA", which requires multiple sequential removals. The implementation correctly handles this with the stack approach because each conflict triggers a pop. A third edge case is a string with no bad pairs at all; in this case, the algorithm must not remove anything, and the stack simply accumulates all characters. This is naturally handled because the removal condition only triggers on bad pairs.