Data Structures · Prefix/Suffix & Stacks · A Field Guide

Max chunks to
make it sorted

Don't simulate sorting every partition. Ask the sharper question: where can you cut so that nothing ever needs to cross the line?

21344
01 · Strip it down

Cut into the most pieces that sort independently to the same result

Split the array into contiguous chunks, sort each chunk on its own, concatenate — the result must equal the fully sorted array. Maximize the number of chunks. For [2,1,3,4,4], the finest valid split is [2,1] | [3] | [4] | [4] → sorts to [1,2,3,4,4], so the answer is 4.

The question that cracks it A cut is only valid if no element needs to cross it. Since elements can't move across a boundary, everything on the left must already belong before everything on the right.

02 · The safe-boundary condition

max(left) ≤ min(right)

You don't need to compare every left-right pair. Extremes summarize everything: if the largest value on the left is ≤ the smallest value on the right, then every left value is ≤ every right value. Any left x ≤ max(left) ≤ min(right) ≤ y any right. So the cut after index i is safe exactly when:

The whole problem in one line prefixMax[i] ≤ suffixMin[i+1] — precompute a running max from the left and a running min from the right, and every boundary check is O(1).

It must be ≤, not < — this is the "II" variant with duplicates. [2] | [2] is a perfectly valid two-chunk split, and 2 ≤ 2 allows it.


03 · Why adjacent comparison fails

A boundary depends on aggregates, not neighbors

Tempting shortcut: cut wherever arr[i] ≤ arr[i+1]. It breaks. In [5,1,4,3,6], at index 1 we have 1 ≤ 4, but cutting [5,1] | [4,3,6] fails — the left max is 5 and the right min is 3, and 5 > 3. The stranded 5 belongs after the 3. Local order can't see a large value hiding earlier in the prefix; you need the aggregate.

A one-way turnstile

Nobody may cross the line. So the tallest person on the left must already be shorter than the shortest on the right — otherwise they're stuck on the wrong side forever.

Two rising watermarks

The prefix max only climbs; the suffix min only rises as you move right. Wherever the left watermark dips to or below the right one, it's safe to slice.

Stacking chunks by their ceiling

The stack view keeps each chunk's max. A too-small newcomer forces greedy chunks to merge until the ceiling behaves — the surviving count is the answer.


04 · Find the cuts

prefixMax above, suffixMin below

Type an array (or pick one). The top row is prefixMax (teal, running max from the left), the middle is the array, the bottom is suffixMin (terracotta, running min from the right). At each gap, the comparison prefixMax[i] vs suffixMin[i+1] lights a solid teal cut when safe, a faint mark when blocked. Chunks = safe cuts + 1.

Chunk boundary finder

chunks: 0
prefixMax (max so far →)
array
suffixMin (← min so far)

Try 5 4 3 2 1 (no safe cut → 1 chunk), 1 2 3 4 (every gap safe → 4), or 2 4 1 (a small tail forces one big chunk).


05 · Dry run

The sample, boundary by boundary

For [2,1,3,4,4]: prefixMax = [2,2,3,4,4], suffixMin = [1,1,3,4,4].

Cut after prefixMax[i] suffixMin[i+1] Safe?
0 2 1 no (2 > 1)
1 2 3 ✓
2 3 4 ✓
3 4 4 ✓ (equality!)

Three safe cuts → four chunks. Every safe boundary can be taken at once: each one guarantees left ≤ right independently, so they never conflict. That's why the answer is just count of safe cuts + 1 — greedy, no DP needed.


06 · The code

One suffix array, a rolling prefix max

int maxChunksToSorted(int[] arr) {
    int n = arr.length;
    int[] suffixMin = new int[n];
    suffixMin[n-1] = arr[n-1];
    for (int i = n-2; i >= 0; i--)
        suffixMin[i] = Math.min(arr[i], suffixMin[i+1]);

    int chunks = 1, prefixMax = Integer.MIN_VALUE;
    for (int i = 0; i < n-1; i++) {
        prefixMax = Math.max(prefixMax, arr[i]);
        if (prefixMax <= suffixMin[i+1]) chunks++;   // safe boundary
    }
    return chunks;
}

Build the suffix-min array once (O(N)), then sweep left keeping a rolling max — no need to store the full prefix array. O(N) time, O(N) space. Start chunks = 1 because an uncut array is already one chunk.


07 · The other O(N) view

A monotonic stack that merges bad chunks

Same answer, opposite lens. Keep a stack where each entry is one candidate chunk's maximum. A new value ≥ the top starts a fresh chunk. A smaller value can't stand alone after a taller chunk, so you pop the merged max, keep popping every chunk whose max exceeds the newcomer, and push back the single largest — those chunks fuse into one. The stack's final size is the chunk count.

Deque<Integer> st = new ArrayDeque<>();
for (int v : arr) {
    if (st.isEmpty() || v >= st.peek()) st.push(v);
    else {
        int mergedMax = st.pop();
        while (!st.isEmpty() && st.peek() > v) st.pop();
        st.push(mergedMax);                // merged chunk keeps its ceiling
    }
}
return st.size();
Two sides of one structure Prefix/suffix asks "can I cut here?" and finds valid boundaries. The stack asks "can these neighbors stay apart?" and destroys invalid ones. Both land on the same maximal partition.

08 · Traps to avoid

Four ways this goes wrong

Using the "I" trick

The easy version (a permutation of 0..n−1) cuts when prefixMax == index. That relies on special values. With duplicates and arbitrary numbers, use prefixMax ≤ suffixMin.

Strict < instead of ≤

[2,2] should give 2 chunks. 2 < 2 is false but 2 ≤ 2 is true — duplicates need the equals.

Comparing only neighbors

arr[i] ≤ arr[i+1] misses a big value stuck earlier in the prefix. The condition is about aggregates, not adjacent cells.

Trying every partition

There are 2^(N−1) ways to cut. Don't search them — characterize when a single boundary is valid and scan once.


09 · The pattern

Characterize the operation instead of simulating it

The array never actually gets sorted in the solution. Instead you ask what would make independent sorting impossible: a large left value that belongs after a small right value — a cross-boundary inversion. A boundary is safe precisely when none exists, i.e. max(left) ≤ min(right).

Whenever you see
partition · process each · concatenate · = global result
Ask: what would need to cross a boundary?
The tool it suggests
prefix aggregate + suffix aggregate
Here: prefix max, suffix min.

It's the same habit behind next-permutation's descending-suffix scan: before simulating an expensive operation, find the condition that tells you whether it would succeed. That converts an exponential search into a linear pass.


10 · One month from now

Reconstruct it, don't memorize it

  1. I want the most independently-sortable pieces.
  2. Elements can't cross a cut, so left must already belong before right.
  3. That's max(left) ≤ min(right) — extremes summarize all pairs.
  4. Precompute prefix max and suffix min so each boundary is O(1).
  5. Cut at every safe boundary; chunks = safe cuts + 1.
The transferable insight To maximize independent partitions, pin down the exact condition under which nothing needs to cross a boundary. Shifting from simulating the operation to characterizing when it's valid is the move that collapses this from "try every split and sort" into a single linear scan.