Split Array into Consecutive Subsequences
MedPrompt
You are given an integer array nums that is sorted in non-decreasing order.
Determine if it is possible to split nums into one or more subsequences such that both of the following conditions are true:
- Each subsequence is a consecutive increasing sequence (i.e. each integer is exactly one more than the previous integer).
- All subsequences have a length of
3or more.
Return true if you can split nums according to the above conditions, or false otherwise.
A subsequence of an array is a new array that is formed from the original array by deleting some (can be none) of the elements without disturbing the relative positions of the remaining elements. (i.e., [1,3,5] is a subsequence of [1,2,3,4,5] while [1,3,2] is not).
Example 1:
Input: nums = [1,2,3,3,4,5]
Output: true
Explanation: nums can be split into the following subsequences:
[1,2,3,3,4,5] --> 1, 2, 3
[1,2,3,3,4,5] --> 3, 4, 5Example 2:
Input: nums = [1,2,3,3,4,4,5,5]
Output: true
Explanation: nums can be split into the following subsequences:
[1,2,3,3,4,4,5,5] --> 1, 2, 3, 4, 5
[1,2,3,3,4,4,5,5] --> 3, 4, 5Example 3:
Input: nums = [1,2,3,4,4,5]
Output: false
Explanation: It is impossible to split nums into consecutive increasing subsequences of length 3 or more.
Constraints:
1 <= nums.length <= 104-1000 <= nums[i] <= 1000numsis sorted in non-decreasing order.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a greedy strategy combined with a min-heap (PriorityQueue in Java) to keep track of the lengths of all active subsequences. The core idea is that for any number x, it's always optimal to append it to an existing subsequence ending in x-1 rather than starting a new one. If there are multiple such subsequences, we extend the one that is currently the shortest. This is because shorter subsequences are 'in more danger' of not reaching the required length of 3, so we prioritize helping them grow.
Algorithm
- Initialize a
HashMapnamedtailswhere the key is an integerxand the value is aPriorityQueueof lengths of consecutive subsequences ending atx. - Iterate through each number
numin the input arraynums. - Check if there is a priority queue for
num - 1in thetailsmap and if it's not empty. This signifies that there's at least one subsequence ending atnum - 1that we can extend. - If such a subsequence exists, we greedily extend the shortest one. We do this by polling the smallest length
lenfrom the priority queuetails.get(num - 1). - We then add a new entry to the priority queue for
num, with the new lengthlen + 1. We usemap.computeIfAbsentto create a new priority queue if one doesn't exist fornum. - If no subsequence ending at
num - 1exists, we must start a new subsequence. We add a new entry of length1to the priority queue fornum. - After iterating through all numbers, we examine the
tailsmap. We iterate through all the priority queues in the map. - For each priority queue, we check if its smallest element (the length of the shortest subsequence ending at that number) is less than 3.
- If we find any subsequence with a length less than 3, it's impossible to satisfy the conditions, so we return
false. - If all subsequences have a length of 3 or more, we return
true.
Walkthrough
We use a HashMap where keys are the ending numbers of subsequences and values are PriorityQueues storing the lengths of these subsequences. The PriorityQueue acts as a min-heap, allowing us to efficiently access the shortest subsequence ending at a particular number.
import java.util.HashMap;import java.util.Map;import java.util.PriorityQueue; class Solution { public boolean isPossible(int[] nums) { // Map from a number to a priority queue of lengths of subsequences ending with that number. Map<Integer, PriorityQueue<Integer>> tails = new HashMap<>(); for (int num : nums) { if (tails.containsKey(num - 1) && !tails.get(num - 1).isEmpty()) { // Extend the shortest subsequence ending at num - 1 int length = tails.get(num - 1).poll(); // Add the new extended subsequence tails.computeIfAbsent(num, k -> new PriorityQueue<>()).add(length + 1); } else { // Start a new subsequence of length 1 tails.computeIfAbsent(num, k -> new PriorityQueue<>()).add(1); } } // Check if all subsequences have length 3 or more for (PriorityQueue<Integer> pq : tails.values()) { if (!pq.isEmpty() && pq.peek() < 3) { return false; } } return true; }}After processing all numbers, we iterate through the map. If any priority queue contains a length less than 3, it means we have a subsequence that is too short, and we return false. If all subsequences meet the length requirement, we return true.
Complexity
Time
O(N log N), where N is the length of `nums`. We iterate through each of the N numbers. For each number, we perform map lookups (which are O(1) on average) and at most one heap poll and one heap add operation. Heap operations take O(log K) time, where K is the size of the heap. In the worst case, K can be on the order of N (e.g., for an array like `[1,1,1,2,2,2,3,3,3]`), leading to an overall complexity of O(N log N).
Space
O(N), where N is the length of `nums`. In the worst-case scenario (e.g., an array with all unique, non-consecutive numbers), the map will store N entries, and each priority queue will have one element.
Trade-offs
Pros
The greedy logic is relatively straightforward to conceptualize.
It correctly solves the problem by prioritizing the extension of shorter subsequences.
Cons
The time complexity is O(N log N), which is not optimal.
The space complexity is O(N), which can be improved.
This is a more efficient greedy approach that achieves linear time complexity. The strategy is based on a simple, powerful observation: for any number x, it is always better to append it to an existing subsequence ending at x-1 than to use it to start a new subsequence. Starting a new subsequence [x, x+1, x+2] immediately consumes future numbers that might be crucial for extending other, already existing subsequences.
To implement this, we use two hash maps. The first, freq, keeps track of the availability of each number. The second, append, keeps track of how many subsequences are 'needing' a particular number to be appended.
Algorithm
- First, create a frequency map,
freq, to store the counts of each number innums. - Second, create another map,
append, whereappend[x]will store the number of subsequences that have ended atx-1and are now looking forxto be appended. - Iterate through each number
numin thenumsarray. - If
freq.get(num) == 0, this number has already been used up by a preceding subsequence, so we can continue to the next number. - Greedy Choice 1: Extend an existing subsequence. Check if
append.get(num) > 0. If true, it means there is at least one subsequence waiting fornum. We use the currentnumto extend it. We decrementappend.get(num)and incrementappend.get(num + 1)because the subsequence now ends atnumand is waiting fornum + 1. - Greedy Choice 2: Start a new subsequence. If
append.get(num) == 0, we have no choice but to start a new subsequence withnum. For this to be valid, it must have a length of at least 3. So, we check ifnum + 1andnum + 2are available (i.e., their counts infreqare greater than 0). - If they are available, we form a new subsequence
[num, num + 1, num + 2]. We decrement the frequencies ofnum + 1andnum + 2in thefreqmap. We then incrementappend.get(num + 3)because this new subsequence is now waiting fornum + 3. - If
num + 1andnum + 2are not available, we cannot place the currentnum, so it's impossible to form a valid split. Returnfalse. - After processing a
num(either by extending or starting a new sequence), we must decrement its count infreq. - If the loop completes without returning
false, it means all numbers were successfully placed into valid subsequences. Returntrue.
Walkthrough
The algorithm makes two passes. The first pass populates the frequency map. The second pass iterates through the numbers and makes a greedy decision for each one.
import java.util.HashMap;import java.util.Map; class Solution { public boolean isPossible(int[] nums) { Map<Integer, Integer> freq = new HashMap<>(); for (int num : nums) { freq.put(num, freq.getOrDefault(num, 0) + 1); } Map<Integer, Integer> append = new HashMap<>(); for (int num : nums) { if (freq.get(num) == 0) { continue; // This number has been used } // Case 1: Append to an existing subsequence if (append.getOrDefault(num, 0) > 0) { append.put(num, append.get(num) - 1); append.put(num + 1, append.getOrDefault(num + 1, 0) + 1); } // Case 2: Start a new subsequence else if (freq.getOrDefault(num + 1, 0) > 0 && freq.getOrDefault(num + 2, 0) > 0) { freq.put(num + 1, freq.get(num + 1) - 1); freq.put(num + 2, freq.get(num + 2) - 1); append.put(num + 3, append.getOrDefault(num + 3, 0) + 1); } // Case 3: Impossible to place the number else { return false; } freq.put(num, freq.get(num) - 1); } return true; }}Complexity
Time
O(N), where N is the length of `nums`. The first loop to build the frequency map takes O(N). The second loop also iterates N times, and each step involves hash map operations which are O(1) on average. Thus, the total time complexity is linear.
Space
O(N), where N is the length of `nums`. In the worst case, if all numbers are distinct, the `freq` and `append` maps could store up to N entries.
Trade-offs
Pros
Achieves optimal O(N) time complexity.
The logic is a very clear and direct implementation of the greedy choice.
Generally easier to implement correctly than the O(1) space solution.
Cons
Requires O(N) extra space for the two hash maps, which is not optimal.
This is the most optimal approach, achieving linear time and constant space. It leverages the fact that the input array nums is sorted. Instead of using maps to track all subsequences, we can process the numbers sequentially and only maintain counts of how many subsequences of certain lengths end at the immediately preceding value. This is sufficient because to extend a subsequence with x, we only care about subsequences that end in x-1.
Algorithm
- Initialize three counters:
ones,twos, andlongsto 0. These will track the number of subsequences ending at the previous number (prev) that have lengths of 1, 2, and >=3, respectively. - Iterate through the
numsarray with an indexi. Since the array is sorted, we can process identical numbers in a single batch. - In each iteration, count the occurrences of the current number
nums[i], let's call itcount. - Handle Gaps: If the current number
nums[i]is notprev + 1(and it's not the first number), it means there is a gap in the sequence. If at this pointones > 0ortwos > 0, it means we have subsequences of length 1 or 2 that cannot be extended. This is an invalid state, so returnfalse. If there are no short subsequences, we can safely 'forget' the oldlongssequences and start fresh. We setones = count,twos = 0, andlongs = 0for the current number. - Handle Consecutive Numbers: If
nums[i] == prev + 1, we must extend the subsequences ending atprev. The number of availablenums[i](count) must be sufficient to extend all the short subsequences, i.e.,count >= ones + twos. If not, returnfalse. - We update the counts for the current number
nums[i]. Theonesfromprevbecometwos. Thetwosfromprevbecomelongs. The remainingcount - ones - twosofnums[i]are used first to extend thelongssequences, and any leftovers must start newonessequences. - Specifically, the new
twoscount is the oldonescount. The newlongscount is the oldtwoscount plusmin(old_longs, remaining_count). The newonescount ismax(0, remaining_count - old_longs). - Update
prevtonums[i]and advanceipast the current group of identical numbers. - After the loop finishes, we must check the state of the very last number processed. If
ones > 0ortwos > 0, it means we ended with subsequences that are too short. Returnfalse. - If all checks pass, return
true.
Walkthrough
We iterate through the array, but instead of one element at a time, we process groups of identical elements. We only need three variables, ones, twos, and longs, to store the number of subsequences ending at the previous value (prev) with lengths 1, 2, and >=3, respectively. This constant-size state is updated as we move from one number to the next consecutive one.
class Solution { public boolean isPossible(int[] nums) { // Counts of subsequences ending at prev with length 1, 2, and >=3 int ones = 0, twos = 0, longs = 0; int prev = Integer.MIN_VALUE; int i = 0; while (i < nums.length) { int start = i; int curr = nums[i]; while (i < nums.length && nums[i] == curr) { i++; } int count = i - start; if (prev != Integer.MIN_VALUE && curr != prev + 1) { // Gap in the sequence if (ones != 0 || twos != 0) { return false; // Short sequences cannot be extended } // Start new sequences ones = count; twos = 0; longs = 0; } else { // Consecutive sequence if (count < ones + twos) { return false; // Not enough numbers to extend short sequences } int remaining = count - ones - twos; int newLongs = twos + Math.min(longs, remaining); int newOnes = Math.max(0, remaining - longs); longs = newLongs; twos = ones; ones = newOnes; } prev = curr; } // After the loop, check if there are any uncompleted short sequences return ones == 0 && twos == 0; }}Complexity
Time
O(N), where N is the length of `nums`. Although there is a nested `while` loop, the outer loop pointer `i` is advanced by the inner loop. This means each element of the array is visited only a constant number of times, resulting in a single pass and linear time complexity.
Space
O(1). We only use a handful of variables (`ones`, `twos`, `longs`, `prev`, `i`, `count`, etc.) to maintain state, which does not depend on the size of the input array.
Trade-offs
Pros
Optimal O(N) time complexity.
Optimal O(1) space complexity.
Intelligently utilizes the sorted property of the input array to avoid extra data structures.
Cons
The logic is more intricate and less intuitive than the hash map approach, making it potentially harder to implement correctly.
Solutions
Solution
class Solution {public boolean isPossible(int[] nums) { Map<Integer, PriorityQueue<Integer>> d = new HashMap<>(); for (int v : nums) { if (d.containsKey(v - 1)) { var q = d.get(v - 1); d.computeIfAbsent(v, k->new PriorityQueue<>()).offer(q.poll() + 1); if (q.isEmpty()) { d.remove(v - 1); } } else { d.computeIfAbsent(v, k->new PriorityQueue<>()).offer(1); } } for (var v : d.values()) { if (v.peek() < 3) { return false; } } return true; }}class Solution {public: bool isPossible(vector<int> &nums) { unordered_map<int, priority_queue<int, vector<int>, greater<int>>> d; for (int v : nums) { if (d.count(v - 1)) { auto &q = d[v - 1]; d[v].push(q.top() + 1); q.pop(); if (q.empty()) { d.erase(v - 1); } } else { d[v].push(1); } } for (auto &[_, v] : d) { if (v.top() < 3) { return false; } } return true; }};class Solution: def isPossible(self, nums: List[int]) -> bool: d = defaultdict(list) for v in nums: if h: = d[v - 1]: heappush(d[v], heappop(h) + 1) else: heappush(d[v], 1) return all(not v or v and v[0] > 2 for v in d . values())Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.