Facebook Pixel

1278. Palindrome Partitioning III

Problem Description

You are given a string s containing lowercase letters and an integer k. Your task is to perform two operations:

  1. Change characters: First, you can change some characters in s to other lowercase English letters.
  2. Partition into palindromes: Then, divide the modified string into exactly k non-empty, non-overlapping substrings where each substring must be a palindrome.

The goal is to find the minimum number of character changes needed to make such a partition possible.

For example, if you have a string that needs to be divided into k palindromic parts, you want to strategically change the fewest characters possible so that when you split the string into k consecutive substrings, each piece forms a palindrome.

A palindrome is a string that reads the same forwards and backwards (like "aba" or "racecar"). The substrings must be disjoint (non-overlapping) and together they must form the entire original string s.

The function should return a single integer representing the minimum number of character changes required to achieve this palindromic partition.

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

Intuition

The key insight is that this is an optimization problem with two interconnected decisions: where to partition the string and which characters to change. We need to find the best way to split the string into k parts while minimizing character changes.

Let's think about this step by step. If we need to divide a string into k palindromic parts, we're essentially making k-1 cuts in the string. For each resulting substring, we need to figure out how many characters must be changed to make it a palindrome.

For any substring to become a palindrome, we compare characters from both ends moving toward the center. If characters at mirror positions don't match, we need to change one of them. For a substring s[i...j], the minimum changes needed is the number of mismatched pairs when comparing s[i] with s[j], s[i+1] with s[j-1], and so on.

This suggests a dynamic programming approach where we build up solutions for smaller subproblems. We can think of it as: "What's the minimum cost to partition the first i characters into j palindromic parts?"

For each position i and number of parts j, we try all possible positions for the last partition. If we place the (j-1)-th partition at position h, then we need:

  • The optimal cost for partitioning the first h characters into j-1 parts
  • Plus the cost of making characters from position h to i-1 into a palindrome

By trying all valid positions for h and taking the minimum, we find the optimal solution for each subproblem. The preprocessing step calculates the palindrome conversion cost for all possible substrings, allowing us to quickly look up these values during the main dynamic programming computation.

This bottom-up approach ensures we consider all possible ways to partition the string while reusing previously computed optimal solutions for smaller subproblems.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses dynamic programming with preprocessing to efficiently solve this problem.

Step 1: Preprocessing - Calculate palindrome conversion costs

First, we create a 2D array g[i][j] where g[i][j] represents the minimum number of changes needed to convert substring s[i...j] into a palindrome.

