3460. Longest Common Prefix After at Most One Removal π
Problem Description
You are given two strings s
and t
.
Your task is to find the length of the longest common prefix between s
and t
after removing at most one character from string s
.
A prefix is a substring that starts from the beginning of a string. A common prefix between two strings is a prefix that both strings share.
The key constraint is that you can remove at most one character from string s
(meaning you can remove 0 or 1 character). The removal, if done, can be from any position in s
. After the optional removal, you need to find how long of a prefix from the modified s
matches with the prefix of t
.
For example:
- If
s = "abc"
andt = "ab"
, without removing any character, the common prefix is"ab"
with length 2. - If
s = "xabc"
andt = "abc"
, by removing the first character'x'
froms
, we get"abc"
which has a common prefix"abc"
witht
, giving us length 3.
The goal is to maximize the length of this common prefix by strategically choosing whether to remove a character from s
and which character to remove if you do.
Intuition
When we think about finding the longest common prefix with at most one removal from s
, we need to match characters from the beginning of both strings sequentially.
The key insight is that we're trying to match prefixes, which means we must start from index 0 of both strings and move forward. As we traverse:
-
When characters match (
s[i] == t[j]
), we can advance both pointers since this character contributes to our common prefix. -
When characters don't match (
s[i] != t[j]
), we have a choice to make:- If we haven't removed any character yet, we can remove
s[i]
and continue. This is our one allowed removal. - If we've already used our removal, we must stop since we can't match any further.
- If we haven't removed any character yet, we can remove
The crucial observation is that when we encounter a mismatch, the only meaningful action is to skip the current character in s
(effectively removing it). We don't skip characters in t
because we're looking for a prefix of t
, and prefixes must start from the beginning without gaps.
This naturally leads to a two-pointer approach where:
- Pointer
i
traverses strings
- Pointer
j
traverses stringt
- A boolean flag
rem
tracks whether we've used our one removal
The length of the common prefix will be equal to how far we've advanced in t
(the value of j
), since that represents how many characters from t
's prefix we've successfully matched.
Learn more about Two Pointers patterns.
Solution Approach
We implement the solution using a two-pointer technique with the following steps:
-
Initialize variables:
- Record the lengths of strings
s
andt
asn
andm
respectively - Set two pointers
i = 0
andj = 0
to track positions ins
andt
- Initialize a boolean flag
rem = False
to track whether we've removed a character
- Record the lengths of strings
-
Traverse both strings simultaneously:
while i < n and j < m:
We continue as long as we haven't reached the end of either string.
-
Handle character comparison:
- Case 1: Characters don't match (
s[i] != t[j]
):- Check if we've already removed a character (
if rem
) - If yes, break the loop since we can't remove another character
- If no, set
rem = True
to mark that we're using our removal - Only increment
i
(skip the character ins
) - Keep
j
unchanged (we still need to match this character int
)
- Check if we've already removed a character (
- Case 2: Characters match (
s[i] == t[j]
):- Increment both pointers:
i += 1
andj += 1
- Move forward in both strings since we found a matching character
- Increment both pointers:
- Case 1: Characters don't match (
-
Return the result:
- Return
j
, which represents the length of the longest common prefix - The value of
j
indicates how many characters from the beginning oft
we successfully matched
- Return
The algorithm efficiently handles the removal decision in a single pass through the strings with O(n)
time complexity where n
is the length of string s
, and O(1)
space complexity since we only use a few variables.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example with s = "bxcd"
and t = "bcd"
.
Initial State:
s = "bxcd"
,n = 4
t = "bcd"
,m = 3
i = 0
,j = 0
,rem = False
Step 1: Compare s[0]
with t[0]
s[0] = 'b'
,t[0] = 'b'
- Characters match! Increment both pointers
i = 1
,j = 1
,rem = False
Step 2: Compare s[1]
with t[1]
s[1] = 'x'
,t[1] = 'c'
- Characters don't match and
rem = False
- We can use our removal: set
rem = True
- Only increment
i
(skip 'x' in s) i = 2
,j = 1
,rem = True
Step 3: Compare s[2]
with t[1]
s[2] = 'c'
,t[1] = 'c'
- Characters match! Increment both pointers
i = 3
,j = 2
,rem = True
Step 4: Compare s[3]
with t[2]
s[3] = 'd'
,t[2] = 'd'
- Characters match! Increment both pointers
i = 4
,j = 3
,rem = True
Step 5: Loop terminates
i = 4
equalsn = 4
, so we exit the loop- Return
j = 3
Result: The longest common prefix has length 3. By removing 'x' from position 1 in s
, we transformed "bxcd" to "bcd", which shares the complete prefix "bcd" with t
.
Solution Implementation
1class Solution:
2 def longestCommonPrefix(self, s: str, t: str) -> int:
3 # Get lengths of both strings
4 len_s, len_t = len(s), len(t)
5
6 # Initialize pointers for both strings
7 pointer_s = 0 # Pointer for string s
8 pointer_t = 0 # Pointer for string t
9
10 # Flag to track if we've already used our one allowed removal
11 removal_used = False
12
13 # Iterate through both strings
14 while pointer_s < len_s and pointer_t < len_t:
15 # Check if characters at current positions match
16 if s[pointer_s] != t[pointer_t]:
17 # If characters don't match
18 if removal_used:
19 # We've already used our removal, can't continue
20 break
21 # Use our one allowed removal (skip character in s)
22 removal_used = True
23 else:
24 # Characters match, advance pointer in t
25 pointer_t += 1
26
27 # Always advance pointer in s
28 pointer_s += 1
29
30 # Return the length of matched prefix in t
31 return pointer_t
32
1class Solution {
2 public int longestCommonPrefix(String s, String t) {
3 int sLength = s.length();
4 int tLength = t.length();
5
6 int sIndex = 0; // Current position in string s
7 int tIndex = 0; // Current position in string t
8 boolean hasRemovedChar = false; // Flag to track if we've already removed one character from s
9
10 // Iterate through both strings simultaneously
11 while (sIndex < sLength && tIndex < tLength) {
12 // Check if current characters match
13 if (s.charAt(sIndex) != t.charAt(tIndex)) {
14 // If characters don't match
15 if (hasRemovedChar) {
16 // We've already used our one removal, so stop here
17 break;
18 }
19 // Mark that we're removing this character from s (skip it)
20 hasRemovedChar = true;
21 // Don't increment tIndex, only sIndex (effectively removing s[sIndex])
22 } else {
23 // Characters match, move forward in string t
24 tIndex++;
25 }
26 // Always move forward in string s
27 sIndex++;
28 }
29
30 // Return the length of the common prefix found in string t
31 return tIndex;
32 }
33}
34
1class Solution {
2public:
3 int longestCommonPrefix(string s, string t) {
4 // Get lengths of both strings
5 int lenS = s.length();
6 int lenT = t.length();
7
8 // Initialize pointers for both strings
9 int indexS = 0; // Pointer for string s
10 int indexT = 0; // Pointer for string t
11
12 // Flag to track if we've already used our one allowed removal
13 bool hasRemovedChar = false;
14
15 // Process both strings while we haven't reached the end of either
16 while (indexS < lenS && indexT < lenT) {
17 // Check if current characters match
18 if (s[indexS] != t[indexT]) {
19 // Characters don't match
20 if (hasRemovedChar) {
21 // Already used our one removal, can't proceed
22 break;
23 }
24 // Use our one allowed removal (skip character in s)
25 hasRemovedChar = true;
26 } else {
27 // Characters match, advance pointer in t
28 ++indexT;
29 }
30 // Always advance pointer in s
31 ++indexS;
32 }
33
34 // Return the length of matched prefix in string t
35 return indexT;
36 }
37};
38
1/**
2 * Finds the length of the longest common prefix between two strings,
3 * allowing at most one character to be skipped from the first string.
4 *
5 * @param s - The first string (from which one character can be skipped)
6 * @param t - The second string (target string to match)
7 * @returns The length of the longest matching prefix in the second string
8 */
9function longestCommonPrefix(s: string, t: string): number {
10 const sLength: number = s.length;
11 const tLength: number = t.length;
12
13 let sIndex: number = 0; // Current position in string s
14 let tIndex: number = 0; // Current position in string t
15 let hasSkippedChar: boolean = false; // Flag to track if we've already skipped a character
16
17 // Iterate through both strings simultaneously
18 while (sIndex < sLength && tIndex < tLength) {
19 if (s[sIndex] !== t[tIndex]) {
20 // Characters don't match
21 if (hasSkippedChar) {
22 // Already skipped once, can't skip again
23 break;
24 }
25 // Skip this character from s (first removal)
26 hasSkippedChar = true;
27 } else {
28 // Characters match, advance in both strings
29 tIndex++;
30 }
31 // Always advance in string s
32 sIndex++;
33 }
34
35 // Return the length of matched prefix in string t
36 return tIndex;
37}
38
Time and Space Complexity
Time Complexity: O(min(n, m))
where n
and m
are the lengths of strings s
and t
respectively.
The while loop iterates through the strings with index i
for string s
and index j
for string t
. The loop continues while i < n
and j < m
. In each iteration, i
is always incremented by 1, and j
is incremented by 1 only when s[i] == t[j]
. The loop terminates when either:
i
reachesn
(processed all characters ins
)j
reachesm
(processed all characters int
)- A second mismatch is encountered (
rem
is alreadyTrue
ands[i] != t[j]
)
Since i
is incremented in every iteration and the loop stops when i >= n
or other conditions are met, the maximum number of iterations is min(n, m)
.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space for variables n
, m
, i
, j
, and rem
, regardless of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Misunderstanding When to Increment Pointers
The Problem:
A common mistake is incorrectly handling pointer increments when characters don't match. Developers often mistakenly increment both pointers or only increment pointer_t
when removing a character from s
.
Incorrect Implementation:
if s[pointer_s] != t[pointer_t]: if removal_used: break removal_used = True pointer_s += 1 pointer_t += 1 # WRONG! This skips a character in t
Why This Fails:
When we remove a character from s
, we're essentially skipping it to see if the next character in s
matches the current character in t
. We should NOT advance pointer_t
because we still need to try matching the same character in t
with the next character in s
.
Example Where This Fails:
s = "xabc"
,t = "abc"
- At position 0:
s[0]='x'
doesn't matcht[0]='a'
- If we increment both pointers after removal, we'd be comparing
s[1]='a'
witht[1]='b'
instead oft[0]='a'
- This would incorrectly return 0 instead of 3
Correct Solution:
The original code correctly handles this by only incrementing pointer_t
when characters match:
if s[pointer_s] != t[pointer_t]: if removal_used: break removal_used = True # Don't increment pointer_t here else: pointer_t += 1 # Only increment when characters match pointer_s += 1 # Always increment pointer_s
Alternative Pitfall: Not Considering the "No Removal" Case
The Problem: Some developers might assume they must always remove a character, leading to suboptimal solutions when no removal gives the best result.
Why This Matters: The problem states "at most one character", meaning 0 or 1 removal is allowed. Sometimes the best solution is to not remove any character at all.
Example:
s = "abc"
,t = "abcd"
- Without removal: common prefix length = 3
- With any removal: common prefix length would be less than 3
The provided solution correctly handles this by using the removal_used
flag as optional - it's only set to True
when needed, not preemptively.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!