Facebook Pixel

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" and k = 2, you can remove characters 'b' and 'e' to get "acdca", which is a palindrome. So this would return true.
  • 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 remove n - L characters to get that palindrome
  • If n - L ≤ k, then we can make a palindrome by removing at most k 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 part s[i+1..j-1]
  • If they don't match, we have to choose: either exclude s[i] and find the best palindrome in s[i+1..j], or exclude s[j] and find the best palindrome in s[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 matrix f where n = len(s)
  • Initialize the diagonal: f[i][i] = 1 for all i, 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 to 0 (outer loop)
  • For each i, iterate j from i+1 to n-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 excluding s[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 Evaluator

Example 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), computing dp[i+1][j-1] means accessing dp[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] where j < 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More