Palindrome Partitioning III
HardPrompt
You are given a string s containing lowercase letters and an integer k. You need to :
- First, change some characters of
sto other lowercase English letters. - Then divide
sintoknon-empty disjoint substrings such that each substring is a palindrome.
Return the minimal number of characters that you need to change to divide the string.
Example 1:
Input: s = "abc", k = 2
Output: 1
Explanation: You can split the string into "ab" and "c", and change 1 character in "ab" to make it palindrome.Example 2:
Input: s = "aabbc", k = 3
Output: 0
Explanation: You can split the string into "aa", "bb" and "c", all of them are palindrome.Example 3:
Input: s = "leetcode", k = 8
Output: 0
Constraints:
1 <= k <= s.length <= 100.sonly contains lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach explores all possible ways to partition the string s into k non-empty substrings. For each partitioning scheme, it calculates the total number of character changes required to make every substring a palindrome and finds the minimum among all schemes. This is done using a recursive helper function that does not use memoization.
Algorithm
- Create a helper function
calculateCost(s, i, j)that computes the minimum changes to make substrings[i...j]a palindrome. It iterates from both ends inwards, counting mismatches. - Create a recursive function
solve(s, k, index). - Base Case 1: If
k == 1, the only option is to make the rest of the strings[index...n-1]a palindrome. ReturncalculateCost(s, index, n-1). - Base Case 2: If
index >= s.length()ork <= 0, it's an invalid state, return a large value to indicate impossibility. - Initialize
minChangesto a very large value. - Iterate
jfromindextos.length() - k. Thisjrepresents the end index of the first of thekpartitions. - For each
j, calculatecurrentCost = calculateCost(s, index, j). - Recursively call
remainingCost = solve(s, k - 1, j + 1)to find the cost for the rest of the string. - Update
minChanges = min(minChanges, currentCost + remainingCost). - Return
minChanges. - The initial call to start the process is
solve(s, k, 0).
Walkthrough
We define a recursive function, say solve(index, partitions_left). This function calculates the minimum cost to partition the suffix of the string starting at index into partitions_left palindromic substrings.
The base cases for the recursion are:
- If
partitions_leftis 1, we must use the entire remaining strings[index...n-1]as the last partition. The cost is the number of changes to make this substring a palindrome. - If we run out of characters (
index == n) but still need to make partitions (partitions_left > 0), or vice-versa, it's an invalid state. We return a very large value (infinity) to signify this.
In the recursive step, we try all possible split points for the current partition. We can form the first substring of the current suffix s[index...n-1] as s[index...j], where j varies from index up to a point that leaves enough characters for the remaining partitions.
For each choice of j, we calculate the cost to make s[index...j] a palindrome. Then, we recursively call the function for the rest of the string s[j+1...n-1] with partitions_left - 1.
The cost to make a substring a palindrome is calculated by comparing characters from both ends and counting the mismatches.
The function returns the minimum cost found among all possible split points j. This approach is highly inefficient because it recomputes the results for the same subproblems (same index and partitions_left) multiple times, leading to an exponential time complexity.
Complexity
Time
Exponential. The number of ways to partition a string of length `n` into `k` parts is given by the stars and bars formula, `C(n-1, k-1)`. For each partition, we spend `O(n^2)` to calculate costs. The complexity is roughly `O(C(n-1, k-1) * n^2)`, which is too slow for the given constraints.
Space
O(n) or O(k) for the recursion stack depth, where n is the length of the string.
Trade-offs
Pros
Conceptually simple to understand as it directly models the problem statement.
Cons
Extremely inefficient due to recomputing the same subproblems.
Will result in a 'Time Limit Exceeded' (TLE) error for most test cases due to its exponential time complexity.
This approach improves upon the brute-force recursion by using memoization (a top-down dynamic programming technique) to store and reuse the results of subproblems. This avoids redundant computations and significantly reduces the time complexity. We also precompute the costs of making any substring a palindrome to speed up the main DP calculation.
Algorithm
- Precomputation: Create a 2D array
cost[n][n]. Iterate through all possible substringss[i...j]and calculate the minimum changes needed to make them a palindrome. Store this value incost[i][j]. This can be done inO(n^2). - Memoization: Create a 2D array
memo[n][k+1]initialized with a value indicating that the state has not been computed (e.g., -1). - Recursive Function with Memoization: Define a function
solve(i, k)that computes the minimum cost to partition the suffixs[i...n-1]intokparts. - Base Cases:
- If we have partitioned the whole string (
i == n), return 0 ifkis also 0, otherwise return a large value (infinity). - If there are not enough characters left for the remaining partitions (
n - i < k) or we need to partition into 0 parts before reaching the end (k == 0), return infinity. - If only one partition is left (
k == 1), we must use the rest of the string, so returncost[i][n-1].
- If we have partitioned the whole string (
- Memoization Check: If
memo[i][k]has been computed, return the stored value. - Recursive Step:
- Initialize
minChangesto a large value. - Iterate
jfromiton - k. Thisjis the end of the first partition. - Calculate
currentTotalChanges = cost[i][j] + solve(j + 1, k - 1). - Update
minChanges = min(minChanges, currentTotalChanges).
- Initialize
- Store and Return: Store the result in
memo[i][k]and return it. - The initial call is
solve(0, k).
Walkthrough
The core idea is to solve the problem recursively but store the results of subproblems to avoid re-computation. We define a function solve(i, k) which computes the minimum changes to partition the suffix s[i...n-1] into k palindromic substrings.
First, to avoid repeatedly calculating the cost of making a substring a palindrome, we precompute these values. A 2D array, cost[i][j], stores the minimum changes for s[i...j]. This can be done efficiently in O(n^2) time using a DP relation: cost[i][j] = cost[i+1][j-1] + (s[i] == s[j] ? 0 : 1).
Next, we set up the memoized recursion. The state is defined by (i, k), the starting index and the number of partitions remaining. We use a 2D array memo[i][k] to store the results.
In the solve(i, k) function:
- We handle base cases: if we need to partition an empty string, or if we don't have enough characters for the required partitions.
- We check if
memo[i][k]already contains a valid result. If so, we return it. - If not, we compute the result by trying all possible split points
jfor the first partitions[i...j]. For eachj, the total cost iscost[i][j]plus the result of the recursive call for the rest of the string,solve(j + 1, k - 1). - We take the minimum cost over all possible
j. - Finally, we store this minimum cost in
memo[i][k]before returning it.
This approach ensures that each of the O(n * k) subproblems is solved only once, making it efficient enough for the given constraints.
class Solution { private int[][] cost; private int[][] memo; private int n; private String s; public int palindromePartition(String s, int k) { this.n = s.length(); this.s = s; this.cost = new int[n][n]; this.memo = new int[n][k + 1]; // Precompute the cost to make any substring a palindrome for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { cost[i][j] = calculateCost(i, j); } } // Initialize memoization table with -1 for (int i = 0; i < n; i++) { for (int j = 0; j <= k; j++) { memo[i][j] = -1; } } return solve(0, k); } private int calculateCost(int i, int j) { int changes = 0; while (i < j) { if (s.charAt(i) != s.charAt(j)) { changes++; } i++; j--; } return changes; } private int solve(int i, int k) { // Base case: If we have partitioned the whole string if (i == n) { return k == 0 ? 0 : 101; // Return 0 if k is also 0, else a large value } // Base case: Not enough characters left for remaining partitions or no partitions left if (n - i < k || k == 0) { return 101; // A value larger than max possible cost (100) } // Base case: Only one partition left, must use the rest of the string if (k == 1) { return cost[i][n - 1]; } // Memoization check if (memo[i][k] != -1) { return memo[i][k]; } int minChanges = 101; // Iterate through all possible split points 'j' for the current partition for (int j = i; j <= n - k; j++) { int currentCost = cost[i][j]; int remainingCost = solve(j + 1, k - 1); minChanges = Math.min(minChanges, currentCost + remainingCost); } return memo[i][k] = minChanges; }}An alternative is to use a bottom-up DP approach, which is iteratively fills a DP table and is sometimes preferred for avoiding recursion overhead.
class Solution { public int palindromePartition(String s, int k) { int n = s.length(); int[][] cost = new int[n][n]; // Precompute cost to make s[i..j] a palindrome for (int len = 2; len <= n; len++) { for (int i = 0; i <= n - len; i++) { int j = i + len - 1; cost[i][j] = cost[i + 1][j - 1] + (s.charAt(i) == s.charAt(j) ? 0 : 1); } } // dp[i][p] = min cost to partition s[0...i-1] into p parts int[][] dp = new int[n + 1][k + 1]; for (int i = 0; i <= n; i++) { for (int p = 0; p <= k; p++) { dp[i][p] = 101; // Initialize with a large value } } dp[0][0] = 0; for (int p = 1; p <= k; p++) { for (int i = 1; i <= n; i++) { // Last partition is s[j...i-1] for (int j = 0; j < i; j++) { if (dp[j][p - 1] != 101) { dp[i][p] = Math.min(dp[i][p], dp[j][p - 1] + cost[j][i - 1]); } } } } return dp[n][k]; }}Complexity
Time
O(n^2) for precomputing costs + `O(n^2 * k)` for the DP/memoized recursion. The total time complexity is dominated by the DP part, so it's `O(n^2 * k)`. Given `n <= 100` and `k <= 100`, this is efficient enough.
Space
O(n^2) for the `cost` table and `O(n * k)` for the `memo` or `dp` table. The total space complexity is `O(n^2)` as `n >= k`.
Trade-offs
Pros
Guaranteed to find the optimal solution.
Efficient enough to pass within the time limits for the given constraints.
Cons
Requires more space compared to the brute-force approach due to the memoization and cost tables.
The logic is slightly more complex to implement than a simple recursive solution.
Solutions
Solution
class Solution {public int palindromePartition(String s, int k) { int n = s.length(); int[][] g = new int[n][n]; for (int i = n - 1; i >= 0; --i) { for (int j = i; j < n; ++j) { g[i][j] = s.charAt(i) != s.charAt(j) ? 1 : 0; if (i + 1 < j) { g[i][j] += g[i + 1][j - 1]; } } } int[][] f = new int[n + 1][k + 1]; for (int i = 1; i <= n; ++i) { for (int j = 1; j <= Math.min(i, k); ++j) { if (j == 1) { f[i][j] = g[0][i - 1]; } else { f[i][j] = 10000; for (int h = j - 1; h < i; ++h) { f[i][j] = Math.min(f[i][j], f[h][j - 1] + g[h][i - 1]); } } } } return f[n][k]; }}class Solution {public: int palindromePartition(string s, int k) { int n = s.size(); vector<vector<int>> g(n, vector<int>(n)); for (int i = n - 1; i >= 0; --i) { for (int j = i; j < n; ++j) { g[i][j] = s[i] != s[j] ? 1 : 0; if (i + 1 < j) g[i][j] += g[i + 1][j - 1]; } } vector<vector<int>> f(n + 1, vector<int>(k + 1)); for (int i = 1; i <= n; ++i) { for (int j = 1; j <= min(i, k); ++j) { if (j == 1) { f[i][j] = g[0][i - 1]; } else { f[i][j] = 10000; for (int h = j - 1; h < i; ++h) { f[i][j] = min(f[i][j], f[h][j - 1] + g[h][i - 1]); } } } } return f[n][k]; }};class Solution: def palindromePartition(self, s: str, k: int) -> int: n = len(s) 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] 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]) return f[n][k]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.