Topics › Dynamic Programming › DP on Strings
Minimum insertions to make string palindrome - DP-29
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a string s, you may insert any character at any position, as many times as you like. Return the minimum number of insertions needed to make s a palindrome.
Constraints (LeetCode 1312): 1 ≤ n ≤ 500, lowercase English letters. The Java methods also accept the empty string.
Example 1
s = "mbadm"
answer = 2 // "mbdadbm" or "mdbabdm"
Example 2
s = "leetcode"
answer = 5 // e.g. "leetcodocteel"
Key insight: keep a palindrome, mirror the rest
Every original character ends up somewhere in the final palindrome, and its mirror position holds either another original character or an inserted one.
- The original characters whose mirrors are also original form a palindromic subsequence of
s: they keep their order and pair up symmetrically. Every other original character needs its own inserted partner. So the number of insertions is at leastn - LPS(s), where LPS is the length of the Longest palindromic subsequence. - That many is always enough: keep a longest palindromic subsequence in place and, for each of the other
n - LPScharacters, insert a copy at its mirror position.
So answer = n - LPS(s). The same reasoning on a range s[i..j] gives a direct recurrence for f(i, j), the fewest insertions for that range:
s[i] == s[j]: the two ends pair with each other, sof(i, j) = f(i + 1, j - 1).s[i] != s[j]: one end needs an inserted partner. Insert a copy ofs[i]just afterj, which leavess[i + 1..j]to fix, or a copy ofs[j]just beforei, which leavess[i..j - 1]. Sof(i, j) = 1 + min(f(i + 1, j), f(i, j - 1)).i >= j(empty or one character): 0.
Each line is exactly “range length minus LPS of the range” rewritten, so the two views always agree.
Approach 1: Recursion
Intuition. Try both ways of fixing a mismatched end and keep the cheaper; matching ends cost nothing.
Algorithm.
- If
i >= j, return 0. - If
s[i] == s[j], returnf(i + 1, j - 1). - Otherwise return
1 + min(f(i + 1, j), f(i, j - 1)).
static int recursive(String s) {
return solve(0, s.length() - 1, s);
}
/** Fewest insertions that turn s[i..j] (inclusive) into a palindrome. */
static int solve(int i, int j, String s) {
if (i >= j) return 0; // empty or one character: already a palindrome
if (s.charAt(i) == s.charAt(j)) return solve(i + 1, j - 1, s); // the ends already pair up
return 1 + Math.min(solve(i + 1, j, s), // insert a copy of s[i] after j
solve(i, j - 1, s)); // insert a copy of s[j] before i
}
Complexity. Time O(2^n): with all-distinct letters each call branches twice and each branch shrinks the range by one. Space O(n) for the stack.
Approach 2: Memoization
Intuition. Only O(n^2) ranges exist, and both branches of a mismatch reach (i + 1, j - 1) again one level down. Cache every range’s answer; -1 means “not computed” because the true answer is never negative.
Algorithm.
dp[n][n]filled with-1.- Same recursion, returning stored answers and storing new ones.
static int memoization(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int[] row : dp) Arrays.fill(row, -1);
return memo(0, n - 1, s, dp);
}
static int memo(int i, int j, String s, int[][] dp) {
if (i >= j) return 0;
if (dp[i][j] != -1) return dp[i][j];
if (s.charAt(i) == s.charAt(j)) return dp[i][j] = memo(i + 1, j - 1, s, dp);
return dp[i][j] = 1 + Math.min(memo(i + 1, j, s, dp), memo(i, j - 1, s, dp));
}
Complexity. Time O(n^2), space O(n^2) plus O(n) stack.
Approach 3: Tabulation
Intuition. A range reads the range starting one later (row i + 1) and the range ending one earlier (same row, column j - 1). So i runs from n - 1 down to 0 and j runs upward from i + 1. The diagonal and everything below it are 0, which covers both base cases, including dp[i + 1][j - 1] when j == i + 1.
Algorithm.
- For
i = n - 1 .. 0,j = i + 1 .. n - 1: apply the recurrence. - Return
dp[0][n - 1].
static int tabulation(String s) {
int n = s.length();
if (n == 0) return 0;
int[][] dp = new int[n][n]; // dp[i][i] = 0 and i > j entries stay 0
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i + 1][j - 1];
else dp[i][j] = 1 + Math.min(dp[i + 1][j], dp[i][j - 1]);
}
}
return dp[0][n - 1];
}
Dry run on s = "mbadm":
| i \ j | 0 m | 1 b | 2 a | 3 d | 4 m |
|---|---|---|---|---|---|
| 0 m | 0 | 1 | 2 | 3 | 2 |
| 1 b | 0 | 1 | 2 | 3 | |
| 2 a | 0 | 1 | 2 | ||
| 3 d | 0 | 1 | |||
| 4 m | 0 |
Every pair of different adjacent letters costs 1. dp[1][3] (“bad”) is 1 + min(dp[2][3], dp[1][2]) = 2. The corner dp[0][4] has matching ends m, so it copies dp[1][3] = 2.
Complexity. Time O(n^2), space O(n^2).
Approach 4: Space optimized
Intuition. Row i needs only row i + 1 (next) and the part of row i already filled (cur). Two arrays suffice. When j == i + 1 the inner range is empty and the code uses 0 directly instead of reading next[i], so correctness does not depend on what the recycled array happens to contain.
Algorithm.
- For
i = n - 1 .. 0:cur[i] = 0; fillcur[j]forj > i; swapcurandnext. - Return
next[n - 1].
static int spaceOptimized(String s) {
int n = s.length();
if (n == 0) return 0;
int[] next = new int[n], cur = new int[n]; // next = row i + 1, cur = row i
for (int i = n - 1; i >= 0; i--) {
cur[i] = 0;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) cur[j] = j - 1 > i ? next[j - 1] : 0; // j == i + 1: nothing inside
else cur[j] = 1 + Math.min(next[j], cur[j - 1]);
}
int[] tmp = next; next = cur; cur = tmp;
}
return next[n - 1];
}
Complexity. Time O(n^2), space O(n).
Approach 5: Via the longest palindromic subsequence
Intuition. Use the identity from the key insight directly. The LPS of s equals the longest common subsequence of s and its reverse, which two LCS rows compute. Subtract it from n.
static int viaLps(String s) {
String r = new StringBuilder(s).reverse().toString();
int n = s.length();
int[] prev = new int[n + 1], cur = new int[n + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (s.charAt(i - 1) == r.charAt(j - 1)) cur[j] = 1 + prev[j - 1];
else cur[j] = Math.max(prev[j], cur[j - 1]);
}
int[] tmp = prev; prev = cur; cur = tmp;
}
return n - prev[n]; // prev[n] = LCS(s, reverse(s)) = LPS(s)
}
Complexity. Time O(n^2), space O(n). This is the route the A2Z sheet takes: it turns a new problem into one already solved, at the cost of filling a full n × n table instead of half of one.
Edge cases and pitfalls
- Already a palindrome.
"zzazz"needs 0 insertions; every range with matching ends just copies its inner range. - Empty or one-character string. Both are palindromes; every method returns 0 (the tabulated versions return before allocating for
n = 0). - All distinct letters.
"abcd"needsn - 1 = 3: the LPS is a single character. - Deletions instead of insertions. The minimum number of deletions to make
sa palindrome is the same number,n - LPS: delete each character you would otherwise have mirrored. - Memo sentinel. 0 is a common answer, so it cannot mean “not computed”.
- Testing. Besides the fixed cases, 300 seeded random strings over
{a, b, c}are run through all five methods.
Related problems
- Longest palindromic subsequence gives the identity
answer = n - LPS. - Longest common subsequence computes the LPS of
sas the LCS ofsand its reverse. - Minimum insertions or deletions to convert string A to B uses the same “keep the common part, fix the rest” counting.
- Minimum insertions to make string palindrome is the same problem as listed in the SDE sheet.
- Shortest Palindrome allows insertions only at the front, which turns it into a string-matching problem.