Facebook Pixel

3135. Equalize Strings by Adding or Removing Characters at Ends 🔒

Problem Description

You are given two strings initial and target. Your goal is to transform initial into target by performing a series of operations.

In each operation, you can:

  • Add one character at the beginning or end of the string
  • Remove one character from the beginning or end of the string

You need to find the minimum number of operations required to transform initial into target.

For example, if initial = "abc" and target = "aec", you could:

  1. Remove 'b' from the end (or middle operations to get to "ac")
  2. Add 'e' to get "aec"

The key insight is that any characters that appear in the same order in both strings can be kept unchanged. The transformation requires removing characters from initial that don't match this common subsequence and adding characters from target that aren't part of it.

The solution uses dynamic programming to find the longest common substring between initial and target. Once we know the length mx of this longest common substring, we can calculate that we need to:

  • Remove m - mx characters from initial (where m is the length of initial)
  • Add n - mx characters to match target (where n is the length of target)

Therefore, the total minimum operations needed is m + n - 2 * mx.

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

Intuition

When we can only add or remove characters from the beginning or end of a string, we need to think about what part of the string can remain unchanged during the transformation.

The key observation is that if we have a substring that appears in both initial and target in the same order and position, we can keep it intact during the transformation. We would only need to remove the characters before and after this common substring from initial, and then add the required characters from target.

For example, if initial = "abcde" and target = "bcdef":

  • The common substring "bcde" can stay unchanged
  • We remove "a" from the beginning of initial
  • We add "f" to the end

This leads us to realize that finding the longest common substring between initial and target minimizes the number of operations needed. Why? Because:

  1. Every character not in this common substring from initial needs to be removed
  2. Every character not in this common substring from target needs to be added

If the longest common substring has length mx:

  • We need to remove m - mx characters from initial (all characters not in the common substring)
  • We need to add n - mx characters to form target (all characters in target not in the common substring)
  • Total operations = (m - mx) + (n - mx) = m + n - 2 * mx

This is why we use dynamic programming to find the longest common substring. The DP table f[i][j] tracks the length of the common substring ending at position i-1 in initial and position j-1 in target. When characters match, we extend the previous substring length by 1: f[i][j] = f[i-1][j-1] + 1. When they don't match, the substring breaks, so f[i][j] = 0.

Learn more about Binary Search, Dynamic Programming and Sliding Window patterns.

Solution Approach

We implement the solution using dynamic programming to find the longest common substring between initial and target.

Step 1: Initialize the DP table

We create a 2D array f of size (m+1) × (n+1), where m and n are the lengths of initial and target respectively. The extra row and column handle the base cases when either string is empty.

f = [[0] * (n + 1) for _ in range(m + 1)]

Step 2: Build the DP table

We iterate through each character of both strings. For each pair of characters at positions i-1 in initial and j-1 in target:

  • If the characters match (initial[i-1] == target[j-1]), we extend the common substring from the previous diagonal position: f[i][j] = f[i-1][j-1] + 1
  • If they don't match, the common substring ending at this position has length 0 (implicitly handled by initialization)
for i, a in enumerate(initial, 1):
    for j, b in enumerate(target, 1):
        if a == b:
            f[i][j] = f[i - 1][j - 1] + 1
            mx = max(mx, f[i][j])

The state transition equation is:

