Maximize the Number of Partitions After Operations solution and explanation
9 more videos on this problem
Maximize the Number of Partitions After Operations | Intuition From Scratch | Leetcode 3003 | MIKcodestorywithMIK · 1:02:18 · 11K views
Maximize Partitions After Operations | LeetCode 3003 | Bitmask DPSanyam IIT Guwahati · 23:29 · 3K views
3003. Maximize the Number of Partitions After Operations | Leetcode Daily - PythonLeetcode Daily · 8:04 · 1K views
Daily Leetcode #2026: Oct 17, 2025 - 3003. Maximize the Number of Partitions After OperationsProgramming Live with Larry · 1:01:40 · 874 views
Maximize the Number of Partitions After Operations | LeetCode 3003 - Pythonimpoldev · 18:48 · 759 views
3003. Maximize the Number of Partitions After Operations (Leetcode Hard)Programming Live with Larry · 51:14 · 665 views
Maximize the Number of Partitions After Operations | LeetCode 3003 | Java Code | Developer CoderDeveloper Coder · 56:36 · 419 views
3003. Maximize the Number of Partitions After Operations | Recursive DP | Leetcode Daily ChallengeEye on AI and DSA · 33:01 · 359 views
Maximize the Number of Partitions After Operations | Leetcode 3003 | Java English ExplanationKernel Queen 👑 · 24:07 · 269 views
LeetCode 3003 | Maximize the Number of Partitions After Operations | Explained | JavaCode Kage · 16:30 · 267 views
The problem
You are given a string s and an integer k.
First, you are allowed to change at most one index in s to another lowercase English letter.
After that, do the following partitioning operation until s is empty:
- Choose the longest prefix of
scontaining at mostkdistinct characters. - Delete the prefix from
sand increase the number of partitions by one. The remaining characters (if any) insmaintain their initial order.
Return an integer denoting the maximum number of resulting partitions after the operations by optimally choosing at most one index to change.
Example 1:
Input: s = "accca", k = 2
Output: 3
Explanation:
The optimal way is to change s[2] to something other than a and c, for example, b. then it becomes "acbca".
Then we perform the operations:
- The longest prefix containing at most 2 distinct characters is
"ac", we remove it andsbecomes"bca". - Now The longest prefix containing at most 2 distinct characters is
"bc", so we remove it andsbecomes"a". - Finally, we remove
"a"andsbecomes empty, so the procedure ends.
Doing the operations, the string is divided into 3 partitions, so the answer is 3.
Example 2:
Input: s = "aabaab", k = 3
Output: 1
Explanation:
Initially s contains 2 distinct characters, so whichever character we change, it will contain at most 3 distinct characters, so the longest prefix with at most 3 distinct characters would always be all of it, therefore the answer is 1.
Example 3:
Input: s = "xxyz", k = 1
Output: 4
Explanation:
The optimal way is to change s[0] or s[1] to something other than characters in s, for example, to change s[0] to w.
Then s becomes "wxyz", which consists of 4 distinct characters, so as k is 1, it will divide into 4 partitions.
Constraints:
1 <= s.length <= 104sconsists only of lowercase English letters.1 <= k <= 26
Approach overview
Problem Overview: You are given a string s and an integer k. The goal is to split the string into the maximum number of partitions such that each partition contains at most k distinct characters. Before partitioning, you are allowed to modify at most one character in the string. The challenge is deciding where to split and whether changing a character increases the number of valid partitions.
Approach 1: Recursive Backtracking (Exponential Time)
This approach explores all possible partition boundaries while optionally changing one character to any of the 26 lowercase letters. Maintain a running set or bitmask of characters in the current segment and start a new partition whenever the number of distinct characters exceeds k. During recursion, try both cases: keep the character as is or replace it (if the modification hasn't been used yet). This brute-force exploration guarantees the correct answer but quickly becomes expensive because every index can branch into up to 26 possibilities. Time complexity is roughly O(n * 26 * branching) in the worst case with O(n) recursion stack space. Useful mainly for understanding the decision space before optimizing.
Approach 2: Dynamic Programming + Bitmask (Optimal)
The optimized approach uses dynamic programming combined with a bitmask to track distinct characters in the current partition. Represent the character set using a 26-bit mask where each bit indicates whether a letter is present. The DP state is typically defined as dp(i, mask, changed): the maximum partitions starting at index i, given the current character mask and whether the one allowed modification has been used. When adding s[i] causes the number of set bits to exceed k, a new partition is started and the mask resets.
If the modification has not been used, try replacing s[i] with any other character and evaluate how that affects the mask and partition count. Memoization avoids recomputing states, and bit operations make distinct-character checks fast. The state transitions rely heavily on bitmask techniques like setting bits and counting them using popcount. This reduces the complexity to roughly O(n * 26 * states) with space O(n * states), which works within constraints.
Recommended for interviews: The Dynamic Programming + Bitmask solution is what interviewers expect. The brute-force recursion shows you understand the search space and the effect of the single allowed modification. The optimized DP demonstrates strong problem-solving skills by compressing character sets into a bitmask and caching overlapping subproblems.
Approach 1: Recursive Backtracking
This approach uses recursive backtracking to explore all possible solutions incrementally. At each step, it makes a choice and recursively explores further solutions. If a solution is invalid or not optimal, it backtracks and tries the next option.
def solve(step):
if step == 0:
# Base case
return
# Recursive case
solve(step - 1)
solve(10)public class Solution {
public static void solve(int step) {
if (step == 0) {
// Base case
return;
}
// Recursive case
solve(step - 1);
}
public static void main(String[] args) {
solve(10);
}
}#include <iostream>
void solve(int step) {
if (step == 0) {
// Base case
return;
}
// Recursive case
solve(step - 1);
}
int main() {
solve(10);
return 0;
}function solve(step) {
if (step === 0) {
// Base case
return;
}
// Recursive case
solve(step - 1);
}
solve(10);#include <stdio.h>
void solve(int step) {
if (step == 0) {
// Base case
return;
}
// Recursive case
solve(step - 1);
}
int main() {
solve(10);
return 0;
}using System;
class Solution {
static void Solve(int step) {
if (step == 0) {
// Base case
return;
}
// Recursive case
Solve(step - 1);
}
static void Main() {
Solve(10);
}
}Python solution uses the same core logic of recursion with a base case and a recursive step, showcasing backtracking implementation.
Time Complexity: O(2^n), due to recursive exploration of states.
Space Complexity: O(n), due to recursion depth.
Approach 2: Dynamic Programming
This approach uses dynamic programming to store intermediate results of computations, avoiding recalculating them and thus optimizing the time complexity. It typically involves building a table of results initially filled by base cases and then filled iteratively or recursively with subproblem results.
dp = [-1] * 100
def solve(n):
if n == 0:
return 0
if dp[n] != -1:
return dp[n]
dp[n] = solve(n - 1) + 1
return dp[n]
print(solve(10))import java.util.Arrays;
class Solution {
static int[] dp = new int[100];
static int solve(int n) {
if (n == 0) return 0;
if (dp[n] != -1) return dp[n];
return dp[n] = solve(n - 1) + 1;
}
public static void main(String[] args) {
Arrays.fill(dp, -1);
System.out.println(solve(10));
}
}#include <iostream>
#include <vector>
std::vector<int> dp(100, -1);
int solve(int n) {
if (n == 0) return 0;
if (dp[n] != -1) return dp[n];
return dp[n] = solve(n - 1) + 1;
}
int main() {
std::cout << solve(10);
return 0;
}let dp = Array(100).fill(-1);
function solve(n) {
if (n === 0) return 0;
if (dp[n] !== -1) return dp[n];
return dp[n] = solve(n - 1) + 1;
}
console.log(solve(10));#include <stdio.h>
#define MAX_N 100
int dp[MAX_N];
int solve(int n) {
if (n == 0) return 0;
if (dp[n] != -1) return dp[n];
return dp[n] = solve(n - 1) + 1;
}
int main() {
for(int i = 0; i < MAX_N; i++) dp[i] = -1;
printf("%d", solve(10));
return 0;
}using System;
class Solution {
static int[] dp = new int[100];
static int Solve(int n) {
if (n == 0) return 0;
if (dp[n] != -1) return dp[n];
return dp[n] = Solve(n - 1) + 1;
}
static void Main() {
Array.Fill(dp, -1);
Console.WriteLine(Solve(10));
}
}Python's solution creates a 1D array for dp where computed results for each subproblem are stored, hence eliminating redundant calculations.
Time Complexity: O(n), derived from the linear calculations.
Space Complexity: O(n), due to storage needed for already computed values.
Approach 3: Memoized Search
We design a function dfs(i, cur, t) that represents the maximum number of partitions we can obtain when currently processing index i of string s, the current prefix already contains the character set cur, and we can still modify t characters. Then the answer is dfs(0, 0, 1).
The execution logic of function dfs(i, cur, t) is as follows:
- If
i geq n, it means we have finished processing strings, return 1. - Calculate the bitmask
v = 1 \ll (s[i] - 'a')corresponding to the current characters[i], and calculate the updated character setnxt = cur \mid v. - If the number of bits in
nxtexceedsk, it means the current prefix already contains more thankdistinct characters. We need to make a partition, increment the partition count by 1, and recursively calldfs(i + 1, v, t). Otherwise, continue recursively callingdfs(i + 1, nxt, t). - If
t > 0, it means we can still modify a character once. We try to change the current characters[i]to any lowercase letter (26 choices in total). For each choice, calculate the updated character setnxt = cur \mid (1 \ll j), and based on whether it exceedskdistinct characters, choose the corresponding recursive call method to update the maximum partition count. - Use a hash table to cache already computed states to avoid redundant calculations.
The time complexity is O(n times |\Sigma| times k) and the space complexity is O(n times |\Sigma| times k), where n is the length of string s, and |\Sigma| is the size of the character set.
function maxPartitionsAfterOperations(s: string, k: number): number {
const n = s.length;
const f: Map<bigint, number> = new Map();
const dfs = (i: number, cur: number, t: number): number => {
if (i >= n) {
return 1;
}
const key = (BigInt(i) << 27n) | (BigInt(cur) << 1n) | BigInt(t);
if (f.has(key)) {
return f.get(key)!;
}
const v = 1 << (s.charCodeAt(i) - 97);
let nxt = cur | v;
let ans = 0;
if (bitCount(nxt) > k) {
ans = dfs(i + 1, v, t) + 1;
} else {
ans = dfs(i + 1, nxt, t);
}
if (t) {
for (let j = 0; j < 26; ++j) {
nxt = cur | (1 << j);
if (bitCount(nxt) > k) {
ans = Math.max(ans, dfs(i + 1, 1 << j, 0) + 1);
} else {
ans = Math.max(ans, dfs(i + 1, nxt, 0));
}
}
}
f.set(key, ans);
return ans;
};
return dfs(0, 0, 1);
}
function bitCount(i: number): number {
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}func maxPartitionsAfterOperations(s string, k int) int {
n := len(s)
type tuple struct{ i, cur, t int }
f := map[tuple]int{}
var dfs func(i, cur, t int) int
dfs = func(i, cur, t int) int {
if i >= n {
return 1
}
key := tuple{i, cur, t}
if v, ok := f[key]; ok {
return v
}
v := 1 << (s[i] - 'a')
nxt := cur | v
var ans int
if bits.OnesCount(uint(nxt)) > k {
ans = dfs(i+1, v, t) + 1
} else {
ans = dfs(i+1, nxt, t)
}
if t > 0 {
for j := 0; j < 26; j++ {
nxt = cur | (1 << j)
if bits.OnesCount(uint(nxt)) > k {
ans = max(ans, dfs(i+1, 1<<j, 0)+1)
} else {
ans = max(ans, dfs(i+1, nxt, 0))
}
}
}
f[key] = ans
return ans
}
return dfs(0, 0, 1)
}Complexity comparison
| Approach | Complexity |
|---|---|
| Recursive Backtracking | Time Complexity: O(2^n), due to recursive exploration of states. |
| Dynamic Programming | Time Complexity: O(n), derived from the linear calculations. |
| Memoized Search | – |
Complexity analysis
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Recursive Backtracking | Exponential (≈ O(n * 26 * branching)) | O(n) | Useful for reasoning about all possible modifications and partitions; not practical for large inputs |
| Dynamic Programming + Bitmask | O(n * 26 * states) | O(n * states) | Best general solution; efficiently tracks distinct characters and modification state |
Frequently asked questions
Is Maximize the Number of Partitions After Operations easy or hard?
Maximize the Number of Partitions After Operations is classified as a Hard problem. The difficulty comes from combining string partitioning logic with a single modification constraint and representing character sets efficiently using bitmasks and dynamic programming.
Maximize the Number of Partitions After Operations Python/Java solution
Python, Java, C++, and other languages typically implement this solution using a recursive DP function with memoization. The function tracks the index in the string, the current bitmask of characters, and whether a modification has been used. Bit operations ensure constant-time updates when adding characters or testing distinct counts.
How to solve Maximize the Number of Partitions After Operations in O(n)?
A near-linear approach processes the string while tracking characters using a bitmask and limiting partitions to k distinct letters. Dynamic Programming stores states for index, mask, and whether a modification was used. Bit operations make updates constant time, and memoization ensures each state is computed once, keeping the overall complexity close to linear with a small constant factor.
What is the best approach for Maximize the Number of Partitions After Operations?
The most effective approach uses Dynamic Programming combined with a bitmask to track distinct characters in the current partition. Each DP state represents the index, current character mask, and whether the single allowed modification has been used. This avoids recomputation and efficiently handles distinct character constraints. The approach runs in roughly O(n * 26 * states) time.
Is Maximize the Number of Partitions After Operations asked at Google/Amazon/Meta?
Problems involving string partitioning with constraints and bitmask-based state tracking frequently appear in interviews at companies like Google, Amazon, and Meta. Variants that combine dynamic programming with bit manipulation are common in senior-level coding interviews.
What data structure is used in Maximize the Number of Partitions After Operations?
The core technique uses a bitmask to represent the set of characters currently in a partition. Dynamic Programming with memoization stores results for states defined by index, mask, and modification usage. Bit operations such as setting bits and counting set bits help enforce the k distinct character constraint efficiently.
What is the time complexity of Maximize the Number of Partitions After Operations?
The optimized Dynamic Programming solution runs in approximately O(n * 26 * states) time because each character may try up to 26 replacement possibilities when the modification is unused. Space complexity is O(n * states) for memoization. A naive recursive approach without memoization can grow exponentially and is not practical for large inputs.
