Minimum Length of String After Deleting Similar Ends: Two-Pointer Trim
LeetCode 1750, Minimum Length of String After Deleting Similar Ends, asks you to repeatedly trim matching character groups from both ends of a string. You pick a non-empty prefix and a non-empty suffix where every character in both is the same letter, then delete them. The goal is to minimize the length of the remaining string after performing as many of these operations as you want.
This is a clean two-pointer problem. The trick is recognizing that you always want to trim greedily from the outside in.
Why this problem matters
Two-pointer trimming from both ends is a pattern that shows up in many string and array problems. Valid Palindrome, for example, uses the same converging pointer structure. The difference here is that instead of comparing single characters, you are comparing character groups and consuming entire runs of matching characters at once.
What makes this problem interesting is the greedy insight. You never need to "save" a trimming operation for later. If both ends match, you should always trim them now. Skipping a valid trim cannot help you find a shorter result, because trimming only removes characters and never changes the characters that remain in the middle.
This problem also reinforces the idea that two pointers can do more than one step at a time. After finding that both ends share the same character, you advance each pointer past the entire run of that character. That inner skip loop is the same technique you see in problems like Remove Duplicates from Sorted Array and 3Sum.
The key insight
Place a left pointer at the start and a right pointer at the end. While left is still to the left of right and s[left] == s[right], you have a match. Record the matching character, then advance left forward past every occurrence of that character and advance right backward past every occurrence of that character. Once the characters at the two pointers differ, or the pointers cross, the remaining substring between left and right (inclusive) is the shortest you can achieve.
The reason this greedy approach works: each trim operation removes characters that are identical on both sides. Trimming now can never make a future trim impossible, because the characters in the middle are untouched. And not trimming when you could gains you nothing, since those matching outer characters will always be part of the string otherwise.
The solution
def minimumLength(s):
left, right = 0, len(s) - 1
while left < right and s[left] == s[right]:
ch = s[left]
while left <= right and s[left] == ch:
left += 1
while left <= right and s[right] == ch:
right -= 1
return right - left + 1
The outer loop checks whether the characters at both ends match. If they do, we store that character in ch and then skip past all occurrences of ch from both sides. The inner while loops use left <= right (not left < right) because it is possible for the pointers to meet and both still be pointing at the matching character. In that case, both pointers pass each other, and the result is zero: the entire string gets deleted.
After the loop, right - left + 1 gives the length of the undeleted portion. If left > right, this evaluates to a negative number or zero, but since left will be exactly right + 1 in the crossed case, the formula gives 0 correctly.
The inner skip loops use left <= right instead of left < right. This matters when the entire string consists of one repeated character, like "aaaa". With left < right, you would stop one character short and return 1 instead of 0.
Visual walkthrough
Step 1: left=0, right=8. s[0]='a' and s[8]='a' match.
Both ends are 'a'. We will delete all leading and trailing 'a's.
Step 2: Skip all 'a's from both sides. Deleted indices 0, 1, and 8.
Left skips two 'a's (indices 0, 1) and lands on 'b' at index 2. Right skips one 'a' (index 8) and lands on 'b' at index 7.
Step 3: left=2, right=7. s[2]='b' and s[7]='b' match.
Both pointers now point to 'b'. We will delete all leading and trailing 'b's from the remaining window.
Step 4: Skip all 'b's from both sides. Deleted indices 2, 7, and 6.
Left skips one 'b' (index 2) and lands on 'c' at index 3. Right skips two 'b's (indices 7, 6) and lands on 'a' at index 5.
Step 5: left=3, right=5. s[3]='c' != s[5]='a'. No match. Done!
Characters differ. The remaining string is "cca" (indices 3 to 5). Minimum length = 3.
The algorithm trims two rounds of matching groups. First, it removes the 'a' characters from both ends. Then it removes the 'b' characters from both ends. The remaining "cca" has 'c' on the left and 'a' on the right, so no further trimming is possible. The answer is 3.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| Two Pointers | O(n) | O(1) |
Time: O(n). Each character is visited at most once by each pointer. The left pointer only moves right, the right pointer only moves left, and together they cover at most n positions. Even with the nested inner loops, the total work is linear.
Space: O(1). Two integer pointers and one character variable. No auxiliary data structures.
The building blocks
1. Two-pointer convergence
Two pointers start at opposite ends and move inward based on a condition. This is the same skeleton used in Valid Palindrome, Container With Most Water, and the inner loop of 3Sum.
left, right = 0, len(s) - 1
while left < right and s[left] == s[right]:
# process the match
left += 1
right -= 1
The convergence guarantees that the search space shrinks by at least one element per outer iteration. You never backtrack.
2. Character group skipping
Once you identify the matching character, you skip past all consecutive occurrences of it from both sides. This is a common sub-pattern in problems that deal with runs of identical values.
ch = s[left]
while left <= right and s[left] == ch:
left += 1
while left <= right and s[right] == ch:
right -= 1
This inner skip is what makes the algorithm trim entire groups rather than single characters. Without it, you would only remove one character from each side per iteration, which would still produce the correct result but would obscure the real structure of the problem.
Edge cases
- Single character.
"a"hasleft == rightfrom the start. The outer loop conditionleft < rightis false, so we return 1. You cannot delete a single character because you need both a non-empty prefix and a non-empty suffix. - All same characters.
"aaaa"matches on the first check. Both inner loops consume all four characters, pointers cross, and the result is 0. The entire string is deleted. - No matching ends.
"abcd"hass[0]='a' != s[3]='d'. The outer loop never executes, and we return the full length, 4. - Entire string deleted except middle.
"aabaa"trims the 'a' groups from both sides. Left skips indices 0, 1. Right skips index 4. The remaining string is"b"at index 2, so the answer is 1. - Two distinct groups.
"abba"trims 'a' from both ends, leaving"bb". Then 'b' matches on both sides, so both are trimmed. The answer is 0.
From understanding to recall
You have read through the logic and seen the walkthrough. The algorithm is short, just a while loop with two inner skip loops. But in a few weeks, the details fade: do the inner loops use left < right or left <= right? Do you store the matching character before or after advancing the pointer? These small choices determine correctness.
The two building blocks here, convergence and group skipping, are each small enough to drill independently. Once they are in your long-term memory, assembling the full solution becomes mechanical. You are not memorizing LeetCode 1750 specifically. You are internalizing two reusable techniques that appear across dozens of problems.
Related posts
- Valid Palindrome - Two pointers converging from both ends of a string
- Container With Most Water - Two-pointer convergence pattern with greedy decisions
- 3Sum Closest - Two pointers within a sorted array for target matching
Practice builds fluency. Reading builds familiarity. CodeBricks bridges the gap with spaced repetition that targets the building blocks, not just the final solution.