Split Array into Consecutive Subsequences

MediumHashing · Greedy🧩 Hashing & CountingLeetCode #659 ↗Updated

Problem

You are given an integer array nums sorted in non-decreasing order. Decide whether every element can be assigned to exactly one subsequence such that each subsequence is a run of consecutive integers (each value one more than the previous) of length at least 3. Return true if such a split exists.
Inputnums = [1,2,3,3,4,5]Outputtrue ([1,2,3] and [3,4,5])
Inputnums = [1,2,3,3,4,4,5,5]Outputtrue ([1,2,3,4,5] and [3,4,5])
Inputnums = [1,2,3,4,4,5]Outputfalse

1 ≤ n ≤ 104  ·  -1000 ≤ nums[i] ≤ 1000  ·  nums is sorted non-decreasing

Try it yourself

Java
class Solution {
    public boolean isPossible(int[] nums) {
        
    }
}
Your code is saved in this browser.Submit on LeetCode ↗
Key insight
For each value x (in order), extending an existing run that ends at x−1 is never worse than starting a new one. Only if no run is waiting must x start a fresh run — and then it must immediately grab x+1 and x+2.
PatternHashing & Counting

Instinct Need "have I seen this before?", "how many times?" or "group things that look alike"? Trade memory for O(1) lookups with a HashMap, HashSet or int[26].

Memory hack Don't search, remember - every element checks in at the front desk, and the desk answers instantly.

Open the Hashing & Counting guide →

Approaches

Brute ForceBacktracking over Assignments

Time ExponentialSpace O(n)

Process values in order. Each value either joins some run currently ending at x−1 or opens a new run; try every option recursively. Prune when a run shorter than 3 has an end below x−1, since it can never grow again.

Java
public boolean isPossible(int[] nums) {
    return assign(nums, 0, new ArrayList<>());
}

// each run is {lastValue, length}
private boolean assign(int[] nums, int i, List<int[]> runs) {
    int x = i < nums.length ? nums[i] : Integer.MAX_VALUE;
    for (int[] r : runs) {
        if (r[1] < 3 && r[0] < x - 1) return false; // dead short run
    }
    if (i == nums.length) return true;
    int size = runs.size(); // deeper calls may append, so iterate by index
    for (int k = 0; k < size; k++) {
        int[] r = runs.get(k);
        if (r[0] != x - 1) continue;
        r[0] = x; r[1]++;
        if (assign(nums, i + 1, runs)) return true;
        r[0] = x - 1; r[1]--; // undo
    }
    runs.add(new int[]{x, 1});
    if (assign(nums, i + 1, runs)) return true;
    runs.remove(runs.size() - 1);
    return false;
}

Explores every way to distribute duplicates — exponential, only for tiny inputs.

At a glance

LevelApproachTimeSpace
Brute ForceBacktracking over AssignmentsExponentialO(n)
BetterMin-Heap of Runs (Extend the Shortest)O(n log n)O(n)
OptimalCounts + “Runs Waiting for x” MapO(n)O(n)
Common pitfalls
  • Preferring to start a new run over extending one — leaves short runs stranded.
  • Opening a run with just x and hoping to finish it later — check x+1, x+2 now.
  • Forgetting to skip values already consumed by an earlier run (count reaches 0).
Interview tips
  • Justify the greedy: extending never hurts because the extended run is already valid.
  • Mention the heap version first if you think of runs; then drop the heap via counts.
  • Walk through [1,2,3,4,4,5] to show a failing case clearly.
Pattern
Greedy with demand counts → group sorted items into chains; track how many chains are waiting for each next value.
My notes saved in this browser