g = [[0] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
    for j in range(i + 1, n):
        g[i][j] = int(s[i] != s[j])
        if i + 1 < j:
            g[i][j] += g[i + 1][j - 1]

We iterate from bottom-right to top-left of the matrix:

  • For each substring s[i...j], we check if the characters at positions i and j match
  • If they don't match, we need 1 change (stored as int(s[i] != s[j]))
  • If there are more characters between them (i + 1 < j), we add the cost for the inner substring s[i+1...j-1]

This gives us the palindrome conversion cost for any substring in O(n²) time.

Step 2: Dynamic Programming - Find optimal partitioning

We define f[i][j] as the minimum changes needed to partition the first i characters into j palindromic substrings.

f = [[0] * (k + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
    for j in range(1, min(i, k) + 1):
        if j == 1:
            f[i][j] = g[0][i - 1]
        else:
            f[i][j] = inf
            for h in range(j - 1, i):
                f[i][j] = min(f[i][j], f[h][j - 1] + g[h][i - 1])

The algorithm works as follows:

  • For each position i (1 to n) and number of partitions j (1 to min(i, k)):
    • Base case: If j = 1, we need to make the entire substring s[0...i-1] a palindrome, so f[i][j] = g[0][i-1]
    • General case: For j > 1, we try all possible positions h for the (j-1)-th partition:
      • The cost is f[h][j-1] (optimal cost for first h characters with j-1 partitions)
      • Plus g[h][i-1] (cost to make substring from position h to i-1 a palindrome)
      • We take the minimum across all valid h values

The final answer is f[n][k], representing the minimum changes needed to partition all n characters into exactly k palindromic substrings.

Time Complexity: O(n²k) - We have n × k states, and for each state, we try up to n positions for the last partition.

Space Complexity: O(n² + nk) - For the preprocessing array g and the DP array f.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with string s = "abcac" and k = 2 (we need to partition into 2 palindromic substrings).

Step 1: Preprocessing - Calculate palindrome conversion costs

We build the g matrix where g[i][j] = minimum changes to make substring s[i...j] a palindrome.

For s = "abcac":

  • g[0][1]: substring "ab" → needs 1 change (a≠b)
  • g[0][2]: substring "abc" → check outer chars: a≠c (1 change), no inner substring → total: 1
  • g[0][3]: substring "abca" → check outer chars: a=a (0 changes), inner is "bc" which needs g[1][2]=1 → total: 1
  • g[0][4]: substring "abcac" → check outer chars: a≠c (1 change), inner is "bca" which needs g[1][3]=1 → total: 2
  • g[1][2]: substring "bc" → needs 1 change (b≠c)
  • g[1][3]: substring "bca" → check outer chars: b≠a (1 change), no inner substring → total: 1
  • g[1][4]: substring "bcac" → check outer chars: b≠c (1 change), inner is "ca" which needs g[2][3]=1 → total: 2
  • g[2][3]: substring "ca" → needs 1 change (c≠a)
  • g[2][4]: substring "cac" → check outer chars: c=c (0 changes), no inner substring → total: 0
  • g[3][4]: substring "ac" → needs 1 change (a≠c)

Complete g matrix:

    0  1  2  3  4
0 [ 0  1  1  1  2 ]
1 [ -  0  1  1  2 ]
2 [ -  -  0  1  0 ]
3 [ -  -  -  0  1 ]
4 [ -  -  -  -  0 ]

Step 2: Dynamic Programming - Find optimal partitioning

We build f[i][j] = minimum changes to partition first i characters into j palindromes.

For i=1, j=1: First 1 character ("a") as 1 palindrome → f[1][1] = g[0][0] = 0

For i=2, j=1: First 2 characters ("ab") as 1 palindrome → f[2][1] = g[0][1] = 1

For i=2, j=2: First 2 characters ("ab") as 2 palindromes

  • Try splitting at position 1: "a" | "b"
    • Cost = f[1][1] + g[1][1] = 0 + 0 = 0
  • f[2][2] = 0

For i=3, j=1: First 3 characters ("abc") as 1 palindrome → f[3][1] = g[0][2] = 1

For i=3, j=2: First 3 characters ("abc") as 2 palindromes

  • Try splitting at position 1: "a" | "bc"
    • Cost = f[1][1] + g[1][2] = 0 + 1 = 1
  • Try splitting at position 2: "ab" | "c"
    • Cost = f[2][1] + g[2][2] = 1 + 0 = 1
  • f[3][2] = min(1, 1) = 1

For i=4, j=2: First 4 characters ("abca") as 2 palindromes

  • Try splitting at position 1: "a" | "bca"
    • Cost = f[1][1] + g[1][3] = 0 + 1 = 1
  • Try splitting at position 2: "ab" | "ca"
    • Cost = f[2][1] + g[2][3] = 1 + 1 = 2
  • Try splitting at position 3: "abc" | "a"
    • Cost = f[3][1] + g[3][3] = 1 + 0 = 1
  • f[4][2] = min(1, 2, 1) = 1

For i=5, j=2: All 5 characters ("abcac") as 2 palindromes

  • Try splitting at position 1: "a" | "bcac"
    • Cost = f[1][1] + g[1][4] = 0 + 2 = 2
  • Try splitting at position 2: "ab" | "cac"
    • Cost = f[2][1] + g[2][4] = 1 + 0 = 1
  • Try splitting at position 3: "abc" | "ac"
    • Cost = f[3][1] + g[3][4] = 1 + 1 = 2
  • Try splitting at position 4: "abca" | "c"
    • Cost = f[4][1] + g[4][4] = 1 + 0 = 1
  • f[5][2] = min(2, 1, 2, 1) = 1

Final Answer: f[5][2] = 1

The optimal solution requires 1 character change. One optimal partition is "ab" | "cac" where we change "ab" to "aa" (or "bb"), requiring 1 change total.

Solution Implementation

1class Solution:
2    def palindromePartition(self, s: str, k: int) -> int:
3        n = len(s)
4      
5        # cost[i][j] stores the minimum changes needed to make substring s[i:j+1] a palindrome
6        cost = [[0] * n for _ in range(n)]
7      
8        # Build the cost table for all substrings
9        # Iterate from right to left for start index
10        for start in range(n - 1, -1, -1):
11            # Iterate from left to right for end index (after start)
12            for end in range(start + 1, n):
13                # Check if characters at both ends match
14                cost[start][end] = int(s[start] != s[end])
15              
16                # If substring has more than 2 characters, add cost of inner substring
17                if start + 1 < end:
18                    cost[start][end] += cost[start + 1][end - 1]
19      
20        # dp[i][j] stores minimum changes to partition first i characters into j palindromes
21        dp = [[0] * (k + 1) for _ in range(n + 1)]
22      
23        # Fill the DP table
24        for length in range(1, n + 1):
25            # Can't partition into more groups than characters available
26            for groups in range(1, min(length, k) + 1):
27                if groups == 1:
28                    # Single partition: cost to make entire substring a palindrome
29                    dp[length][groups] = cost[0][length - 1]
30                else:
31                    # Multiple partitions: try all possible last partition points
32                    dp[length][groups] = float('inf')
33                  
34                    # Try splitting at position split_point
35                    # First split_point characters into (groups-1) partitions
36                    # Remaining characters form the last partition
37                    for split_point in range(groups - 1, length):
38                        dp[length][groups] = min(
39                            dp[length][groups], 
40                            dp[split_point][groups - 1] + cost[split_point][length - 1]
41                        )
42      
43        return dp[n][k]
44
1class Solution {
2    public int palindromePartition(String s, int k) {
3        int n = s.length();
4      
5        // cost[i][j] represents the minimum number of character changes needed
6        // to make substring s[i...j] a palindrome
7        int[][] cost = new int[n][n];
8      
9        // Precompute the cost to convert each substring into a palindrome
10        // Iterate from bottom-right to top-left to ensure subproblems are solved first
11        for (int i = n - 1; i >= 0; i--) {
12            for (int j = i; j < n; j++) {
13                // If characters at both ends don't match, we need 1 change
14                cost[i][j] = (s.charAt(i) != s.charAt(j)) ? 1 : 0;
15              
16                // For substrings longer than 2 characters, add the cost of inner substring
17                if (i + 1 < j) {
18                    cost[i][j] += cost[i + 1][j - 1];
19                }
20            }
21        }
22      
23        // dp[i][j] represents the minimum changes needed to partition
24        // the first i characters of string s into j palindromes
25        int[][] dp = new int[n + 1][k + 1];
26      
27        // Fill the DP table
28        for (int length = 1; length <= n; length++) {
29            for (int partitions = 1; partitions <= Math.min(length, k); partitions++) {
30                if (partitions == 1) {
31                    // If only 1 partition, we need to make entire substring [0...length-1] a palindrome
32                    dp[length][partitions] = cost[0][length - 1];
33                } else {
34                    // Initialize with a large value
35                    dp[length][partitions] = Integer.MAX_VALUE / 2;
36                  
37                    // Try all possible positions for the last partition
38                    // Last partition starts at position splitPoint
39                    for (int splitPoint = partitions - 1; splitPoint < length; splitPoint++) {
40                        // Minimum of current value and:
41                        // cost of first splitPoint characters in (partitions-1) parts +
42                        // cost of making substring [splitPoint...length-1] a palindrome
43                        dp[length][partitions] = Math.min(
44                            dp[length][partitions], 
45                            dp[splitPoint][partitions - 1] + cost[splitPoint][length - 1]
46                        );
47                    }
48                }
49            }
50        }
51      
52        // Return minimum changes for n characters partitioned into k palindromes
53        return dp[n][k];
54    }
55}
56
1class Solution {
2public:
3    int palindromePartition(string s, int k) {
4        int n = s.size();
5      
6        // cost[i][j] = minimum changes needed to make substring s[i..j] a palindrome
7        vector<vector<int>> cost(n, vector<int>(n, 0));
8      
9        // Build the cost matrix using dynamic programming
10        // Iterate from bottom-right to top-left to ensure subproblems are solved first
11        for (int left = n - 1; left >= 0; --left) {
12            for (int right = left; right < n; ++right) {
13                // Check if characters at boundaries match
14                cost[left][right] = (s[left] != s[right]) ? 1 : 0;
15              
16                // For substrings longer than 2 characters, add the cost of inner substring
17                if (left + 1 < right) {
18                    cost[left][right] += cost[left + 1][right - 1];
19                }
20            }
21        }
22      
23        // dp[i][j] = minimum changes to partition first i characters into j palindromes
24        vector<vector<int>> dp(n + 1, vector<int>(k + 1, 0));
25      
26        // Fill the DP table
27        for (int length = 1; length <= n; ++length) {
28            for (int partitions = 1; partitions <= min(length, k); ++partitions) {
29                if (partitions == 1) {
30                    // Base case: one partition means making entire substring a palindrome
31                    dp[length][partitions] = cost[0][length - 1];
32                } else {
33                    // Initialize with a large value
34                    dp[length][partitions] = 10000;
35                  
36                    // Try all possible positions for the last partition
37                    // Last partition starts at position splitPoint
38                    for (int splitPoint = partitions - 1; splitPoint < length; ++splitPoint) {
39                        // Cost = cost of first (partitions-1) partitions + cost of last partition
40                        dp[length][partitions] = min(dp[length][partitions], 
41                                                    dp[splitPoint][partitions - 1] + cost[splitPoint][length - 1]);
42                    }
43                }
44            }
45        }
46      
47        // Return the minimum changes to partition entire string into k palindromes
48        return dp[n][k];
49    }
50};
51
1/**
2 * Partitions a string into k palindromes with minimum character changes
3 * @param s - The input string to partition
4 * @param k - The number of palindromes to partition into
5 * @returns The minimum number of character changes needed
6 */
7function palindromePartition(s: string, k: number): number {
8    const stringLength: number = s.length;
9  
10    // costMatrix[i][j] represents the cost to convert substring s[i...j] into a palindrome
11    const costMatrix: number[][] = Array.from(
12        { length: stringLength }, 
13        () => Array(stringLength).fill(0)
14    );
15  
16    // Calculate cost to convert each substring into a palindrome
17    // Iterate from right to left for starting position
18    for (let startIndex = stringLength - 1; startIndex >= 0; startIndex--) {
19        // Iterate from left to right for ending position
20        for (let endIndex = startIndex + 1; endIndex < stringLength; endIndex++) {
21            // Check if characters at both ends match
22            costMatrix[startIndex][endIndex] = s[startIndex] !== s[endIndex] ? 1 : 0;
23          
24            // Add the cost of inner substring if it exists
25            if (startIndex + 1 < endIndex) {
26                costMatrix[startIndex][endIndex] += costMatrix[startIndex + 1][endIndex - 1];
27            }
28        }
29    }
30  
31    // dp[i][j] represents minimum cost to partition first i characters into j palindromes
32    const dp: number[][] = Array.from(
33        { length: stringLength + 1 }, 
34        () => Array(k + 1).fill(0)
35    );
36  
37    // Fill the dp table
38    for (let charCount = 1; charCount <= stringLength; charCount++) {
39        for (let partitionCount = 1; partitionCount <= Math.min(charCount, k); partitionCount++) {
40            if (partitionCount === 1) {
41                // Base case: single partition, convert entire substring to palindrome
42                dp[charCount][partitionCount] = costMatrix[0][charCount - 1];
43            } else {
44                // Initialize with a large value
45                dp[charCount][partitionCount] = 1 << 30;
46              
47                // Try all possible positions for the last partition
48                for (let lastPartitionStart = partitionCount - 1; lastPartitionStart < charCount; lastPartitionStart++) {
49                    // Calculate minimum cost by combining previous partitions with current one
50                    dp[charCount][partitionCount] = Math.min(
51                        dp[charCount][partitionCount], 
52                        dp[lastPartitionStart][partitionCount - 1] + costMatrix[lastPartitionStart][charCount - 1]
53                    );
54                }
55            }
56        }
57    }
58  
59    // Return the minimum cost to partition entire string into k palindromes
60    return dp[stringLength][k];
61}
62

Time and Space Complexity

Time Complexity: O(n^2 × k)

The time complexity is determined by three main parts:

  1. Building the cost matrix g: The nested loops iterate from i = n-1 to 0 and j = i+1 to n-1, resulting in O(n^2) operations to calculate the cost of converting each substring s[i:j+1] into a palindrome.

  2. Dynamic programming calculation: The main DP section has three nested loops:

    • The outer loop iterates i from 1 to n: O(n)
    • The middle loop iterates j from 1 to min(i, k): O(k) in the worst case
    • The inner loop (when j > 1) iterates h from j-1 to i-1: O(n) in the worst case

    This gives us O(n × k × n) = O(n^2 × k) for the DP computation.

Since the DP computation dominates, the overall time complexity is O(n^2 × k).

Space Complexity: O(n × (n + k))

The space complexity comes from:

  1. Matrix g: A 2D array of size n × n, requiring O(n^2) space to store the cost of converting each substring into a palindrome.

  2. DP table f: A 2D array of size (n+1) × (k+1), requiring O(n × k) space to store the minimum cost for partitioning the first i characters into j palindromes.

The total space complexity is O(n^2 + n × k) = O(n × (n + k)) since we can factor out n from both terms.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Cost Calculation for Single Character Substrings

A common mistake is not handling single-character substrings correctly in the cost preprocessing table. Single characters are always palindromes and require 0 changes, but the cost calculation logic might incorrectly compute a non-zero value.

Problematic Code:

# This might incorrectly calculate cost for single characters
for start in range(n - 1, -1, -1):
    for end in range(start + 1, n):  # Missing start == end case
        cost[start][end] = int(s[start] != s[end])
        if start + 1 < end:
            cost[start][end] += cost[start + 1][end - 1]

Solution:

# Initialize diagonal (single characters) explicitly
for i in range(n):
    cost[i][i] = 0  # Single character is always a palindrome

for start in range(n - 1, -1, -1):
    for end in range(start + 1, n):
        cost[start][end] = int(s[start] != s[end])
        if start + 1 < end:
            cost[start][end] += cost[start + 1][end - 1]

2. Off-by-One Errors in Index Mapping

The DP table uses 1-based indexing for string length while the cost table uses 0-based indexing. This mismatch often leads to accessing wrong indices.

Problematic Code:

# Incorrect index mapping
dp[length][groups] = cost[0][length]  # Should be length - 1
# Or
dp[split_point][groups - 1] + cost[split_point - 1][length - 1]  # Wrong!

Solution:

# Correct index mapping
dp[length][groups] = cost[0][length - 1]  # length characters means index 0 to length-1
# And
dp[split_point][groups - 1] + cost[split_point][length - 1]  # split_point is already 0-based

3. Insufficient Initialization of DP Table

Not properly initializing the DP table with infinity for impossible states can lead to incorrect minimum calculations.

Problematic Code:

dp = [[0] * (k + 1) for _ in range(n + 1)]  # All initialized to 0
# Later comparison might pick 0 as minimum incorrectly

Solution:

dp = [[float('inf')] * (k + 1) for _ in range(n + 1)]
dp[0][0] = 0  # Base case: 0 characters into 0 groups needs 0 changes

for length in range(1, n + 1):
    for groups in range(1, min(length, k) + 1):
        if groups == 1:
            dp[length][groups] = cost[0][length - 1]
        else:
            for split_point in range(groups - 1, length):
                dp[length][groups] = min(
                    dp[length][groups], 
                    dp[split_point][groups - 1] + cost[split_point][length - 1]
                )

4. Incorrect Loop Bounds for Split Points

The split point iteration must ensure that there are enough characters left for both the previous partitions and the current partition.

Problematic Code:

# Might not leave enough characters for previous groups
for split_point in range(1, length):  # Wrong starting point

Solution:

# Ensure at least (groups - 1) characters for previous (groups - 1) partitions
for split_point in range(groups - 1, length):
    # split_point characters go to first (groups - 1) partitions
    # Remaining (length - split_point) characters form the last partition
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More