f[i][j]={f[i1][j1]+1,if initial[i1]=target[j1]0,otherwisef[i][j] = \begin{cases} f[i - 1][j - 1] + 1, & \text{if } \text{initial}[i - 1] = \text{target}[j - 1] \\ 0, & \text{otherwise} \end{cases}

Step 3: Track the maximum length

As we build the table, we keep track of the maximum value mx, which represents the length of the longest common substring found.

Step 4: Calculate the minimum operations

Once we have mx, the minimum number of operations is:

  • Remove m - mx characters from initial
  • Add n - mx characters to form target
  • Total: m + n - 2 × mx

Time Complexity: O(m × n) where m and n are the lengths of the two strings, as we iterate through all pairs of characters.

Space Complexity: O(m × n) for the DP table.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with initial = "abc" and target = "aec".

Step 1: Setup

  • m = 3 (length of "abc")
  • n = 3 (length of "aec")
  • Initialize DP table f of size 4×4 with all zeros
  • mx = 0 (tracks longest common substring)

Step 2: Build DP Table

We iterate through each character pair and update f[i][j] when characters match:

     ""  a  e  c
""  [0] [0][0][0]
a   [0] [?][?][?]
b   [0] [?][?][?]
c   [0] [?][?][?]
  • i=1, j=1: 'a' == 'a' ✓

    • f[1][1] = f[0][0] + 1 = 1
    • mx = 1
  • i=1, j=2: 'a' != 'e' ✗

    • f[1][2] = 0
  • i=1, j=3: 'a' != 'c' ✗

    • f[1][3] = 0
  • i=2, j=1: 'b' != 'a' ✗

    • f[2][1] = 0
  • i=2, j=2: 'b' != 'e' ✗

    • f[2][2] = 0
  • i=2, j=3: 'b' != 'c' ✗

    • f[2][3] = 0
  • i=3, j=1: 'c' != 'a' ✗

    • f[3][1] = 0
  • i=3, j=2: 'c' != 'e' ✗

    • f[3][2] = 0
  • i=3, j=3: 'c' == 'c' ✓

    • f[3][3] = f[2][2] + 1 = 0 + 1 = 1
    • mx remains 1

Final DP Table:

     ""  a  e  c
""  [0] [0][0][0]
a   [0] [1][0][0]
b   [0] [0][0][0]
c   [0] [0][0][1]

Step 3: Calculate Result

  • Longest common substring length: mx = 1
  • Minimum operations = m + n - 2 × mx = 3 + 3 - 2 × 1 = 4

Verification: The longest common substrings are "a" (at positions 0,0) and "c" (at positions 2,2), both with length 1. Since they can't both be kept (not consecutive), we can keep one:

  • Keep "a": Remove "bc" (2 ops), add "ec" (2 ops) → Total: 4 operations
  • Keep "c": Remove "ab" (2 ops), add "ae" (2 ops) → Total: 4 operations

The answer is 4 operations.

Solution Implementation

1class Solution:
2    def minOperations(self, initial: str, target: str) -> int:
3        # Get lengths of both strings
4        initial_length, target_length = len(initial), len(target)
5      
6        # Create a 2D DP table to store lengths of common substrings
7        # dp[i][j] represents the length of common substring ending at initial[i-1] and target[j-1]
8        dp = [[0] * (target_length + 1) for _ in range(initial_length + 1)]
9      
10        # Track the maximum length of common substring found
11        max_common_substring_length = 0
12      
13        # Iterate through each character in initial string
14        for i, char_initial in enumerate(initial, 1):
15            # Compare with each character in target string
16            for j, char_target in enumerate(target, 1):
17                # If characters match, extend the common substring length
18                if char_initial == char_target:
19                    dp[i][j] = dp[i - 1][j - 1] + 1
20                    max_common_substring_length = max(max_common_substring_length, dp[i][j])
21      
22        # Calculate minimum operations:
23        # - Remove all characters not in the longest common substring from initial
24        # - Add all characters not in the longest common substring from target
25        # Total = (initial_length - max_common) + (target_length - max_common)
26        return initial_length + target_length - max_common_substring_length * 2
27
1class Solution {
2    public int minOperations(String initial, String target) {
3        // Get lengths of both strings
4        int initialLength = initial.length();
5        int targetLength = target.length();
6      
7        // Create DP table to store lengths of common substrings
8        // dp[i][j] represents the length of common substring ending at initial[i-1] and target[j-1]
9        int[][] dp = new int[initialLength + 1][targetLength + 1];
10      
11        // Track the maximum length of common substring found
12        int maxCommonSubstringLength = 0;
13      
14        // Iterate through all characters in both strings
15        for (int i = 1; i <= initialLength; i++) {
16            for (int j = 1; j <= targetLength; j++) {
17                // If characters match, extend the common substring length from previous diagonal cell
18                if (initial.charAt(i - 1) == target.charAt(j - 1)) {
19                    dp[i][j] = dp[i - 1][j - 1] + 1;
20                    // Update maximum common substring length if current is longer
21                    maxCommonSubstringLength = Math.max(maxCommonSubstringLength, dp[i][j]);
22                }
23                // If characters don't match, dp[i][j] remains 0 (default value)
24            }
25        }
26      
27        // Calculate minimum operations needed:
28        // - Delete all characters not in the longest common substring from initial string
29        // - Add all characters not in the longest common substring from target string
30        // Total operations = (initialLength - maxCommon) + (targetLength - maxCommon)
31        return initialLength + targetLength - 2 * maxCommonSubstringLength;
32    }
33}
34
1class Solution {
2public:
3    int minOperations(string initial, string target) {
4        int initialLength = initial.size();
5        int targetLength = target.size();
6      
7        // dp[i][j] represents the length of common substring ending at initial[i-1] and target[j-1]
8        int dp[initialLength + 1][targetLength + 1];
9        memset(dp, 0, sizeof(dp));
10      
11        // Track the maximum length of common substring found
12        int maxCommonLength = 0;
13      
14        // Iterate through all positions in both strings
15        for (int i = 1; i <= initialLength; ++i) {
16            for (int j = 1; j <= targetLength; ++j) {
17                // If characters match, extend the common substring length
18                if (initial[i - 1] == target[j - 1]) {
19                    dp[i][j] = dp[i - 1][j - 1] + 1;
20                    maxCommonLength = max(maxCommonLength, dp[i][j]);
21                }
22                // If characters don't match, dp[i][j] remains 0 (already initialized)
23            }
24        }
25      
26        // Minimum operations = total characters to remove + total characters to add
27        // = (initialLength - maxCommonLength) + (targetLength - maxCommonLength)
28        // = initialLength + targetLength - 2 * maxCommonLength
29        return initialLength + targetLength - 2 * maxCommonLength;
30    }
31};
32
1/**
2 * Calculates the minimum number of operations to transform initial string to target string.
3 * Operations allowed are inserting and deleting characters.
4 * 
5 * @param initial - The initial string to transform
6 * @param target - The target string to achieve
7 * @returns The minimum number of operations needed
8 */
9function minOperations(initial: string, target: string): number {
10    const initialLength: number = initial.length;
11    const targetLength: number = target.length;
12  
13    // Create a 2D DP table to store the length of longest common subsequence
14    // dp[i][j] represents the length of LCS ending at initial[i-1] and target[j-1]
15    const dp: number[][] = Array.from(
16        { length: initialLength + 1 }, 
17        () => Array(targetLength + 1).fill(0)
18    );
19  
20    // Track the maximum length of common subsequence found
21    let maxCommonLength: number = 0;
22  
23    // Fill the DP table
24    for (let i = 1; i <= initialLength; i++) {
25        for (let j = 1; j <= targetLength; j++) {
26            // If characters match, extend the common subsequence
27            if (initial[i - 1] === target[j - 1]) {
28                dp[i][j] = dp[i - 1][j - 1] + 1;
29                maxCommonLength = Math.max(maxCommonLength, dp[i][j]);
30            }
31        }
32    }
33  
34    // Minimum operations = total characters to remove + total characters to add
35    // = (initialLength - maxCommonLength) + (targetLength - maxCommonLength)
36    // = initialLength + targetLength - 2 * maxCommonLength
37    return initialLength + targetLength - 2 * maxCommonLength;
38}
39

Time and Space Complexity

The time complexity is O(m × n), where m is the length of the string initial and n is the length of the string target. This is because the code uses two nested loops: the outer loop iterates through each character in initial (m iterations), and the inner loop iterates through each character in target (n iterations). For each combination of positions, the code performs constant-time operations (comparison, assignment, and max calculation), resulting in a total of m × n operations.

The space complexity is O(m × n) as well. The code creates a 2D array f with dimensions (m + 1) × (n + 1) to store the dynamic programming values. This array requires O((m + 1) × (n + 1)) space, which simplifies to O(m × n). The additional variables (m, n, mx, i, j, a, b) only use constant space O(1), so the overall space complexity is dominated by the 2D array, giving us O(m × n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Confusing Longest Common Substring with Longest Common Subsequence

The Problem: Many developers mistakenly implement a solution for the Longest Common Subsequence (LCS) instead of the Longest Common Substring. This is a critical error because:

  • Longest Common Substring: Characters must be consecutive in both strings
  • Longest Common Subsequence: Characters must appear in the same order but don't need to be consecutive

Example of the mistake:

# INCORRECT: This finds LCS, not longest common substring
for i in range(1, m + 1):
    for j in range(1, n + 1):
        if initial[i-1] == target[j-1]:
            dp[i][j] = dp[i-1][j-1] + 1
        else:
            # This line makes it LCS, not substring!
            dp[i][j] = max(dp[i-1][j], dp[i][j-1])

The Fix: When characters don't match, the DP value should remain 0 (as in the correct solution), not carry forward the maximum from adjacent cells.

Pitfall 2: Incorrect Understanding of Valid Operations

The Problem: The problem states you can only add/remove from the beginning or end of the string. However, the solution treats this as finding the longest common substring that can be preserved. This works because:

  1. Any substring common to both strings can theoretically be reached by removing characters around it
  2. The order of operations allows us to remove from either end until we reach the common substring

Potential Confusion: If someone interprets this too literally, they might think they need to track whether operations are feasible in a specific order, leading to overly complex solutions involving BFS or other search algorithms.

The Fix: Understand that the problem is essentially asking for the minimum edit distance with restricted operations, which simplifies to finding the maximum portion that can remain unchanged (the longest common substring).

Pitfall 3: Memory Optimization Opportunity Missed

The Problem: The current solution uses O(m × n) space, but since we only need the previous row to compute the current row, we could optimize space usage.

Space-Optimized Solution:

def minOperations(self, initial: str, target: str) -> int:
    m, n = len(initial), len(target)
  
    # Use only two rows instead of full matrix
    prev = [0] * (n + 1)
    curr = [0] * (n + 1)
    max_length = 0
  
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if initial[i-1] == target[j-1]:
                curr[j] = prev[j-1] + 1
                max_length = max(max_length, curr[j])
            else:
                curr[j] = 0
        prev, curr = curr, [0] * (n + 1)
  
    return m + n - 2 * max_length

This reduces space complexity from O(m × n) to O(n).

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

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More