LeetCode 3460 - Longest Common Prefix After at Most One Removal
We are given two lowercase strings, s and t. We may remove at most one character from s. The removal is optional, so we are also allowed to leave s unchanged.
Difficulty: 🟡 Medium
Topics: Two Pointers, String
Solution
LeetCode 3460 - Longest Common Prefix After at Most One Removal
Problem Understanding
We are given two lowercase strings, s and t.
We may remove at most one character from s. The removal is optional, so we are also allowed to leave s unchanged.
After performing zero or one removal, we want to compute the length of the longest common prefix between the resulting string and t.
A common prefix is the longest sequence of characters starting from index 0 that is identical in both strings. As soon as a mismatch occurs, the common prefix ends.
The task is to return the maximum possible common prefix length obtainable after removing at most one character from s.
For example, if:
s = "madxa"t = "madam"
then removing 'x' from s produces "mada", whose first four characters match "mada" in t. Therefore the answer is 4.
The constraints are:
1 <= len(s) <= 1000001 <= len(t) <= 100000
The strings can be very large, so any solution with quadratic behavior is too slow. We need a solution that runs in linear time.
Several edge cases are important:
- The strings may already match perfectly, so no removal is needed.
- The optimal removal may occur at the beginning of
s. - The optimal removal may occur in the middle of
s. - The optimal removal may occur near the end of
s. - Removing a character may still not help at all.
- One or both strings may have length
1.
The key challenge is determining whether skipping exactly one character in s can extend the matching prefix.
Approaches
Brute Force
A straightforward solution is to try every possible removal position.
For each index i in s:
- Remove
s[i]. - Construct the resulting string.
- Compute the common prefix length with
t.
We must also consider the case where no character is removed.
This approach is correct because it explicitly checks every valid operation and returns the best result.
However, if n = len(s), there are n + 1 possibilities. Computing the common prefix for each possibility can take O(n) time. Therefore the total complexity becomes O(n²), which is far too slow when n can reach 100000.
Optimal Observation
Since only one character may be removed, the matching process can encounter at most one mismatch that we choose to skip.
Suppose we scan both strings from left to right.
If characters match, we advance both pointers.
If we encounter the first mismatch, we have only one possible action:
- Remove the current character from
s. - Continue matching the remainder.
Any mismatch occurring after that immediately ends the common prefix because no additional removals are available.
Therefore we only need to find:
- The first position where
sandtdiffer. - How many additional characters match after skipping that character in
s.
This can be done with a simple two pointer scan in linear time.
Comparison of Approaches
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Try every possible removal and recompute prefix |
| Optimal | O(n) | O(1) | Find first mismatch, then simulate the single allowed skip |
Algorithm Walkthrough
- Let
n = len(s)andm = len(t). - Start with an index
i = 0. - Move
iforward while:
i < ni < ms[i] == t[i]
This finds the length of the common prefix before any mismatch occurs.
4. If i == min(n, m), then every character up to the shorter string matched.
Since removing a character cannot increase the common prefix beyond the available characters in t, the answer is simply i.
5. Otherwise, a mismatch occurs at position i.
6. Treat s[i] as the character we remove. Set:
j = i + 1insk = iint
- Continue matching while:
j < nk < ms[j] == t[k]
Advance both pointers for every successful match.
8. The common prefix length equals the final value of k, because k represents how many characters of t have been matched from the beginning.
9. Return k.
Why it works
Before the first mismatch, every valid common prefix must include all matching characters, so those characters are forced.
When the first mismatch occurs, the only possible beneficial operation is to remove the mismatching character from s. Removing any earlier character would destroy already matched prefix characters, and removing a later character would not fix the current mismatch. Therefore the optimal solution is uniquely determined at the first mismatch. After using the single removal, matching continues until another mismatch or until one string ends. This guarantees that the computed prefix length is the maximum achievable value.
Python Solution
class Solution:
def longestCommonPrefix(self, s: str, t: str) -> int:
n, m = len(s), len(t)
i = 0
while i < n and i < m and s[i] == t[i]:
i += 1
if i == min(n, m):
return i
j = i + 1
k = i
while j < n and k < m and s[j] == t[k]:
j += 1
k += 1
return k
The implementation first finds the longest matching prefix before any mismatch occurs. The variable i represents the first position where the strings differ.
If no mismatch exists within the overlapping portion of the strings, the answer is immediately known because removing a character cannot create a longer prefix than the entire matched region.
When a mismatch appears, the algorithm consumes the one allowed removal by skipping s[i]. The pointers j and k then continue comparing the remainder of the strings. The final value of k corresponds exactly to the number of prefix characters that match after the optimal removal.
The algorithm performs only sequential scans and never revisits characters.
Go Solution
func longestCommonPrefix(s string, t string) int {
n, m := len(s), len(t)
i := 0
for i < n && i < m && s[i] == t[i] {
i++
}
if i == min(n, m) {
return i
}
j := i + 1
k := i
for j < n && k < m && s[j] == t[k] {
j++
k++
}
return k
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
The Go implementation follows exactly the same logic as the Python version. Since the input contains only lowercase English letters, indexing strings by byte is safe. No special handling for Unicode is required. All operations use integer indices, and there are no overflow concerns because string lengths are at most 100000.
Worked Examples
Example 1
Input
s = "madxa"
t = "madam"
Phase 1: Find First Mismatch
| i | s[i] | t[i] | Match? |
|---|---|---|---|
| 0 | m | m | Yes |
| 1 | a | a | Yes |
| 2 | d | d | Yes |
| 3 | x | a | No |
Now:
i = 3
Remove s[3].
Phase 2: Continue Matching
Initial state:
j = 4
k = 3
| j | k | s[j] | t[k] | Match? |
|---|---|---|---|---|
| 4 | 3 | a | a | Yes |
After matching:
j = 5
k = 4
Loop ends.
Answer:
4
Example 2
Input
s = "leetcode"
t = "eetcode"
Phase 1
| i | s[i] | t[i] | Match? |
|---|---|---|---|
| 0 | l | e | No |
First mismatch:
i = 0
Remove 'l'.
Phase 2
| j | k | s[j] | t[k] | Match? |
|---|---|---|---|---|
| 1 | 0 | e | e | Yes |
| 2 | 1 | e | e | Yes |
| 3 | 2 | t | t | Yes |
| 4 | 3 | c | c | Yes |
| 5 | 4 | o | o | Yes |
| 6 | 5 | d | d | Yes |
| 7 | 6 | e | e | Yes |
Final:
k = 7
Answer:
7
Example 3
Input
s = "one"
t = "one"
Phase 1
| i | s[i] | t[i] | Match? |
|---|---|---|---|
| 0 | o | o | Yes |
| 1 | n | n | Yes |
| 2 | e | e | Yes |
Now:
i = 3
Since:
i == min(len(s), len(t))
Return:
3
Example 4
Input
s = "a"
t = "b"
Phase 1
| i | s[i] | t[i] | Match? |
|---|---|---|---|
| 0 | a | b | No |
Remove 'a'.
Phase 2
j = 1
k = 0
The loop cannot run because j == len(s).
Answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m) | Each pointer moves forward at most once |
| Space | O(1) | Only a few integer variables are used |
The first scan advances through the common prefix. The second scan processes the remaining suffix after the removal. Across both scans, every character is examined at most once, giving linear time complexity. No auxiliary data structures are required, so the space usage is constant.
Test Cases
sol = Solution()
assert sol.longestCommonPrefix("madxa", "madam") == 4 # remove in middle
assert sol.longestCommonPrefix("leetcode", "eetcode") == 7 # remove first char
assert sol.longestCommonPrefix("one", "one") == 3 # no removal needed
assert sol.longestCommonPrefix("a", "b") == 0 # no common prefix
assert sol.longestCommonPrefix("abc", "abc") == 3 # identical strings
assert sol.longestCommonPrefix("xabc", "abc") == 3 # remove first character
assert sol.longestCommonPrefix("abxc", "abc") == 3 # remove middle character
assert sol.longestCommonPrefix("abcdx", "abcd") == 4 # mismatch after full prefix
assert sol.longestCommonPrefix("abc", "abd") == 2 # removal does not help
assert sol.longestCommonPrefix("aaab", "aaaa") == 3 # mismatch near end
assert sol.longestCommonPrefix("z", "z") == 1 # single matching char
assert sol.longestCommonPrefix("ba", "a") == 1 # remove leading char
assert sol.longestCommonPrefix("abcdef", "abqdef") == 2 # second mismatch stops match
assert sol.longestCommonPrefix("cabc", "abc") == 3 # entire match after removal
assert sol.longestCommonPrefix("abc", "xyz") == 0 # completely different
Test Summary
| Test | Why |
|---|---|
"madxa", "madam" |
Removal in the middle |
"leetcode", "eetcode" |
Removal at index 0 |
"one", "one" |
No removal needed |
"a", "b" |
Smallest mismatch case |
"abc", "abc" |
Fully identical strings |
"xabc", "abc" |
Remove first character |
"abxc", "abc" |
Remove internal character |
"abcdx", "abcd" |
Extra trailing character |
"abc", "abd" |
Removal provides no benefit |
"aaab", "aaaa" |
Mismatch near the end |
"z", "z" |
Single character match |
"ba", "a" |
Length reduction creates match |
"abcdef", "abqdef" |
Second mismatch terminates prefix |
"cabc", "abc" |
Complete alignment after removal |
"abc", "xyz" |
No common prefix at all |
Edge Cases
Removal at the First Character
A common source of bugs is assuming that matching must begin before a removal occurs. Consider s = "leetcode" and t = "eetcode". The optimal move is removing the very first character. The implementation handles this naturally because the first mismatch occurs at index 0, and the second phase begins with j = 1 and k = 0.
Strings Already Match Completely
When the strings are identical, there is no mismatch to trigger the removal logic. An incorrect implementation might unnecessarily remove a character and reduce the answer. The algorithm explicitly checks whether the entire overlapping region matched and immediately returns that length.
Removal Does Not Fix the Mismatch
Consider s = "abc" and t = "abd". The first mismatch occurs at index 2. Removing 'c' still does not create additional matching characters because s ends immediately afterward. The implementation correctly enters the second phase, performs no additional matches, and returns the existing prefix length of 2.
Single Character Strings
For inputs such as s = "a" and t = "b", the first mismatch occurs immediately and skipping the only character leaves an empty string. The loop conditions safely prevent out of bounds access, and the algorithm correctly returns 0.
Multiple Mismatches
Consider s = "abcdef" and t = "abqxyz". Only one removal is allowed. After skipping the first problematic character in s, another mismatch appears immediately. The algorithm stops at that point, correctly reflecting the fact that no second removal is available.