1312. Minimum Insertion Steps to Make a String Palindrome
Problem Description
You are given a string s
. You can insert any character at any position in the string (including at the beginning or end). Your goal is to find the minimum number of character insertions needed to make the string a palindrome.
A palindrome is a string that reads the same forwards and backwards. For example, "racecar" and "noon" are palindromes.
The solution uses dynamic programming with memoization. The approach works by comparing characters from both ends of the string:
- The function
dfs(i, j)
finds the minimum insertions needed to make the substring from indexi
to indexj
a palindrome - If
i >= j
, we've processed the entire substring (base case), so return 0 - If the characters at positions
i
andj
match (s[i] == s[j]
), we can move both pointers inward without any insertions - If they don't match, we have two choices:
- Insert a character matching
s[j]
before positioni
, then solve fordfs(i, j-1)
- Insert a character matching
s[i]
after positionj
, then solve fordfs(i+1, j)
- Take the minimum of these two options and add 1 for the insertion
- Insert a character matching
The @cache
decorator memoizes results to avoid recalculating the same subproblems.
For example, with string "ab":
- Characters don't match, so we need at least 1 insertion
- We can either insert 'b' at the beginning to get "bab" or insert 'a' at the end to get "aba"
- Both result in 1 insertion, which is the minimum
Intuition
The key insight is recognizing that this problem is closely related to finding the Longest Palindromic Subsequence (LPS). When we think about making a string a palindrome through insertions, we're essentially trying to match characters that don't already form a palindromic pattern.
Consider what happens when we examine a string from both ends. If the characters at the two ends match, they already contribute to the palindromic structure - we don't need to insert anything for them. The problem then reduces to making the middle portion a palindrome.
When the characters don't match, we face a decision: which character should we "duplicate" through insertion? If we have s[i]
on the left and s[j]
on the right that don't match, we could either:
- Insert
s[j]
somewhere on the left side to match with the existings[j]
on the right - Insert
s[i]
somewhere on the right side to match with the existings[i]
on the left
This naturally leads to a recursive structure where we try both options and pick the one requiring fewer insertions. The beauty of this approach is that it breaks down the problem into smaller subproblems - each recursive call handles a smaller substring.
The relationship to LPS becomes clearer when we realize that the minimum insertions needed equals n - LPS_length
, where n
is the string length. The characters already in the LPS don't need matching insertions; we only need to insert characters for those not part of the LPS. However, directly computing using the two-pointer comparison approach as shown in the solution is more intuitive and equally efficient.
The overlapping subproblems make this perfect for dynamic programming - the same substring ranges will be evaluated multiple times in different recursive paths, so memoization dramatically improves efficiency from exponential to O(n²)
.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach using recursion with memoization. Let's walk through the implementation step by step:
Function Structure:
The main function minInsertions
defines a nested helper function dfs(i, j)
that computes the minimum insertions needed for the substring s[i:j+1]
.
Memoization:
The @cache
decorator is used to automatically memoize the results of dfs
. This prevents redundant calculations when the same substring range (i, j)
is encountered multiple times during recursion.
Base Case:
if i >= j: return 0
When i >= j
, it means we're dealing with either:
- A single character (
i == j
), which is already a palindrome - An empty substring (
i > j
), which is also considered a palindrome Both cases require 0 insertions.
Matching Characters:
if s[i] == s[j]: return dfs(i + 1, j - 1)
When characters at both ends match, they already form part of the palindrome structure. We move both pointers inward without adding any insertions, recursively solving for the inner substring.
Non-matching Characters:
return 1 + min(dfs(i + 1, j), dfs(i, j - 1))
When characters don't match, we have two choices:
dfs(i + 1, j)
: Skip character at positioni
, meaning we'll insert a matching character fors[i]
on the right side laterdfs(i, j - 1)
: Skip character at positionj
, meaning we'll insert a matching character fors[j]
on the left side later
We take the minimum of both options and add 1 for the required insertion.
Initial Call:
return dfs(0, len(s) - 1)
The recursion starts with the entire string, from index 0 to len(s) - 1
.
Time Complexity: O(n²)
where n is the length of the string, as there are n²
possible unique (i, j)
pairs, and each is computed once due to memoization.
Space Complexity: O(n²)
for the memoization cache storing results for all possible substring ranges.
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 trace through the algorithm with the string s = "abc"
to find the minimum insertions needed to make it a palindrome.
Initial Call: dfs(0, 2)
- examining the full string "abc"
- Compare
s[0]='a'
withs[2]='c'
- They don't match, so we need to choose between two options:
- Option 1:
dfs(1, 2)
- skip 'a', will insert 'a' on the right later - Option 2:
dfs(0, 1)
- skip 'c', will insert 'c' on the left later
- Option 1:
Exploring Option 1: dfs(1, 2)
- examining substring "bc"
- Compare
s[1]='b'
withs[2]='c'
- They don't match, so we evaluate:
dfs(2, 2)
- single character 'c', returns 0 (base case)dfs(1, 1)
- single character 'b', returns 0 (base case)
- Result:
1 + min(0, 0) = 1
Exploring Option 2: dfs(0, 1)
- examining substring "ab"
- Compare
s[0]='a'
withs[1]='b'
- They don't match, so we evaluate:
dfs(1, 1)
- single character 'b', returns 0 (base case)dfs(0, 0)
- single character 'a', returns 0 (base case)
- Result:
1 + min(0, 0) = 1
Final Calculation: dfs(0, 2) = 1 + min(1, 1) = 2
The algorithm determines we need 2 insertions to make "abc" a palindrome. We could either:
- Insert 'c' at the beginning and 'a' at the end: "cabca" → "cabca"
- Insert 'b' after 'a' and 'a' at the end: "abcba" → "abcba"
Both create valid palindromes with exactly 2 insertions, confirming our result.
Solution Implementation
1class Solution:
2 def minInsertions(self, s: str) -> int:
3 """
4 Find minimum number of insertions needed to make a string palindrome.
5
6 Args:
7 s: Input string
8
9 Returns:
10 Minimum number of character insertions required
11 """
12 from functools import cache
13
14 @cache
15 def dfs(left: int, right: int) -> int:
16 """
17 Recursively find minimum insertions for substring s[left:right+1].
18
19 Args:
20 left: Left pointer index
21 right: Right pointer index
22
23 Returns:
24 Minimum insertions needed for current substring
25 """
26 # Base case: single character or empty substring is already palindrome
27 if left >= right:
28 return 0
29
30 # If characters at both ends match, no insertion needed for these positions
31 if s[left] == s[right]:
32 return dfs(left + 1, right - 1)
33
34 # Characters don't match, need to insert
35 # Either insert s[right] at beginning or s[left] at end
36 insert_at_left = dfs(left, right - 1) # Insert s[right] before s[left]
37 insert_at_right = dfs(left + 1, right) # Insert s[left] after s[right]
38
39 return 1 + min(insert_at_left, insert_at_right)
40
41 # Start with entire string
42 return dfs(0, len(s) - 1)
43
1class Solution {
2 // Memoization table to store results of subproblems
3 // dp[i][j] represents minimum insertions needed for substring from index i to j
4 private Integer[][] dp;
5 private String inputString;
6
7 /**
8 * Finds the minimum number of insertions needed to make the string a palindrome
9 * @param s The input string
10 * @return Minimum number of character insertions required
11 */
12 public int minInsertions(String s) {
13 this.inputString = s;
14 int n = s.length();
15 // Initialize memoization table
16 dp = new Integer[n][n];
17 // Start recursive process from the entire string (index 0 to n-1)
18 return calculateMinInsertions(0, n - 1);
19 }
20
21 /**
22 * Recursive helper function with memoization to calculate minimum insertions
23 * @param left Left pointer of the current substring
24 * @param right Right pointer of the current substring
25 * @return Minimum insertions needed for substring from left to right
26 */
27 private int calculateMinInsertions(int left, int right) {
28 // Base case: single character or empty substring needs no insertions
29 if (left >= right) {
30 return 0;
31 }
32
33 // Return memoized result if already computed
34 if (dp[left][right] != null) {
35 return dp[left][right];
36 }
37
38 int minInsertions;
39
40 // If characters at both ends match, no insertion needed for these positions
41 // Check the substring between them
42 if (inputString.charAt(left) == inputString.charAt(right)) {
43 minInsertions = calculateMinInsertions(left + 1, right - 1);
44 } else {
45 // Characters don't match, we need to insert one character
46 // Either insert character at left to match right, or insert character at right to match left
47 // Take the minimum of both options and add 1 for the insertion
48 minInsertions = Math.min(
49 calculateMinInsertions(left + 1, right), // Insert s[right] at position left
50 calculateMinInsertions(left, right - 1) // Insert s[left] at position right
51 ) + 1;
52 }
53
54 // Store and return the result
55 dp[left][right] = minInsertions;
56 return minInsertions;
57 }
58}
59
1class Solution {
2public:
3 int minInsertions(string s) {
4 int n = s.size();
5
6 // dp[i][j] represents the minimum number of insertions needed
7 // to make substring s[i...j] a palindrome
8 int dp[n][n];
9 memset(dp, -1, sizeof(dp));
10
11 // Recursive function with memoization to find minimum insertions
12 function<int(int, int)> dfs = [&](int left, int right) -> int {
13 // Base case: single character or empty string is already a palindrome
14 if (left >= right) {
15 return 0;
16 }
17
18 // Return memoized result if already computed
19 if (dp[left][right] != -1) {
20 return dp[left][right];
21 }
22
23 int result = INT_MAX;
24
25 // If characters at both ends match, no insertion needed for these two
26 // Just check the inner substring
27 if (s[left] == s[right]) {
28 result = dfs(left + 1, right - 1);
29 } else {
30 // Characters don't match, we need to insert one character
31 // Option 1: Insert s[right] at the beginning (match with right end)
32 // Option 2: Insert s[left] at the end (match with left end)
33 result = min(dfs(left + 1, right), dfs(left, right - 1)) + 1;
34 }
35
36 // Store and return the result
37 return dp[left][right] = result;
38 };
39
40 // Find minimum insertions for the entire string
41 return dfs(0, n - 1);
42 }
43};
44
1function minInsertions(s: string): number {
2 const n: number = s.length;
3
4 // dp[i][j] represents the minimum number of insertions needed
5 // to make substring s[i...j] a palindrome
6 const dp: number[][] = Array(n).fill(null).map(() => Array(n).fill(-1));
7
8 // Recursive function with memoization to find minimum insertions
9 const dfs = (left: number, right: number): number => {
10 // Base case: single character or empty string is already a palindrome
11 if (left >= right) {
12 return 0;
13 }
14
15 // Return memoized result if already computed
16 if (dp[left][right] !== -1) {
17 return dp[left][right];
18 }
19
20 let result: number;
21
22 // If characters at both ends match, no insertion needed for these two
23 // Just check the inner substring
24 if (s[left] === s[right]) {
25 result = dfs(left + 1, right - 1);
26 } else {
27 // Characters don't match, we need to insert one character
28 // Option 1: Insert s[right] at the beginning (match with right end)
29 // Option 2: Insert s[left] at the end (match with left end)
30 result = Math.min(dfs(left + 1, right), dfs(left, right - 1)) + 1;
31 }
32
33 // Store and return the result
34 dp[left][right] = result;
35 return result;
36 };
37
38 // Find minimum insertions for the entire string
39 return dfs(0, n - 1);
40}
41
Time and Space Complexity
Time Complexity: O(n^2)
where n
is the length of string s
.
The recursive function dfs(i, j)
explores all possible substrings of s
. The memoization decorator @cache
ensures each unique state (i, j)
is computed only once. Since i
ranges from 0
to n-1
and j
ranges from 0
to n-1
, there are at most n^2
unique states. Each state performs constant time operations (comparison and minimum calculation), resulting in O(n^2)
time complexity.
Space Complexity: O(n^2)
The space complexity consists of two components:
- Memoization cache: The
@cache
decorator stores up toO(n^2)
unique states, each takingO(1)
space to store the result. - Recursion stack: In the worst case, the recursion depth can be
O(n)
when we continuously move eitheri
forward orj
backward without finding matching characters.
The dominant factor is the memoization cache, giving us an overall space complexity of O(n^2)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Handling
A common mistake is using only i == j
as the base case, forgetting that pointers can cross over (i > j
) when processing even-length palindromic substrings.
Incorrect:
if i == j: # Wrong! Misses the i > j case return 0
Correct:
if i >= j: # Handles both single character and crossed pointers return 0
2. Confusion About What We're Actually Inserting
Many developers get confused about the insertion logic. When s[i] != s[j]
, the recursive calls don't represent "inserting at position i or j" but rather:
dfs(i+1, j)
: We're planning to inserts[i]
somewhere on the right side to match itdfs(i, j-1)
: We're planning to inserts[j]
somewhere on the left side to match it
Mental Model Error: Thinking we need to track actual insertion positions or build the palindrome string.
Solution: Focus on counting insertions only. The algorithm determines the minimum count without constructing the actual palindrome.
3. Forgetting to Import or Apply Memoization
Without memoization, the solution has exponential time complexity O(2^n), causing timeout on larger inputs.
Incorrect:
def dfs(left, right): # No memoization - will be too slow!
if left >= right:
return 0
# ... rest of logic
Correct:
from functools import cache
@cache # Critical for performance!
def dfs(left, right):
# ... implementation
4. Using Wrong Indices in Recursive Calls
A subtle but critical error is incrementing/decrementing the wrong pointer when characters don't match.
Incorrect:
if s[left] != s[right]:
# Wrong! Both moving the same direction
return 1 + min(dfs(left + 1, right + 1), dfs(left - 1, right - 1))
Correct:
if s[left] != s[right]:
# One pointer moves inward while the other stays
return 1 + min(dfs(left + 1, right), dfs(left, right - 1))
5. Alternative But Incorrect DP Formulation
Some might try to use a different DP state definition, like "minimum insertions to make s[0:i] a palindrome", which doesn't work because palindromes require matching from both ends.
Why This Fails: Single-ended DP can't capture the two-pointer matching nature of palindrome checking.
Solution: Always use two pointers (i, j) to represent the substring being processed, allowing comparison of characters from both ends.
Which technique can we use to find the middle of a linked list?
Recommended Readings
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
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!