LeetCode 3223 - Minimum Length of String After Operations
The problem asks us to repeatedly remove characters from a string based on a specific rule. Specifically, for any character s[i] in the string, we can remove the nearest occurrence of the same character to the left of i and the nearest occurrence to the right of i.
Difficulty: 🟡 Medium
Topics: Hash Table, String, Counting
Solution
Problem Understanding
The problem asks us to repeatedly remove characters from a string based on a specific rule. Specifically, for any character s[i] in the string, we can remove the nearest occurrence of the same character to the left of i and the nearest occurrence to the right of i. This operation can be applied any number of times, and the goal is to determine the minimum possible length of the string after performing all valid operations.
The input is a single string s of lowercase English letters, with a length between 1 and 2 * 10^5. The output is a single integer representing the shortest length achievable. Because of the constraints, a naive simulation that checks every possible index for removal would be too slow, especially for the upper bound of 200,000 characters.
Important edge cases include strings where no operation is possible, such as strings of length 1 or strings with no repeating characters in a suitable pattern, and strings where the entire string is the same character, allowing maximum reduction.
Approaches
Brute Force Approach
The brute force approach would involve repeatedly scanning the string to find all indices where the operation can be applied, performing the deletions, and repeating until no more operations are possible. This guarantees correctness but is highly inefficient because each scan and removal operation is O(n), and multiple scans may be needed. For a string of length up to 2 * 10^5, this would lead to O(n^2) or worse complexity, which is impractical.
Optimal Approach
The key observation is that the operation can only remove matching characters from the edges inward. Once the leftmost and rightmost characters differ, no further removal can occur involving those edges. This means we can simulate the process efficiently using a two-pointer approach: start pointers at the beginning (l) and end (r) of the string and move them inward as long as the characters at both ends are equal. We also count consecutive characters at both ends to ensure at least three occurrences are present to allow a removal. Once the pointers cannot move further, the remaining length is the minimum achievable.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2) | O(n) | Repeatedly scan and remove characters |
| Optimal | O(n) | O(1) | Two-pointer method, shrinking edges inward |
Algorithm Walkthrough
-
Initialize two pointers:
lat index 0 andrat indexlen(s) - 1. -
While
l < rands[l] == s[r]: -
Count the consecutive occurrences of
s[l]starting from the left (count_left). -
Count the consecutive occurrences of
s[r]starting from the right (count_right). -
If
count_left + count_right < 3, break the loop because we need at least one character in the middle to remove. -
Move
lright bycount_leftandrleft bycount_right. -
Return
r - l + 1as the minimum length of the remaining string.
Why it works: This algorithm correctly simulates the operation’s effect without actually removing characters. The invariant is that the edges can only be removed if there are matching characters on both sides, so moving the pointers inward mimics the maximal possible reduction.
Python Solution
class Solution:
def minimumLength(self, s: str) -> int:
l, r = 0, len(s) - 1
while l < r and s[l] == s[r]:
char = s[l]
count_left = 0
while l + count_left <= r and s[l + count_left] == char:
count_left += 1
count_right = 0
while r - count_right >= l and s[r - count_right] == char:
count_right += 1
if count_left + count_right < 3:
break
l += count_left
r -= count_right
return max(0, r - l + 1)
Implementation Explanation: We use two pointers and count consecutive characters at both ends. The while loops for count_left and count_right ensure we correctly handle multiple consecutive characters. The max(0, ...) is a safeguard for fully reducible strings.
Go Solution
func minimumLength(s string) int {
l, r := 0, len(s)-1
for l < r && s[l] == s[r] {
char := s[l]
countLeft := 0
for l+countLeft <= r && s[l+countLeft] == char {
countLeft++
}
countRight := 0
for r-countRight >= l && s[r-countRight] == char {
countRight++
}
if countLeft+countRight < 3 {
break
}
l += countLeft
r -= countRight
}
if r-l+1 < 0 {
return 0
}
return r - l + 1
}
Go Implementation Notes: Go does not require dynamic memory allocation here, and we handle indices carefully to prevent slice out-of-bounds errors. We also check for negative length to handle fully reducible strings.
Worked Examples
Example 1: s = "abaacbcbb"
| Step | l | r | s[l] | s[r] | Count Left | Count Right | New l | New r |
|---|---|---|---|---|---|---|---|---|
| Initial | 0 | 8 | a | b | 1 | 2 | 0 | 8 |
| After inner scan | 0 | 8 | a | b | 1 | 1 | 1 | 7 |
| After second operation | 1 | 5 | b | b | 1 | 2 | 2 | 3 |
| Final remaining | 2 | 6 | - | - | - | - | - | - |
Minimum length = 5.
Example 2: s = "aa"
Pointers cannot move because count_left + count_right = 2 < 3. Minimum length = 2.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each character is visited at most once by the two pointers. |
| Space | O(1) | Only a few integer counters are used, no extra storage. |
The linear time complexity arises because the two-pointer approach ensures that each index is considered at most twice.
Test Cases
# Provided examples
assert Solution().minimumLength("abaacbcbb") == 5 # example 1
assert Solution().minimumLength("aa") == 2 # example 2
# Edge cases
assert Solution().minimumLength("a") == 1 # single character
assert Solution().minimumLength("ab") == 2 # no operation possible
assert Solution().minimumLength("aaa") == 1 # fully reducible
assert Solution().minimumLength("aabaaa") == 2 # only edges reducible
assert Solution().minimumLength("abcabc") == 6 # no removable pattern
assert Solution().minimumLength("aaaaa") == 1 # all same character
| Test | Why |
|---|---|
| "abaacbcbb" | Tests multiple operations and mixed characters |
| "aa" | Tests inability to perform any operation |
| "a" | Single character edge case |
| "ab" | Two distinct characters, no operation |
| "aaa" | Fully reducible string |
| "aabaaa" | Only edges removable |
| "abcabc" | Non-reducible string |
| "aaaaa" | All same characters, maximal reduction |
Edge Cases
Single Character: Input "a" cannot have any operation since at least three occurrences are needed. The algorithm correctly returns 1.
Two Different Characters: Input "ab" demonstrates that operations are impossible when the string lacks repeated edge characters. The two-pointer check prevents any movement.
Fully Reducible String: Input "aaaaa" shows that if the entire string consists of a single character, repeated inward removal can reduce it to length 1. The algorithm handles this by moving pointers past all identical characters.
Strings with Only Edge Matches: Input "aabaaa" tests partial reducibility. The algorithm correctly counts consecutive edge characters and stops when the total at the edges is insufficient to remove any more.