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:
- Change characters: First, you can change some characters in
s
to other lowercase English letters. - 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.
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 intoj-1
parts - Plus the cost of making characters from position
h
toi-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 positionsi
andj
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 substrings[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 ton
) and number of partitionsj
(1 tomin(i, k)
):- Base case: If
j = 1
, we need to make the entire substrings[0...i-1]
a palindrome, sof[i][j] = g[0][i-1]
- General case: For
j > 1
, we try all possible positionsh
for the(j-1)
-th partition:- The cost is
f[h][j-1]
(optimal cost for firsth
characters withj-1
partitions) - Plus
g[h][i-1]
(cost to make substring from positionh
toi-1
a palindrome) - We take the minimum across all valid
h
values
- The cost is
- Base case: If
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 EvaluatorExample 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: 1g[0][3]
: substring "abca" → check outer chars: a=a (0 changes), inner is "bc" which needs g[1][2]=1 → total: 1g[0][4]
: substring "abcac" → check outer chars: a≠c (1 change), inner is "bca" which needs g[1][3]=1 → total: 2g[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: 1g[1][4]
: substring "bcac" → check outer chars: b≠c (1 change), inner is "ca" which needs g[2][3]=1 → total: 2g[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: 0g[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
- Cost =
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
- Cost =
- Try splitting at position 2: "ab" | "c"
- Cost =
f[2][1] + g[2][2] = 1 + 0 = 1
- Cost =
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
- Cost =
- Try splitting at position 2: "ab" | "ca"
- Cost =
f[2][1] + g[2][3] = 1 + 1 = 2
- Cost =
- Try splitting at position 3: "abc" | "a"
- Cost =
f[3][1] + g[3][3] = 1 + 0 = 1
- Cost =
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
- Cost =
- Try splitting at position 2: "ab" | "cac"
- Cost =
f[2][1] + g[2][4] = 1 + 0 = 1
- Cost =
- Try splitting at position 3: "abc" | "ac"
- Cost =
f[3][1] + g[3][4] = 1 + 1 = 2
- Cost =
- Try splitting at position 4: "abca" | "c"
- Cost =
f[4][1] + g[4][4] = 1 + 0 = 1
- Cost =
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:
-
Building the cost matrix
g
: The nested loops iterate fromi = n-1
to0
andj = i+1
ton-1
, resulting inO(n^2)
operations to calculate the cost of converting each substrings[i:j+1]
into a palindrome. -
Dynamic programming calculation: The main DP section has three nested loops:
- The outer loop iterates
i
from1
ton
:O(n)
- The middle loop iterates
j
from1
tomin(i, k)
:O(k)
in the worst case - The inner loop (when
j > 1
) iteratesh
fromj-1
toi-1
:O(n)
in the worst case
This gives us
O(n × k × n) = O(n^2 × k)
for the DP computation. - The outer loop iterates
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:
-
Matrix
g
: A 2D array of sizen × n
, requiringO(n^2)
space to store the cost of converting each substring into a palindrome. -
DP table
f
: A 2D array of size(n+1) × (k+1)
, requiringO(n × k)
space to store the minimum cost for partitioning the firsti
characters intoj
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
Which of the following uses divide and conquer strategy?
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!