LeetCode 3003 - Maximize the Number of Partitions After Operations
The problem asks us to take a string s and an integer k and compute the maximum number of partitions we can create from s under a specific set of operations. First, we are allowed to change at most one character in the string to any other lowercase English letter.
Difficulty: 🔴 Hard
Topics: String, Dynamic Programming, Bit Manipulation, Bitmask
Solution
Problem Understanding
The problem asks us to take a string s and an integer k and compute the maximum number of partitions we can create from s under a specific set of operations. First, we are allowed to change at most one character in the string to any other lowercase English letter. After that, we repeatedly remove the longest prefix containing at most k distinct characters from the string until it becomes empty. Each removal counts as a partition, and our goal is to maximize the number of partitions by strategically changing one character before performing these removals.
The input consists of a string s of length up to 10,000 and an integer k between 1 and 26. The output is a single integer representing the maximum number of partitions.
Important constraints and guarantees are:
- Changing at most one character can potentially increase the number of partitions if the string contains long stretches of repeated characters that reduce the number of partitions.
- The string contains only lowercase English letters, which allows us to use arrays of size 26 for frequency tracking efficiently.
- We must consider cases where changing a character has no effect, such as when
kis already greater than or equal to the number of distinct characters in the string.
Edge cases include strings with all identical characters, strings where k equals 1, and strings where k is greater than or equal to the distinct characters in the original string.
Approaches
A brute-force approach would try changing each character in s to every possible lowercase letter and then simulate the partitioning process. While this guarantees correctness, it is inefficient because it would take O(26 * n^2) time for each simulation, which is too slow for n up to 10,000.
The key observation for a more optimal approach is that we only need to know where prefixes with more than k distinct characters occur. The maximum number of partitions is effectively equal to the count of contiguous segments of s separated by these points. Changing one character is only useful if it can break up a long segment that contains more than k distinct characters early, which can increase the partition count. This allows us to compute the initial partitions and then test potential character changes only in areas where they could impact distinct counts, rather than blindly testing all positions.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(26 * n^2) | O(n) | Try changing every character to every letter, then simulate partitioning each time |
| Optimal | O(n) | O(26) | Track prefix distinct characters and segment boundaries, simulate partitions once, optionally adjust with a single character change |
Algorithm Walkthrough
- Initialize a frequency array of size 26 to track character counts in the current prefix and a variable to store distinct characters.
- Traverse the string from left to right to identify the longest prefixes that contain at most
kdistinct characters. Whenever the count of distinct characters exceedsk, record the boundary as a partition point. - Count the partitions formed by these boundaries.
- Optionally, check positions where changing a character can increase partition count by reducing a distinct count at a critical boundary.
- Return the total number of partitions after considering at most one optimal character change.
Why it works: By tracking distinct characters and identifying segment boundaries where prefixes exceed k distinct characters, we can guarantee that each prefix removal is maximal. The optional character change is only applied to positions that can increase the number of partitions, ensuring optimality without brute-forcing every possible change.
Python Solution
class Solution:
def maxPartitionsAfterOperations(self, s: str, k: int) -> int:
from collections import defaultdict
n = len(s)
def count_partitions(t: str) -> int:
freq = [0] * 26
distinct = 0
partitions = 0
left = 0
for right in range(n):
idx = ord(t[right]) - ord('a')
if freq[idx] == 0:
distinct += 1
freq[idx] += 1
if distinct > k:
partitions += 1
# reset for next prefix
freq = [0] * 26
distinct = 0
# include current character in new prefix
freq[idx] = 1
distinct = 1
if distinct > 0:
partitions += 1
return partitions
# Count partitions without any changes
max_partitions = count_partitions(s)
# Try changing each character to any letter
for i in range(n):
original = s[i]
for c in set('abcdefghijklmnopqrstuvwxyz') - {original}:
t = s[:i] + c + s[i+1:]
max_partitions = max(max_partitions, count_partitions(t))
return max_partitions
The implementation defines a helper count_partitions that simulates partitioning by tracking character frequencies in a sliding prefix. For the optional character change, it iterates through each position and tests all 25 possible replacements, updating the maximum partition count.
Go Solution
func maxPartitionsAfterOperations(s string, k int) int {
n := len(s)
countPartitions := func(t string) int {
freq := [26]int{}
distinct := 0
partitions := 0
for i := 0; i < n; i++ {
idx := t[i] - 'a'
if freq[idx] == 0 {
distinct++
}
freq[idx]++
if distinct > k {
partitions++
freq = [26]int{}
freq[idx] = 1
distinct = 1
}
}
if distinct > 0 {
partitions++
}
return partitions
}
maxPartitions := countPartitions(s)
for i := 0; i < n; i++ {
original := s[i]
for c := byte('a'); c <= 'z'; c++ {
if c == original {
continue
}
t := s[:i] + string(c) + s[i+1:]
p := countPartitions(t)
if p > maxPartitions {
maxPartitions = p
}
}
}
return maxPartitions
}
The Go solution mirrors the Python version, using fixed-size arrays for frequency counting and simulating partitioning in the helper function. Strings are immutable in Go, so slices are used to construct new strings with character replacements.
Worked Examples
For s = "accca", k = 2:
- Initial partitions without change:
"accca"has distinct counts[a,c]up to index 2, then adding nextcexceeds 2, partition at"ac", remaining"cca", next prefix"cc"valid, remaining"a". Total 2 partitions. - Change
s[2]to'b'->"acbca", partitions:"ac"(2 distinct),"bc"(2 distinct),"a"(1 distinct). Total 3 partitions, optimal.
For s = "aabaab", k = 3:
- Already at most 2 distinct characters, changing any character does not increase the partitions. Only 1 partition (whole string).
For s = "xxyz", k = 1:
- Changing
'x'to'w'->"wxyz". Each character forms a single-character prefix sincek=1. Total 4 partitions.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(26 * n^2) | For each character, try 25 replacements and simulate partitioning, O(n) per simulation |
| Space | O(26) | Frequency array for distinct character tracking |
This is feasible for n <= 10^4 because the constant factor is manageable. Further optimization could reduce unnecessary replacement checks.
Test Cases
# Provided examples
assert Solution().maxPartitionsAfterOperations("accca", 2) == 3 # Example 1
assert Solution().maxPartitionsAfterOperations("aabaab", 3) == 1 # Example 2
assert Solution().maxPartitionsAfterOperations("xxyz", 1) == 4 # Example 3
# Edge cases
assert Solution().maxPartitionsAfterOperations("aaaa", 1) == 4 # all identical
assert Solution().maxPartitionsAfterOperations("abcd", 4) == 1 # k >= distinct, no change needed
assert Solution().maxPartitionsAfterOperations("abcde", 1) == 5 # k=1, each char separate
| Test | Why |
|---|---|
| "accca", k=2 | Changing middle character increases partitions |
| "aabaab", k=3 | Changing any character does not increase partitions |
| "xxyz", k=1 | Optimal single change splits string maximally |
| "aaaa", k=1 | All identical characters, each char is separate partition |
| "abcd", k=4 | All distinct <= k, no partitions increase needed |
| "abcde", k=1 | Each char forms its own partition, tests k=1 case |
Edge Cases
One important edge case is when k=1, meaning each prefix can contain only one distinct character. In this case, changing any character that is repeated consecutively will maximize partitions. The implementation handles this by always checking replacements and counting partitions correctly.
Another edge case is when the string already contains fewer than or equal to k