768. Max Chunks To Make Sorted II
Problem Description
You are given an integer array arr
.
The task is to split this array into some number of chunks (partitions), where each chunk is sorted individually. After sorting each chunk separately and then concatenating them back together in their original order, the final result should equal the fully sorted version of the original array.
For example, if we have arr = [2, 1, 3, 4]
, we could split it into chunks [2, 1]
and [3, 4]
. After sorting each chunk individually, we get [1, 2]
and [3, 4]
. Concatenating them gives us [1, 2, 3, 4]
, which equals the sorted original array.
The goal is to find the maximum number of chunks we can create while still satisfying the condition that sorting each chunk individually and concatenating them produces the sorted array.
The key insight is that for a valid chunk partition, the maximum value in any chunk must be less than or equal to the minimum value in all subsequent chunks. This ensures that when chunks are sorted individually and concatenated, the overall order is maintained.
Intuition
To understand how to maximize the number of chunks, let's think about what makes a valid chunk boundary. When we split the array into chunks and sort each chunk individually, the chunks must "fit together" properly after sorting to form the complete sorted array.
The critical observation is: for any valid chunk, all elements in that chunk must end up in the same positions in the final sorted array as they would if we just sorted that chunk alone. This means the maximum value in a chunk cannot be greater than any value that comes after it in a different chunk.
Consider walking through the array from left to right. As we process each element, we need to decide whether it can start a new chunk or must belong to the previous chunk.
If we encounter a value that is greater than or equal to the maximum value seen so far in the current chunk, it could potentially start a new chunk. Why? Because it won't violate the ordering when chunks are sorted and concatenated.
However, if we encounter a value that is smaller than the maximum of the current chunk, we have a problem. This smaller value needs to be sorted within a chunk that includes the previous maximum. In fact, it might even need to be merged with earlier chunks if it's smaller than their maximums too.
This naturally leads to a stack-based approach where:
- Each element in the stack represents the maximum value of a chunk
- When we see a new element that's greater than or equal to the stack top, it can form its own chunk
- When we see a smaller element, we need to merge chunks by popping from the stack until we find where this element belongs, but we preserve the maximum of the merged chunks
The number of elements remaining in the stack at the end represents the maximum number of chunks we can create.
Learn more about Stack, Greedy, Sorting and Monotonic Stack patterns.
Solution Approach
The solution uses a monotonic stack to track the maximum values of each chunk. The stack maintains the property that elements are in non-decreasing order from bottom to top.
Here's how the algorithm works step by step:
-
Initialize an empty stack
stk
to store the maximum values of chunks. -
Iterate through each element
v
in the array:- Case 1: Can form a new chunk - If the stack is empty OR the current element
v
is greater than or equal to the top of the stack (v >= stk[-1]
):- Simply push
v
onto the stack - This means
v
can be the maximum of a new chunk
- Simply push
- Case 2: Need to merge chunks - If
v
is less than the stack top:- First, save the current maximum by popping it:
mx = stk.pop()
- Keep popping elements from the stack while the stack is not empty AND the top element is greater than
v
(stk[-1] > v
) - This merges all chunks whose maximum values are greater than the current element
- Finally, push back the saved maximum
mx
onto the stack - This represents the maximum of the newly merged chunk
- First, save the current maximum by popping it:
- Case 1: Can form a new chunk - If the stack is empty OR the current element
-
Return the stack size - The number of elements in the stack represents the maximum number of chunks.
Why this works:
- The stack maintains maximum values of chunks in increasing order
- When we encounter a smaller value, it must belong to a chunk with all previous elements that have maximums greater than it
- By merging chunks (popping from stack) and keeping the overall maximum, we ensure the chunk boundaries are valid
- Each element in the final stack represents one valid chunk
Time Complexity: O(n)
where n
is the length of the array, as each element is pushed and popped at most once.
Space Complexity: O(n)
in the worst case when the array is already sorted and each element forms its own chunk.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with arr = [2, 1, 3, 4, 3]
.
Initial state: stk = []
Step 1: Process element 2
- Stack is empty, so push 2
stk = [2]
- This creates our first chunk with maximum 2
Step 2: Process element 1
- 1 < 2 (top of stack), so we need to merge
- Save max:
mx = 2
(pop from stack) - Stack is now empty, so stop popping
- Push back
mx = 2
stk = [2]
- Elements [2, 1] form one chunk with maximum 2
Step 3: Process element 3
- 3 ≥ 2 (top of stack), so we can form a new chunk
- Push 3
stk = [2, 3]
- Now we have two chunks: [2, 1] with max 2, and [3] with max 3
Step 4: Process element 4
- 4 ≥ 3 (top of stack), so we can form a new chunk
- Push 4
stk = [2, 3, 4]
- Three chunks: [2, 1], [3], and [4]
Step 5: Process element 3
- 3 < 4 (top of stack), so we need to merge
- Save max:
mx = 4
(pop from stack) - Now check: 3 ≥ 3 (new top), so stop popping
- Push back
mx = 4
stk = [2, 3, 4]
- The last two elements [4, 3] merge into one chunk with maximum 4
Final result: stk = [2, 3, 4]
has 3 elements, so maximum chunks = 3
The chunks are: [2, 1]
, [3]
, [4, 3]
- Sorting individually:
[1, 2]
,[3]
,[3, 4]
- Concatenating:
[1, 2, 3, 3, 4]
✓ (equals the sorted original array)
Solution Implementation
1class Solution:
2 def maxChunksToSorted(self, arr: List[int]) -> int:
3 # Stack to maintain the maximum value of each chunk
4 stack = []
5
6 for value in arr:
7 # If stack is empty or current value is greater than or equal to
8 # the max of the last chunk, start a new chunk
9 if not stack or value >= stack[-1]:
10 stack.append(value)
11 else:
12 # Current value is smaller than the max of the last chunk
13 # Need to merge chunks
14
15 # Save the maximum value of the current chunk being merged
16 max_value = stack.pop()
17
18 # Pop all chunks whose max value is greater than current value
19 # These chunks need to be merged because the current smaller value
20 # would need to be sorted before their elements
21 while stack and stack[-1] > value:
22 stack.pop()
23
24 # Push back the maximum value of all merged chunks
25 stack.append(max_value)
26
27 # The number of elements in stack represents the number of chunks
28 return len(stack)
29
1class Solution {
2 public int maxChunksToSorted(int[] arr) {
3 // Use a stack to track the maximum value of each chunk
4 Deque<Integer> stack = new ArrayDeque<>();
5
6 for (int currentValue : arr) {
7 // If stack is empty or current value is greater than or equal to the top,
8 // it can form a new chunk
9 if (stack.isEmpty() || stack.peek() <= currentValue) {
10 stack.push(currentValue);
11 } else {
12 // Current value is smaller than the top, need to merge chunks
13 // Save the maximum value of the chunk that needs to be merged
14 int maxValue = stack.pop();
15
16 // Keep popping chunks whose maximum is greater than current value
17 // These chunks need to be merged because current value belongs to them
18 while (!stack.isEmpty() && stack.peek() > currentValue) {
19 stack.pop();
20 }
21
22 // Push back the maximum value of the merged chunk
23 stack.push(maxValue);
24 }
25 }
26
27 // The size of the stack represents the maximum number of chunks
28 return stack.size();
29 }
30}
31
1class Solution {
2public:
3 int maxChunksToSorted(vector<int>& arr) {
4 // Stack to maintain the maximum value of each chunk
5 // Each element in the stack represents the max value of a chunk
6 stack<int> maxStack;
7
8 for (int& value : arr) {
9 // Case 1: If stack is empty or current value is greater than or equal to
10 // the max of the last chunk, we can start a new chunk
11 if (maxStack.empty() || maxStack.top() <= value) {
12 maxStack.push(value);
13 }
14 // Case 2: Current value is smaller than the max of the last chunk
15 // We need to merge chunks
16 else {
17 // Save the maximum value of the current chunk before merging
18 int currentChunkMax = maxStack.top();
19 maxStack.pop();
20
21 // Keep popping and merging chunks while their max values are greater than current value
22 // This ensures that after sorting, all values in merged chunks will be in correct position
23 while (!maxStack.empty() && maxStack.top() > value) {
24 maxStack.pop();
25 }
26
27 // Push back the maximum value of all merged chunks
28 maxStack.push(currentChunkMax);
29 }
30 }
31
32 // The size of the stack represents the maximum number of chunks
33 return maxStack.size();
34 }
35};
36
1/**
2 * Finds the maximum number of chunks that can be made to sort the array.
3 * Each chunk can be sorted independently, and concatenating sorted chunks gives the sorted array.
4 *
5 * @param arr - The input array to be partitioned into chunks
6 * @returns The maximum number of chunks that can be created
7 */
8function maxChunksToSorted(arr: number[]): number {
9 // Stack to maintain the maximum value of each chunk
10 const stack: number[] = [];
11
12 // Process each element in the array
13 for (const value of arr) {
14 // If stack is empty or current value is greater than or equal to the last chunk's max,
15 // we can start a new chunk
16 if (stack.length === 0 || value >= stack[stack.length - 1]) {
17 stack.push(value);
18 } else {
19 // Current value is smaller than the last chunk's max,
20 // so it must belong to an existing chunk
21
22 // Store the maximum value of the current chunk
23 const currentChunkMax = stack.pop()!;
24
25 // Merge all previous chunks that have max values greater than current value
26 // These chunks cannot be independent as the current value needs to be sorted with them
27 while (stack.length > 0 && stack[stack.length - 1] > value) {
28 stack.pop();
29 }
30
31 // Push back the maximum value of the merged chunk
32 stack.push(currentChunkMax);
33 }
34 }
35
36 // The size of the stack represents the number of chunks
37 return stack.length;
38}
39
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input array.
The algorithm iterates through each element in the array exactly once. For each element, we perform stack operations (push and pop). While there's a nested while loop that pops elements from the stack, each element can be pushed to and popped from the stack at most once throughout the entire execution. Therefore, the total number of operations across all iterations is bounded by 2n
(each element is pushed once and popped at most once), resulting in O(n)
time complexity.
Space Complexity: O(n)
in the worst case.
The space is used by the stack stk
. In the worst-case scenario, when the input array is already sorted in ascending order (e.g., [1, 2, 3, 4, 5]
), each element will be pushed to the stack and remain there, resulting in the stack containing all n
elements. Therefore, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrectly Handling Equal Values
The Problem:
A common mistake is using strict inequality (>
) instead of >=
when deciding whether to create a new chunk. This would incorrectly merge chunks when encountering equal values.
Incorrect Implementation:
if not stack or value > stack[-1]: # Wrong: using > instead of >= stack.append(value)
Why It's Wrong:
Consider arr = [1, 1, 2]
. With strict inequality:
- Push 1 (stack: [1])
- Second 1 is not greater than stack top, so merge (stack: [1])
- Push 2 (stack: [1, 2])
- Result: 2 chunks
But the correct answer is 3 chunks: [1], [1], [2], since equal values can be in separate chunks as long as they maintain sorted order.
Correct Approach:
if not stack or value >= stack[-1]: # Correct: using >= stack.append(value)
Pitfall 2: Forgetting to Save the Maximum Before Merging
The Problem: When merging chunks, forgetting to save the maximum value of the chunks being merged leads to incorrect results.
Incorrect Implementation:
else: # Forgot to save the maximum! while stack and stack[-1] > value: stack.pop() stack.append(value) # Wrong: pushing current value instead of maximum
Why It's Wrong:
Consider arr = [2, 1, 3]
:
- Push 2 (stack: [2])
- 1 < 2, need to merge, but if we push 1 instead of 2, stack becomes [1]
- Push 3 (stack: [1, 3])
- Result: 2 chunks with incorrect maximums
The issue is that we lose track of the actual maximum value in the merged chunk, which should be 2, not 1.
Correct Approach:
else: max_value = stack.pop() # Save the maximum first while stack and stack[-1] > value: stack.pop() stack.append(max_value) # Push back the saved maximum
Pitfall 3: Over-Merging Chunks
The Problem: Popping too many elements from the stack by using wrong comparison conditions during the merge process.
Incorrect Implementation:
else: max_value = stack.pop() while stack and stack[-1] >= value: # Wrong: using >= instead of > stack.pop() stack.append(max_value)
Why It's Wrong:
Consider arr = [1, 2, 1, 3]
:
- Push 1 (stack: [1])
- Push 2 (stack: [1, 2])
- For the second 1: max_value = 2, and with
>=
, we'd pop the first 1 too (since 1 >= 1) - This incorrectly merges all three elements into one chunk
The correct behavior should only merge chunks whose maximum is strictly greater than the current value, allowing chunks with equal maximums to remain separate.
Correct Approach:
while stack and stack[-1] > value: # Correct: using strict > stack.pop()
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!