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
kcharacters 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 - Lcharacters to get that palindrome - If
n - L ≤ k, then we can make a palindrome by removing at mostkcharacters - 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 nmatrixfwheren = len(s) - Initialize the diagonal:
f[i][i] = 1for 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-2and go backwards to0(outer loop) - For each
i, iteratejfromi+1ton-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] + 2We 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
Trueimmediately
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
451class 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}
471class 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};
401/**
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}
48Time 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-1might give false positives - We might return
Truebased 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.
A heap is a ...?
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!