1216. Valid Palindrome III 🔒
Problem Description
You are given a string s
and an integer k
. Your task is to determine if the string is a k-palindrome.
A string is considered a k-palindrome if you can transform it into a palindrome by removing at most k
characters from it.
For example:
- If
s = "abcdeca"
andk = 2
, you can remove characters 'b' and 'e' to get "acdca", which is a palindrome. So this would returntrue
. - If a string is already a palindrome, it's a k-palindrome for any
k ≥ 0
. - If you need to remove more than
k
characters to make it a palindrome, then it's not a k-palindrome.
The function should return true
if the string can be made into a palindrome by removing at most k
characters, and false
otherwise.
Intuition
The key insight is to reframe the problem: instead of thinking about which characters to remove, we can think about which characters to keep. If we need to remove at most k
characters to make a palindrome, that means we need to keep at least n - k
characters that form a palindrome.
This transforms our problem into finding the longest palindromic subsequence (LPS) in the string. A subsequence doesn't need to be contiguous - we can pick any characters from the string while maintaining their relative order.
Why does this work? Consider that:
- If the LPS has length
L
, then we need to removen - L
characters to get that palindrome - If
n - L ≤ k
, then we can make a palindrome by removing at mostk
characters - This is equivalent to checking if
L + k ≥ n
For finding the LPS, we can use dynamic programming. The idea is to build up solutions for smaller substrings and combine them:
- For a single character, the LPS length is 1
- For a substring
s[i..j]
, if the characters at both ends match (s[i] == s[j]
), we can include both in our palindrome along with whatever palindrome we can form from the middle parts[i+1..j-1]
- If they don't match, we have to choose: either exclude
s[i]
and find the best palindrome ins[i+1..j]
, or excludes[j]
and find the best palindrome ins[i..j-1]
By systematically building up these solutions from smaller to larger substrings, we can determine if any substring's LPS is long enough that removing the remaining characters would require at most k
deletions.
Learn more about Dynamic Programming patterns.
Solution Approach
We implement the solution using dynamic programming with a 2D table f[i][j]
where f[i][j]
represents the length of the longest palindromic subsequence in the substring s[i..j]
.
Initialization:
- Create an
n x n
matrixf
wheren = len(s)
- Initialize the diagonal:
f[i][i] = 1
for alli
, since every single character is a palindrome of length 1
Building the DP table: We fill the table by iterating through substrings of increasing length:
- Start from
i = n-2
and go backwards to0
(outer loop) - For each
i
, iteratej
fromi+1
ton-1
(inner loop) - This ensures we always have the values for smaller substrings before computing larger ones
Transition formula:
For each substring s[i..j]
:
-
If
s[i] == s[j]
: The characters at both ends match, so we can include both in our palindrome:f[i][j] = f[i+1][j-1] + 2
We add 2 to the LPS of the inner substring
s[i+1..j-1]
-
If
s[i] != s[j]
: The characters don't match, so we take the maximum of:f[i][j] = max(f[i+1][j], f[i][j-1])
Either excluding
s[i]
or excludings[j]
Early termination optimization:
After computing f[i][j]
, we immediately check if f[i][j] + k >= n
:
- If true, we've found a palindromic subsequence that's long enough
- This means we can remove at most
n - f[i][j]
characters, which is≤ k
- Return
True
immediately
Final check:
If we complete all iterations without finding a valid palindrome, return False
.
The time complexity is O(n²)
for filling the DP table, and space complexity is also O(n²)
for storing the 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 s = "abcdeca"
and k = 2
.
Step 1: Initialize the DP table We create a 7×7 matrix (since n=7) and initialize the diagonal with 1s:
a b c d e c a 0 1 2 3 4 5 6 a 0 [1 ? ? ? ? ? ?] b 1 [ 1 ? ? ? ? ?] c 2 [ 1 ? ? ? ?] d 3 [ 1 ? ? ?] e 4 [ 1 ? ?] c 5 [ 1 ?] a 6 [ 1]
Step 2: Fill the table bottom-up We start from i=5 (second-to-last row) and work backwards:
When i=5, j=6:
- Compare s[5]='c' with s[6]='a' → they don't match
- f[5][6] = max(f[6][6], f[5][5]) = max(1, 1) = 1
- Check: 1 + 2 >= 7? No, continue
When i=4:
- j=5: s[4]='e' vs s[5]='c' → don't match → f[4][5] = 1
- j=6: s[4]='e' vs s[6]='a' → don't match → f[4][6] = max(f[5][6], f[4][5]) = 1
When i=3:
- j=4: s[3]='d' vs s[4]='e' → don't match → f[3][4] = 1
- j=5: s[3]='d' vs s[5]='c' → don't match → f[3][5] = 1
- j=6: s[3]='d' vs s[6]='a' → don't match → f[3][6] = 1
When i=2:
- j=3: s[2]='c' vs s[3]='d' → don't match → f[2][3] = 1
- j=4: s[2]='c' vs s[4]='e' → don't match → f[2][4] = 1
- j=5: s[2]='c' vs s[5]='c' → match! → f[2][5] = f[3][4] + 2 = 1 + 2 = 3
- Check: 3 + 2 >= 7? No, continue
- j=6: s[2]='c' vs s[6]='a' → don't match → f[2][6] = max(f[3][6], f[2][5]) = 3
When i=1:
- j=2: s[1]='b' vs s[2]='c' → don't match → f[1][2] = 1
- j=3: s[1]='b' vs s[3]='d' → don't match → f[1][3] = 1
- j=4: s[1]='b' vs s[4]='e' → don't match → f[1][4] = 1
- j=5: s[1]='b' vs s[5]='c' → don't match → f[1][5] = max(f[2][5], f[1][4]) = 3
- j=6: s[1]='b' vs s[6]='a' → don't match → f[1][6] = max(f[2][6], f[1][5]) = 3
When i=0:
- j=1: s[0]='a' vs s[1]='b' → don't match → f[0][1] = 1
- j=2: s[0]='a' vs s[2]='c' → don't match → f[0][2] = 1
- j=3: s[0]='a' vs s[3]='d' → don't match → f[0][3] = 1
- j=4: s[0]='a' vs s[4]='e' → don't match → f[0][4] = 1
- j=5: s[0]='a' vs s[5]='c' → don't match → f[0][5] = max(f[1][5], f[0][4]) = 3
- j=6: s[0]='a' vs s[6]='a' → match! → f[0][6] = f[1][5] + 2 = 3 + 2 = 5
- Check: 5 + 2 >= 7? Yes!
Step 3: Return True
We found that the longest palindromic subsequence from index 0 to 6 has length 5. This means we only need to remove 7 - 5 = 2 characters, which equals our k value. The function returns True
.
The palindromic subsequence is "acdca" (keeping characters at positions 0, 2, 3, 5, 6), achieved by removing 'b' and 'e'.
Solution Implementation
1class Solution:
2 def isValidPalindrome(self, s: str, k: int) -> bool:
3 """
4 Check if string s can become a palindrome by removing at most k characters.
5
6 Args:
7 s: Input string to check
8 k: Maximum number of characters that can be removed
9
10 Returns:
11 True if s can become a palindrome with at most k removals, False otherwise
12 """
13 n = len(s)
14
15 # dp[i][j] represents the length of longest palindromic subsequence
16 # in substring s[i:j+1]
17 dp = [[0] * n for _ in range(n)]
18
19 # Base case: single character is always a palindrome of length 1
20 for i in range(n):
21 dp[i][i] = 1
22
23 # Fill the dp table from bottom-right to top-left
24 # Process substrings of increasing length
25 for i in range(n - 2, -1, -1):
26 for j in range(i + 1, n):
27 if s[i] == s[j]:
28 # If characters match, extend the palindrome from inner substring
29 # Add 2 for the matching characters at both ends
30 dp[i][j] = dp[i + 1][j - 1] + 2
31 else:
32 # If characters don't match, take the maximum of:
33 # - Excluding character at position i
34 # - Excluding character at position j
35 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
36
37 # Early termination optimization:
38 # If longest palindromic subsequence + k >= total length,
39 # we can form a palindrome by removing at most k characters
40 if dp[i][j] + k >= n:
41 return True
42
43 # Check if the full string satisfies the condition
44 return dp[0][n - 1] + k >= n
45
1class Solution {
2 /**
3 * Determines if a string can be made into a palindrome by removing at most k characters.
4 *
5 * @param s the input string to check
6 * @param k the maximum number of characters that can be removed
7 * @return true if the string can be made into a palindrome with at most k removals, false otherwise
8 */
9 public boolean isValidPalindrome(String s, int k) {
10 int length = s.length();
11
12 // dp[i][j] represents the length of the longest palindromic subsequence
13 // in the substring s[i...j]
14 int[][] dp = new int[length][length];
15
16 // Initialize base case: single characters are palindromes of length 1
17 for (int i = 0; i < length; ++i) {
18 dp[i][i] = 1;
19 }
20
21 // Fill the DP table bottom-up
22 // Process substrings of increasing length
23 for (int startIndex = length - 2; startIndex >= 0; --startIndex) {
24 for (int endIndex = startIndex + 1; endIndex < length; ++endIndex) {
25 // If characters at both ends match, extend the palindrome
26 if (s.charAt(startIndex) == s.charAt(endIndex)) {
27 dp[startIndex][endIndex] = dp[startIndex + 1][endIndex - 1] + 2;
28 } else {
29 // Otherwise, take the maximum by excluding either the start or end character
30 dp[startIndex][endIndex] = Math.max(
31 dp[startIndex + 1][endIndex],
32 dp[startIndex][endIndex - 1]
33 );
34 }
35
36 // Early termination: if longest palindromic subsequence + k >= string length,
37 // we can form a palindrome by removing at most k characters
38 if (dp[startIndex][endIndex] + k >= length) {
39 return true;
40 }
41 }
42 }
43
44 return false;
45 }
46}
47
1class Solution {
2public:
3 bool isValidPalindrome(string s, int k) {
4 int n = s.length();
5
6 // dp[i][j] represents the length of longest palindromic subsequence
7 // in substring s[i...j]
8 int dp[n][n];
9 memset(dp, 0, sizeof(dp));
10
11 // Base case: single character is always a palindrome of length 1
12 for (int i = 0; i < n; ++i) {
13 dp[i][i] = 1;
14 }
15
16 // Fill the dp table bottom-up
17 // Start from second last row and move upward
18 for (int i = n - 2; i >= 0; --i) {
19 // For each row, fill columns from i+1 to n-1
20 for (int j = i + 1; j < n; ++j) {
21 // If characters at both ends match, add 2 to the inner substring's result
22 if (s[i] == s[j]) {
23 dp[i][j] = dp[i + 1][j - 1] + 2;
24 } else {
25 // If they don't match, take maximum of excluding either end
26 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
27 }
28
29 // Early termination: if longest palindromic subsequence plus k deletions
30 // can cover the entire string, it's valid
31 if (dp[i][j] + k >= n) {
32 return true;
33 }
34 }
35 }
36
37 return false;
38 }
39};
40
1/**
2 * Determines if a string can be made into a palindrome by removing at most k characters
3 * @param s - The input string to check
4 * @param k - Maximum number of characters that can be removed
5 * @returns true if the string can be made into a palindrome with at most k deletions
6 */
7function isValidPalindrome(s: string, k: number): boolean {
8 const stringLength = s.length;
9
10 // dp[i][j] represents the length of the longest palindromic subsequence
11 // in the substring from index i to j
12 const dp: number[][] = Array.from(
13 { length: stringLength },
14 () => Array.from({ length: stringLength }, () => 0)
15 );
16
17 // Base case: single characters are palindromes of length 1
18 for (let i = 0; i < stringLength; i++) {
19 dp[i][i] = 1;
20 }
21
22 // Fill the dp table bottom-up
23 // Start from the second last row and move upwards
24 for (let startIndex = stringLength - 2; startIndex >= 0; startIndex--) {
25 // For each starting position, check all ending positions after it
26 for (let endIndex = startIndex + 1; endIndex < stringLength; endIndex++) {
27 // If characters at both ends match, add 2 to the inner subsequence length
28 if (s[startIndex] === s[endIndex]) {
29 dp[startIndex][endIndex] = dp[startIndex + 1][endIndex - 1] + 2;
30 } else {
31 // Otherwise, take the maximum of excluding either the left or right character
32 dp[startIndex][endIndex] = Math.max(
33 dp[startIndex + 1][endIndex],
34 dp[startIndex][endIndex - 1]
35 );
36 }
37
38 // Early termination: if longest palindromic subsequence plus k
39 // is at least the string length, we can form a palindrome
40 if (dp[startIndex][endIndex] + k >= stringLength) {
41 return true;
42 }
43 }
44 }
45
46 return false;
47}
48
Time and Space Complexity
The time complexity is O(n^2)
, where n
is the length of the string s
. This is because the algorithm uses two nested loops - the outer loop iterates from n-2
to 0
(approximately n
iterations), and for each iteration, the inner loop runs from i+1
to n
(up to n
iterations in the worst case). Each operation inside the nested loops takes constant time O(1)
, resulting in an overall time complexity of O(n^2)
.
The space complexity is O(n^2)
, where n
is the length of the string s
. This is due to the 2D array f
of size n × n
that stores the dynamic programming states. Each cell in this array stores the length of the longest palindromic subsequence for the substring s[i:j+1]
, requiring O(n^2)
space in total.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Index Out of Bounds When Accessing dp[i+1][j-1]
The most common pitfall in this solution is accessing dp[i+1][j-1]
when i+1 > j-1
, which happens when processing substrings of length 2 (where j = i+1
). In this case, we're trying to access an "invalid" substring where the start index is greater than the end index.
Why this happens:
- When
j = i+1
(substring of length 2), computingdp[i+1][j-1]
means accessingdp[i+1][i]
- This represents an empty substring between the two characters
- The value at this position might be uninitialized (0) or garbage, leading to incorrect results
Solution: Add a special case to handle substrings of length 2:
if s[i] == s[j]:
if j == i + 1: # Adjacent characters
dp[i][j] = 2
else:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
Or alternatively, initialize the base case for empty substrings:
# After initializing diagonal elements
# Handle the conceptual "empty" substrings
for i in range(n):
for j in range(i):
dp[i][j] = 0 # Empty substring has LPS of 0
2. Incorrect Early Termination Logic
Another pitfall is misunderstanding when to return True
. The condition dp[i][j] + k >= n
checks if we can keep enough characters to form a palindrome, but placing this check inside the nested loops means we're checking partial substrings, not just the full string.
Why this could be problematic:
- Checking partial substrings
s[i..j]
wherej < n-1
might give false positives - We might return
True
based on a substring's properties rather than the full string
Solution: If you want to be more conservative and only check the full string:
# Remove the early termination inside the loops
for i in range(n - 2, -1, -1):
for j in range(i + 1, n):
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
# Only check the full string at the end
return dp[0][n - 1] + k >= n
However, the original early termination is actually correct because if any substring s[i..j]
containing all characters (when j = n-1
) satisfies the condition, then the full string definitely can be made into a palindrome with at most k removals.
Which of the following is a min heap?
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!