Facebook Pixel

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" and t = "ab", without removing any character, the common prefix is "ab" with length 2.
  • If s = "xabc" and t = "abc", by removing the first character 'x' from s, we get "abc" which has a common prefix "abc" with t, 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. When characters match (s[i] == t[j]), we can advance both pointers since this character contributes to our common prefix.

  2. 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.

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 string s
  • Pointer j traverses string t
  • 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:

  1. Initialize variables:

    • Record the lengths of strings s and t as n and m respectively
    • Set two pointers i = 0 and j = 0 to track positions in s and t
    • Initialize a boolean flag rem = False to track whether we've removed a character
  2. Traverse both strings simultaneously:

    while i < n and j < m:

    We continue as long as we haven't reached the end of either string.

  3. 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 in s)
      • Keep j unchanged (we still need to match this character in t)
    • Case 2: Characters match (s[i] == t[j]):
      • Increment both pointers: i += 1 and j += 1
      • Move forward in both strings since we found a matching character
  4. 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 of t we successfully matched

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 Evaluator

Example 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 equals n = 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 reaches n (processed all characters in s)
  • j reaches m (processed all characters in t)
  • A second mismatch is encountered (rem is already True and s[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 match t[0]='a'
  • If we increment both pointers after removal, we'd be comparing s[1]='a' with t[1]='b' instead of t[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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More