Given a string s
and an integer k
, determine if s
can be transformed into a palindrome by removing at most k
characters. In other words, you may delete up to k
characters from s
, and after these deletions, the remaining string should read the same forwards and backwards.
Constraints:
s.length
≤ 1000k
≤ s.length
s
consists only of lowercase English letters.
At first glance, the problem resembles the classic palindrome check, but with the added flexibility of deleting up to k
characters. A brute-force approach would be to try all possible ways of deleting up to k
characters and check if any result in a palindrome. However, this is computationally expensive and impractical for strings of length up to 1000.
To optimize, we can draw on principles from dynamic programming (DP). The key insight is: the minimum number of deletions needed to make s
a palindrome is related to the length of its longest palindromic subsequence (LPS). If the minimum deletions required is less than or equal to k
, then it is possible.
Alternatively, we can use a DP approach to directly compute whether a substring can be made a palindrome using at most k
deletions, by recursively checking and memoizing the results.
dp(i, j)
that returns the minimum number of deletions needed to make s[i..j]
a palindrome.i >= j
, the substring is already a palindrome (empty or single character), so return 0.s[i] == s[j]
, no deletion is needed at the ends, so recurse on dp(i+1, j-1)
.s[i] != s[j]
, we need to delete either s[i]
or s[j]
. So, compute 1 + min(dp(i+1, j), dp(i, j-1))
.dp(0, n-1)
to k
. If it is less than or equal to k
, return true; otherwise, false.s.length - LPS(s)
.k
, return true.
Example: s = "abcdeca"
, k = 2
k
deletions. For each character, decide to delete or not.n
.dp(i, j)
is computed once.
The "Valid Palindrome III" problem asks whether a string can be made into a palindrome by deleting at most k
characters. Brute-force is impractical, but dynamic programming—either via direct minimum deletions or longest palindromic subsequence—offers an elegant O(n^2) solution. The core insight is to break the problem into subproblems of substrings, use memoization to avoid redundant work, and check if the minimum deletions needed fit within the allowed budget k
.
class Solution:
def isValidPalindrome(self, s: str, k: int) -> bool:
from functools import lru_cache
n = len(s)
@lru_cache(maxsize=None)
def dp(i, j):
if i >= j:
return 0
if s[i] == s[j]:
return dp(i + 1, j - 1)
else:
return 1 + min(dp(i + 1, j), dp(i, j - 1))
return dp(0, n - 1) <= k
class Solution {
public:
bool isValidPalindrome(string s, int k) {
int n = s.size();
vector<vector<int>> memo(n, vector<int>(n, -1));
function<int(int, int)> dp = [&](int i, int j) {
if (i >= j) return 0;
if (memo[i][j] != -1) return memo[i][j];
if (s[i] == s[j])
memo[i][j] = dp(i + 1, j - 1);
else
memo[i][j] = 1 + min(dp(i + 1, j), dp(i, j - 1));
return memo[i][j];
};
return dp(0, n - 1) <= k;
}
};
class Solution {
public boolean isValidPalindrome(String s, int k) {
int n = s.length();
Integer[][] memo = new Integer[n][n];
return dp(s, 0, n - 1, memo) <= k;
}
private int dp(String s, int i, int j, Integer[][] memo) {
if (i >= j) return 0;
if (memo[i][j] != null) return memo[i][j];
if (s.charAt(i) == s.charAt(j))
memo[i][j] = dp(s, i + 1, j - 1, memo);
else
memo[i][j] = 1 + Math.min(dp(s, i + 1, j, memo), dp(s, i, j - 1, memo));
return memo[i][j];
}
}
var isValidPalindrome = function(s, k) {
const n = s.length;
const memo = Array.from({length: n}, () => Array(n).fill(-1));
function dp(i, j) {
if (i >= j) return 0;
if (memo[i][j] !== -1) return memo[i][j];
if (s[i] === s[j])
memo[i][j] = dp(i + 1, j - 1);
else
memo[i][j] = 1 + Math.min(dp(i + 1, j), dp(i, j - 1));
return memo[i][j];
}
return dp(0, n - 1) <= k;
};