LeetCode 1278 - Palindrome Partitioning III
The problem gives us a string s and an integer k. We are allowed to modify characters in the string, and our goal is to
Difficulty: 🔴 Hard
Topics: String, Dynamic Programming
Solution
LeetCode 1278 - Palindrome Partitioning III
Problem Understanding
The problem gives us a string s and an integer k. We are allowed to modify characters in the string, and our goal is to partition the string into exactly k non-empty substrings such that every substring becomes a palindrome after the modifications.
A palindrome is a string that reads the same forward and backward. For example, "aba" and "aa" are palindromes, while "abc" is not.
The important detail is that we are not required to preserve the original characters. We may change any character to any lowercase English letter. The objective is to minimize the total number of character changes across all substrings.
For example, if s = "abc" and k = 2, we can split the string into "ab" and "c". The substring "ab" is not a palindrome, but changing either 'a' to 'b' or 'b' to 'a' makes it one. That requires exactly one modification, so the answer is 1.
The constraints are small enough to strongly suggest a dynamic programming solution:
1 <= k <= s.length <= 100- The string contains only lowercase English letters.
Since the maximum string length is only 100, algorithms with cubic complexity are acceptable. Exponential brute force solutions, however, would still be far too slow.
Several edge cases are important:
- When
k == s.length, every character can form its own substring, so the answer is always0. - When
k == 1, the entire string must become one palindrome. - Some substrings may already be palindromes and require no changes.
- The partition positions significantly affect the total cost, so greedy strategies can fail.
Approaches
Brute Force Approach
A brute force solution would try every possible way to divide the string into k substrings. For each partitioning, we would compute how many character modifications are needed to make every substring a palindrome.
To compute the palindrome cost of a substring, we compare characters from both ends toward the center. Every mismatched pair contributes one required modification.
The main issue is the number of possible partitions. Choosing k - 1 split positions among n - 1 possible locations leads to a combinatorial explosion. Even though checking each partition is relatively cheap, the total number of possibilities becomes impractical.
This approach is correct because it exhaustively evaluates all valid partitions, but it is too slow for n = 100.
Optimal Dynamic Programming Approach
The key observation is that many substring palindrome costs are reused repeatedly across different partitions.
We can separate the problem into two smaller subproblems:
- Precompute the cost to convert every substring into a palindrome.
- Use dynamic programming to find the minimum partition cost.
For the first step, define:
cost[i][j]= minimum changes needed to makes[i:j+1]a palindrome.
This can be computed efficiently using interval DP.
For the second step, define:
dp[i][p]= minimum cost to partition the substring starting at indexiinto exactlyppalindromic parts.
We try every possible ending position for the first partition, then recursively solve the remaining suffix.
Because overlapping subproblems are heavily reused, dynamic programming dramatically reduces the complexity.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | O(k) | Tries every partition configuration |
| Optimal Dynamic Programming | O(n³ + kn²) | O(n² + kn) | Precomputes palindrome costs and uses DP |
Algorithm Walkthrough
Step 1: Precompute palindrome conversion costs
Create a 2D array cost where:
cost[i][j]represents the minimum changes needed to convert substrings[i:j+1]into a palindrome.
To compute this:
- If
i >= j, the substring has length0or1, so the cost is0. - Otherwise:
- Compare
s[i]ands[j] - If they differ, one modification is needed
- Add the cost of the inner substring
The recurrence becomes:
$cost[i][j] = cost[i+1][j-1] + [s_i \neq s_j]$
We process substrings by increasing length so smaller intervals are already computed.
Step 2: Define the DP state
Create a DP table:
dp[i][p]= minimum cost to partition substrings[i:]into exactlyppalindromic substrings.
The final answer will be:
$dp[0][k]$
Step 3: Transition between states
For every state (i, p):
- Try every possible endpoint
jfor the first partition. - The first substring is
s[i:j+1]. - Its palindrome conversion cost is
cost[i][j]. - The remaining suffix begins at
j + 1and must be partitioned intop - 1parts.
Transition:
$dp[i][p] = \min_{j \ge i}(cost[i][j] + dp[j+1][p-1])$
Step 4: Base cases
If only one partition remains:
dp[i][1] = cost[i][n-1]
This means the entire remaining suffix must become one palindrome.
Step 5: Return the answer
The answer is dp[0][k].
Why it works
The algorithm works because every valid partition can be decomposed into:
- A first palindromic substring.
- An optimal partitioning of the remaining suffix.
The DP recurrence checks all possible first cuts and chooses the minimum total cost. Since every subproblem is solved optimally and reused consistently, the final result is globally optimal.
Python Solution
class Solution:
def palindromePartition(self, s: str, k: int) -> int:
n = len(s)
# cost[i][j] = minimum changes needed
# to make s[i:j+1] a palindrome
cost = [[0] * n for _ in range(n)]
# Build costs by substring length
for length in range(2, n + 1):
for left in range(n - length + 1):
right = left + length - 1
if left + 1 <= right - 1:
cost[left][right] = cost[left + 1][right - 1]
if s[left] != s[right]:
cost[left][right] += 1
INF = float('inf')
# dp[i][p] = minimum cost to partition
# s[i:] into p palindromic substrings
dp = [[INF] * (k + 1) for _ in range(n + 1)]
# Base case
dp[n][0] = 0
for i in range(n - 1, -1, -1):
dp[i][1] = cost[i][n - 1]
# Fill DP table
for parts in range(2, k + 1):
for start in range(n):
# Ensure enough characters remain
for end in range(start, n - parts + 1):
dp[start][parts] = min(
dp[start][parts],
cost[start][end] + dp[end + 1][parts - 1]
)
return dp[0][k]
The implementation starts by precomputing palindrome conversion costs for all substrings. This avoids recalculating mismatch counts repeatedly during partitioning.
The nested loops over substring length ensure that smaller intervals are processed before larger ones. This is necessary because cost[left][right] depends on cost[left + 1][right - 1].
After preprocessing, the algorithm initializes the dynamic programming table. Each entry represents the minimum cost for partitioning a suffix into a specific number of palindromic parts.
The transition iterates over every possible ending index for the first partition. For each candidate split, the algorithm combines:
- The cost of converting the current substring into a palindrome.
- The optimal cost for partitioning the remaining suffix.
Finally, dp[0][k] contains the minimum total number of modifications.
Go Solution
func palindromePartition(s string, k int) int {
n := len(s)
// cost[i][j] = minimum changes needed
// to make s[i:j+1] a palindrome
cost := make([][]int, n)
for i := range cost {
cost[i] = make([]int, n)
}
// Build palindrome costs
for length := 2; length <= n; length++ {
for left := 0; left+length-1 < n; left++ {
right := left + length - 1
if left+1 <= right-1 {
cost[left][right] = cost[left+1][right-1]
}
if s[left] != s[right] {
cost[left][right]++
}
}
}
const INF = int(1e9)
// dp[i][p] = minimum cost to partition
// s[i:] into p palindromic substrings
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, k+1)
for j := range dp[i] {
dp[i][j] = INF
}
}
dp[n][0] = 0
for i := n - 1; i >= 0; i-- {
dp[i][1] = cost[i][n-1]
}
for parts := 2; parts <= k; parts++ {
for start := 0; start < n; start++ {
for end := start; end <= n-parts; end++ {
candidate := cost[start][end] + dp[end+1][parts-1]
if candidate < dp[start][parts] {
dp[start][parts] = candidate
}
}
}
}
return dp[0][k]
}
The Go implementation follows the same logic as the Python version, but uses slices instead of lists.
Since Go does not provide an infinity constant for integers, a large sentinel value such as 1e9 is used.
The DP arrays are explicitly initialized because Go does not support Python-style list comprehensions. Otherwise, the algorithm structure and recurrence relations remain identical.
Worked Examples
Example 1
Input:
s = "abc"
k = 2
Step 1: Compute palindrome costs
| Substring | Cost |
|---|---|
| "a" | 0 |
| "b" | 0 |
| "c" | 0 |
| "ab" | 1 |
| "bc" | 1 |
| "abc" | 1 |
For "abc":
- Compare
'a'and'c' - They differ, so one change is needed
Step 2: Initialize DP
Base case:
| State | Value |
|---|---|
| dp[2][1] | 0 |
| dp[1][1] | 1 |
| dp[0][1] | 1 |
Step 3: Compute dp[0][2]
Try all first partitions:
| First Partition | Cost | Remaining | Remaining Cost | Total |
|---|---|---|---|---|
| "a" | 0 | "bc" | 1 | 1 |
| "ab" | 1 | "c" | 0 | 1 |
Minimum = 1
Answer:
1
Example 2
Input:
s = "aabbc"
k = 3
Possible partition:
"aa" | "bb" | "c"
Palindrome costs:
| Substring | Cost |
|---|---|
| "aa" | 0 |
| "bb" | 0 |
| "c" | 0 |
Total cost:
0
The DP eventually discovers this optimal partition.
Example 3
Input:
s = "leetcode"
k = 8
Each character becomes its own palindrome:
"l" | "e" | "e" | "t" | "c" | "o" | "d" | "e"
Every single-character substring is already a palindrome.
Answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n³ + kn²) | O(n²) substrings for preprocessing, plus partition DP transitions |
| Space | O(n² + kn) | Stores palindrome costs and DP states |
The palindrome preprocessing requires examining all substring intervals, which takes O(n²) states. Each state performs constant work.
The partition DP has O(nk) states, and each state may try up to O(n) partition endpoints, leading to O(kn²) time overall.
Since n <= 100, this complexity easily fits within limits.
Test Cases
sol = Solution()
assert sol.palindromePartition("abc", 2) == 1
# Basic example with one required change
assert sol.palindromePartition("aabbc", 3) == 0
# Already partitionable into palindromes
assert sol.palindromePartition("leetcode", 8) == 0
# Every character forms its own palindrome
assert sol.palindromePartition("a", 1) == 0
# Single character string
assert sol.palindromePartition("aa", 1) == 0
# Already a palindrome
assert sol.palindromePartition("ab", 1) == 1
# One modification needed
assert sol.palindromePartition("abcd", 2) == 1
# Optimal split reduces total cost
assert sol.palindromePartition("abcba", 1) == 0
# Entire string already palindrome
assert sol.palindromePartition("abcdef", 3) == 2
# Multiple partitions with modifications
assert sol.palindromePartition("aaaaaa", 3) == 0
# Repeated characters
assert sol.palindromePartition("racecar", 2) == 1
# Splitting a palindrome into two parts
assert sol.palindromePartition("xyz", 1) == 1
# Entire string converted into one palindrome
| Test | Why |
|---|---|
"abc", 2 |
Verifies standard partitioning |
"aabbc", 3 |
Confirms zero-cost partitions |
"leetcode", 8 |
Tests maximum partition count |
"a", 1 |
Smallest valid input |
"aa", 1 |
Already palindrome |
"ab", 1 |
Minimal modification case |
"abcd", 2 |
Ensures optimal partition selection |
"abcba", 1 |
Full-string palindrome |
"abcdef", 3 |
Multiple modification paths |
"aaaaaa", 3 |
Repeated characters |
"racecar", 2 |
Nontrivial split handling |
"xyz", 1 |
Odd-length palindrome conversion |
Edge Cases
Case 1: k == s.length
When every character becomes its own substring, each substring is automatically a palindrome. A buggy implementation might still attempt unnecessary modifications or invalid transitions.
This implementation handles the case naturally because the DP allows partitions of length 1, whose palindrome cost is always 0.
Case 2: Entire string must remain one partition
When k == 1, the algorithm must compute the minimum modifications required for the entire string.
The preprocessing table directly supports this through:
dp[i][1] = cost[i][n-1]
This avoids any special-case logic during recursion or transitions.
Case 3: Uneven partition distributions
Some strings require highly unbalanced partitions for the optimal answer. For example, one partition may contain most of the characters while others contain single letters.
A greedy approach could fail here by choosing locally optimal palindrome substrings too early. The DP solution avoids this issue because it evaluates every possible partition endpoint before choosing the minimum total cost.