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:
- Remove 'b' from the end (or middle operations to get to "ac")
- 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 frominitial
(wherem
is the length ofinitial
) - Add
n - mx
characters to matchtarget
(wheren
is the length oftarget
)
Therefore, the total minimum operations needed is m + n - 2 * mx
.
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:
- Every character not in this common substring from
initial
needs to be removed - 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 frominitial
(all characters not in the common substring) - We need to add
n - mx
characters to formtarget
(all characters intarget
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:
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 frominitial
- Add
n - mx
characters to formtarget
- 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 EvaluatorExample 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:
- Any substring common to both strings can theoretically be reached by removing characters around it
- 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).
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Want a Structured Path to Master System Design Too? Don’t Miss This!