659. Split Array into Consecutive Subsequences
Problem Description
You have an integer array nums
that is sorted in non-decreasing order (like [1, 2, 2, 3, 3, 4, 4, 5]
).
Your task is to determine if you can split all elements of this array into groups (subsequences) where:
-
Each group forms a consecutive increasing sequence: The numbers in each group must be consecutive integers that increase by exactly 1. For example,
[3, 4, 5]
is valid, but[3, 5, 6]
is not (missing 4). -
Each group must have at least 3 elements: Groups like
[1, 2]
are too short and not allowed.
The key point is that you're selecting elements from the original array to form these groups, and you can skip elements as long as you maintain the relative order. Every element in nums
must be used exactly once in exactly one group.
For example:
nums = [1, 2, 3, 3, 4, 5]
can be split into[1, 2, 3]
and[3, 4, 5]
, so returntrue
nums = [1, 2, 3, 3, 4, 4, 5, 5]
can be split into[1, 2, 3, 4, 5]
and[3, 4, 5]
, so returntrue
nums = [1, 2, 3, 4, 4, 5]
cannot be split according to the rules, so returnfalse
The solution uses a greedy approach with heaps. For each number, it either extends an existing sequence that ends at v-1
, or starts a new sequence. It tracks the length of sequences ending at each value using min-heaps. At the end, it checks if all sequences have length greater than 2.
Intuition
When we encounter each number in the sorted array, we face a critical decision: should this number extend an existing sequence or start a new one?
The key insight is that we should always prefer to extend an existing sequence rather than starting a new one. Why? Because starting a new sequence creates an additional constraint - we'll need to find at least two more consecutive numbers to complete it. By extending an existing sequence, we're making progress toward satisfying the length requirement.
Consider what happens when we see a number v
:
- If there's a sequence ending at
v-1
, we can extend it by addingv
- If no sequence ends at
v-1
, we have no choice but to start a new sequence beginning withv
But what if multiple sequences end at v-1
? Which one should we extend? Here's the crucial observation: we should extend the shortest sequence first. This is because shorter sequences are at greater risk of not meeting the minimum length requirement of 3. By prioritizing them, we maximize our chances of making all sequences valid.
This naturally leads to using a min-heap (priority queue) data structure. For each value, we maintain a min-heap of sequence lengths ending at that value. When we process a number v
:
- Check if any sequences end at
v-1
- If yes, pop the shortest one (using
heappop
), increment its length, and push it to the heap for valuev
- If no, start a new sequence of length 1 at value
v
After processing all numbers, we simply verify that every sequence has reached a length of at least 3. The beauty of this approach is that it greedily makes the locally optimal choice at each step, which turns out to give us the globally optimal solution.
Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The implementation uses a greedy algorithm with min-heaps to track and extend sequences optimally.
Data Structure:
d = defaultdict(list)
: A dictionary where each key is a number and the value is a min-heap containing lengths of all sequences ending at that number.
Algorithm Steps:
-
Iterate through each number
v
in the sorted array:for v in nums:
-
Check if we can extend an existing sequence:
if h := d[v - 1]: heappush(d[v], heappop(h) + 1)
- Use the walrus operator
:=
to check if there are sequences ending atv-1
- If yes, pop the shortest sequence from
d[v-1]
usingheappop(h)
- Extend this sequence by incrementing its length by 1
- Push the extended sequence to
d[v]
usingheappush
- Use the walrus operator
-
Start a new sequence if no extension is possible:
else: heappush(d[v], 1)
- If no sequences end at
v-1
, start a new sequence of length 1 at valuev
- If no sequences end at
-
Validate all sequences:
return all(not v or v and v[0] > 2 for v in d.values())
- Check each heap in the dictionary
not v
: Empty heap is valid (no sequences ending at this value)v and v[0] > 2
: If heap exists, the minimum length (at index 0) must be greater than 2- The
all()
function ensures every sequence meets the length requirement
Why This Works:
- By always extending the shortest sequence first (using min-heap), we maximize the chance that all sequences reach the minimum required length
- The greedy choice at each step (extend shortest or start new) leads to the optimal global solution
- Since the array is sorted, we process numbers in order, ensuring we can only extend sequences in a valid consecutive manner
Time Complexity: O(n log n)
where n is the length of nums, due to heap operations
Space Complexity: O(n)
for storing the dictionary and heaps
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with nums = [1, 2, 3, 3, 4, 5]
to see how it determines this can be split into valid groups.
Initial state: d = {}
(empty dictionary)
Step 1: Process v = 1
- Check if sequences end at
v-1 = 0
: No sequences atd[0]
- Start new sequence:
d[1] = [1]
(sequence of length 1 ending at 1)
Step 2: Process v = 2
- Check if sequences end at
v-1 = 1
: Yes!d[1] = [1]
- Extend shortest sequence: Pop length 1 from
d[1]
, add 1, push tod[2]
- Result:
d[1] = []
,d[2] = [2]
(sequence of length 2 ending at 2)
Step 3: Process v = 3
- Check if sequences end at
v-1 = 2
: Yes!d[2] = [2]
- Extend shortest sequence: Pop length 2 from
d[2]
, add 1, push tod[3]
- Result:
d[2] = []
,d[3] = [3]
(sequence of length 3 ending at 3)
Step 4: Process v = 3
(second occurrence)
- Check if sequences end at
v-1 = 2
: No sequences atd[2]
(empty now) - Start new sequence:
d[3] = [3, 1]
→ After heappush:d[3] = [1, 3]
- Now we have two sequences ending at 3: one of length 3, one of length 1
Step 5: Process v = 4
- Check if sequences end at
v-1 = 3
: Yes!d[3] = [1, 3]
- Extend shortest sequence: Pop length 1 (the minimum), add 1, push to
d[4]
- Result:
d[3] = [3]
,d[4] = [2]
(extended the newer sequence)
Step 6: Process v = 5
- Check if sequences end at
v-1 = 4
: Yes!d[4] = [2]
- Extend shortest sequence: Pop length 2, add 1, push to
d[5]
- Result:
d[4] = []
,d[5] = [3]
Final validation:
d[1] = []
(empty, valid)d[2] = []
(empty, valid)d[3] = [3]
(minimum length is 3, valid ✓)d[4] = []
(empty, valid)d[5] = [3]
(minimum length is 3, valid ✓)
All sequences have length ≥ 3, so return true
.
The algorithm successfully identified two sequences: [1, 2, 3]
(the first three elements) and [3, 4, 5]
(elements at indices 3, 4, 5).
Solution Implementation
1from collections import defaultdict
2from heapq import heappush, heappop
3from typing import List
4
5class Solution:
6 def isPossible(self, nums: List[int]) -> bool:
7 # Dictionary to store min-heaps of subsequence lengths ending at each number
8 # Key: ending number of subsequence
9 # Value: min-heap of lengths of subsequences ending at this number
10 subsequence_lengths = defaultdict(list)
11
12 # Process each number in the input array
13 for current_num in nums:
14 # Check if there's any subsequence ending at (current_num - 1)
15 # that we can extend with current_num
16 if subsequence_lengths[current_num - 1]:
17 # Get the shortest subsequence ending at (current_num - 1)
18 shortest_length = heappop(subsequence_lengths[current_num - 1])
19 # Extend it by 1 and add to subsequences ending at current_num
20 heappush(subsequence_lengths[current_num], shortest_length + 1)
21 else:
22 # No subsequence to extend, start a new subsequence of length 1
23 heappush(subsequence_lengths[current_num], 1)
24
25 # Check if all subsequences have length > 2 (at least 3 elements)
26 for heap in subsequence_lengths.values():
27 # If heap is empty, skip it
28 if not heap:
29 continue
30 # If the shortest subsequence (heap[0]) has length <= 2, return False
31 if heap[0] <= 2:
32 return False
33
34 return True
35
1class Solution {
2 public boolean isPossible(int[] nums) {
3 // Map to store ending number -> priority queue of subsequence lengths ending at that number
4 // Priority queue keeps shortest subsequence length at the top (min heap)
5 Map<Integer, PriorityQueue<Integer>> endingNumberToLengths = new HashMap<>();
6
7 // Process each number in the input array
8 for (int currentNum : nums) {
9 // Check if there's a subsequence ending at (currentNum - 1) that we can extend
10 if (endingNumberToLengths.containsKey(currentNum - 1)) {
11 // Get the priority queue of subsequences ending at (currentNum - 1)
12 PriorityQueue<Integer> previousQueue = endingNumberToLengths.get(currentNum - 1);
13
14 // Remove the shortest subsequence and extend it by 1
15 int shortestLength = previousQueue.poll();
16
17 // Add the extended subsequence to subsequences ending at currentNum
18 endingNumberToLengths.computeIfAbsent(currentNum, k -> new PriorityQueue<>())
19 .offer(shortestLength + 1);
20
21 // Clean up: remove entry if no more subsequences end at (currentNum - 1)
22 if (previousQueue.isEmpty()) {
23 endingNumberToLengths.remove(currentNum - 1);
24 }
25 } else {
26 // No subsequence to extend, start a new subsequence of length 1
27 endingNumberToLengths.computeIfAbsent(currentNum, k -> new PriorityQueue<>())
28 .offer(1);
29 }
30 }
31
32 // Check if all subsequences have length at least 3
33 for (PriorityQueue<Integer> lengths : endingNumberToLengths.values()) {
34 // If the shortest subsequence is less than 3, return false
35 if (lengths.peek() < 3) {
36 return false;
37 }
38 }
39
40 // All subsequences have valid length (>= 3)
41 return true;
42 }
43}
44
1class Solution {
2public:
3 bool isPossible(vector<int>& nums) {
4 // Map to store ending values and their corresponding subsequence lengths
5 // Key: ending value of subsequences
6 // Value: min-heap of lengths of subsequences ending at this value
7 unordered_map<int, priority_queue<int, vector<int>, greater<int>>> endingValueToLengths;
8
9 // Process each number in the input array
10 for (int currentNum : nums) {
11 // Check if there's a subsequence ending at (currentNum - 1)
12 if (endingValueToLengths.count(currentNum - 1)) {
13 // Get the min-heap of subsequences ending at (currentNum - 1)
14 auto& previousSubsequences = endingValueToLengths[currentNum - 1];
15
16 // Extend the shortest subsequence by adding current number
17 int shortestLength = previousSubsequences.top();
18 endingValueToLengths[currentNum].push(shortestLength + 1);
19
20 // Remove the extended subsequence from previous ending value
21 previousSubsequences.pop();
22
23 // Clean up empty entries
24 if (previousSubsequences.empty()) {
25 endingValueToLengths.erase(currentNum - 1);
26 }
27 } else {
28 // Start a new subsequence with current number
29 endingValueToLengths[currentNum].push(1);
30 }
31 }
32
33 // Check if all subsequences have length >= 3
34 for (const auto& [endingValue, lengthsHeap] : endingValueToLengths) {
35 // If any subsequence has length < 3, return false
36 if (lengthsHeap.top() < 3) {
37 return false;
38 }
39 }
40
41 return true;
42 }
43};
44
1function isPossible(nums: number[]): boolean {
2 // Map to store ending values and their corresponding subsequence lengths
3 // Key: ending value of subsequences
4 // Value: array of lengths of subsequences ending at this value (simulating min-heap)
5 const endingValueToLengths: Map<number, number[]> = new Map();
6
7 // Helper function to get the minimum value from array (simulating heap.top())
8 const getMinLength = (lengths: number[]): number => {
9 return Math.min(...lengths);
10 };
11
12 // Helper function to remove the minimum value from array (simulating heap.pop())
13 const removeMinLength = (lengths: number[]): void => {
14 const minValue = Math.min(...lengths);
15 const index = lengths.indexOf(minValue);
16 lengths.splice(index, 1);
17 };
18
19 // Process each number in the input array
20 for (const currentNum of nums) {
21 // Check if there's a subsequence ending at (currentNum - 1)
22 if (endingValueToLengths.has(currentNum - 1)) {
23 // Get the array of subsequences ending at (currentNum - 1)
24 const previousSubsequences = endingValueToLengths.get(currentNum - 1)!;
25
26 // Extend the shortest subsequence by adding current number
27 const shortestLength = getMinLength(previousSubsequences);
28
29 // Initialize array for current number if it doesn't exist
30 if (!endingValueToLengths.has(currentNum)) {
31 endingValueToLengths.set(currentNum, []);
32 }
33 endingValueToLengths.get(currentNum)!.push(shortestLength + 1);
34
35 // Remove the extended subsequence from previous ending value
36 removeMinLength(previousSubsequences);
37
38 // Clean up empty entries
39 if (previousSubsequences.length === 0) {
40 endingValueToLengths.delete(currentNum - 1);
41 }
42 } else {
43 // Start a new subsequence with current number
44 if (!endingValueToLengths.has(currentNum)) {
45 endingValueToLengths.set(currentNum, []);
46 }
47 endingValueToLengths.get(currentNum)!.push(1);
48 }
49 }
50
51 // Check if all subsequences have length >= 3
52 for (const [endingValue, lengthsArray] of endingValueToLengths) {
53 // If any subsequence has length < 3, return false
54 if (getMinLength(lengthsArray) < 3) {
55 return false;
56 }
57 }
58
59 return true;
60}
61
Time and Space Complexity
Time Complexity: O(n log n)
The algorithm iterates through the input array nums
once, performing the following operations for each element:
- Checking if
d[v - 1]
exists:O(1)
average case for dictionary lookup heappop
operation:O(log k)
wherek
is the size of the heap at keyv - 1
heappush
operation:O(log m)
wherem
is the size of the heap at keyv
In the worst case, all elements could end up in the same heap, making the heap operations O(log n)
. Since we perform these operations for each of the n
elements, the overall time complexity is O(n log n)
.
The final validation step iterates through all dictionary values, checking the minimum element of each heap. Since accessing the minimum of a heap is O(1)
and we have at most n
distinct keys, this step is O(n)
.
Therefore, the total time complexity is O(n log n) + O(n) = O(n log n)
.
Space Complexity: O(n)
The space is used by:
- The dictionary
d
which stores heaps: In the worst case, we haven
distinct values as keys, and the total number of elements across all heaps equalsn
(since each input element is pushed exactly once) - The heaps themselves store integers representing sequence lengths
The total space required is O(n)
for storing all the elements across the dictionary and its heaps.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Understanding Why Greedy Extension Works
Many developers struggle to understand why always extending the shortest sequence is optimal. The key insight is that by extending the shortest sequences first, we give them the best chance to reach the minimum required length of 3. If we extended longer sequences instead, shorter ones might get "stranded" without enough consecutive numbers to reach length 3.
Example of Wrong Approach:
# WRONG: Extending any available sequence randomly if subsequence_lengths[current_num - 1]: # Just pop any sequence, not necessarily the shortest some_length = subsequence_lengths[current_num - 1].pop() # Wrong! subsequence_lengths[current_num].append(some_length + 1)
2. Forgetting to Use a Min-Heap
A critical pitfall is using a regular list instead of a heap, which breaks the algorithm's correctness.
Incorrect Implementation:
# WRONG: Using regular lists instead of heaps
subsequence_lengths = defaultdict(list)
for current_num in nums:
if subsequence_lengths[current_num - 1]:
# This doesn't get the minimum!
length = subsequence_lengths[current_num - 1].pop(0) # Wrong!
subsequence_lengths[current_num].append(length + 1)
Correct Fix: Always use heappush
and heappop
to maintain the min-heap property.
3. Incorrect Final Validation
A subtle but common mistake is checking the wrong condition at the end.
Wrong Validation Examples:
# WRONG: Checking if ANY sequence is valid
return any(not v or min(v) > 2 for v in subsequence_lengths.values())
# WRONG: Checking maximum instead of minimum
return all(not v or max(v) > 2 for v in subsequence_lengths.values())
# WRONG: Off-by-one error in length check
return all(not v or v[0] >= 2 for v in subsequence_lengths.values()) # Should be > 2
Correct Validation:
# Check that ALL sequences have length > 2 (at least 3 elements)
return all(not heap or heap[0] > 2 for heap in subsequence_lengths.values())
4. Misunderstanding Empty Heaps in Final Check
Some developers get confused about why we need to check not heap
in the final validation. Empty heaps are valid because they mean no sequences end at that particular number - all sequences that passed through that number have been extended further.
Overly Complicated Check:
# Unnecessary: Filtering out empty heaps
non_empty_heaps = [heap for heap in subsequence_lengths.values() if heap]
return all(heap[0] > 2 for heap in non_empty_heaps)
While this works, the simpler not heap or heap[0] > 2
pattern is cleaner and more idiomatic.
5. Not Handling Edge Cases Properly
Failing to consider arrays where all numbers are the same or have large gaps.
Example Edge Cases to Consider:
nums = [1, 1, 1, 2, 2, 2, 3, 3, 3]
- All numbers appear exactly 3 timesnums = [1, 2, 3, 6, 7, 8]
- Gap between consecutive sequencesnums = [1]
- Single element (should return False)
The algorithm handles these correctly, but understanding why helps avoid modifications that break these cases.
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
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
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!