659. Split Array into Consecutive Subsequences
Problem Description
You're given a sorted integer array nums
with non-decreasing elements. Your task is to check whether you can split the array into one or more subsequences where each subsequence is a strictly increasing consecutive sequence (each number is exactly one more than the previous one), and each subsequence must contain at least three numbers. If it's possible to do such a split, you should return true
, otherwise return false
.
Consider the array [1,2,3,3,4,5]
, it can be split into two subsequences [1,2,3]
and [3,4,5]
which satisfy the conditions above, so the output is true
.
Intuition
The intuition behind the solution is to use a greedy approach. We try to build all possible subsequences by iterating through the nums
array, and for each element, we want to extend a subsequence that ends at the element before the current one (this would make it a consecutive sequence). If such a subsequence does not exist, we'll start a new subsequence.
To make this efficient, we use a hashmap to store the ends of the subsequences, mapping the last element of a subsequence to a min-heap of lengths of all subsequences ending with that element. Min-heaps are used because we want to extend the shortest subsequence available, which minimizes the chance of not being able to create valid subsequences as we add more elements.
If at any point we can extend a subsequence, we'll pop the length of the shortest subsequence from the min-heap of the predecessor of the current element, increment the length (since we're adding the current element to it), and push the new length to the current element's min-heap. If there is no subsequence to extend, we start a new one by pushing a length of 1
to the current element's min-heap.
In the end, we go through the hashmap and check if any subsequence lengths are less than 3
. If we find any, that means we have an invalid subsequence, and we return false
. If all lengths are 3
or greater, we return true
as we've successfully created valid subsequences.
Thus, the key to this approach is maintaining and updating the min-heaps efficiently as we move through the array.
Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The implementation uses a hashmap (defaultdict
from the collections
module in Python) paired with a min-heap (from the heapq
module) for each unique key. These data structures are crucial as they store subsequences' lengths efficiently and allow for quick retrieval and update operations.
Here's the approach broken down into steps:
-
Iterate over the sorted
nums
array:- For each number
v
innums
, check if there is a subsequence that ended just beforev
(i.e., atv - 1
).
- For each number
-
If such a subsequence exists (
if h := d[v - 1]:
):- Pop the length of the shortest such subsequence from the min-heap at
v - 1
(usingheappop(h)
). - Increment this length by
1
, sincev
will extend this subsequence (heappop(h) + 1
). - Push the new length to the min-heap for
v
to keep track of this extended subsequence (heappush(d[v], heappop(h) + 1)
).
- Pop the length of the shortest such subsequence from the min-heap at
-
If no such subsequence exists (
else:
):- Initialize a new subsequence with a single element
v
by pushing1
to the min-heap forv
(heappush(d[v], 1)
).
- Initialize a new subsequence with a single element
At each step, subsequences are either extended or started, and the d
hashmap tracks these actions efficiently using min-heaps.
After processing all the elements in nums
, check if any subsequence is invalid (less than 3 elements). The final check (return all(not v or v and v[0] > 2 for v in d.values())
) iterates over all the min-heaps stored in the hashmap:
not v
checks if the min-heap is empty, which would mean no invalid subsequence was found for that key, so aTrue
is implied.v and v[0] > 2
checks if the min-heap is not empty and the length of the shortest subsequence is greater than2
for that particular element, implying a valid subsequence.
If all min-heaps validate the condition (meaning all subsequences are of length 3
or more), True
is returned, otherwise False
.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a smaller example to illustrate the solution approach using the array [1, 2, 3, 4, 4, 5]
.
Following the steps outlined in the solution approach:
-
We start by iterating through the sorted array
nums = [1, 2, 3, 4, 4, 5]
. -
For the first element
1
, there is no previous subsequence, so we initiate a subsequence by pushing1
into min-heap for1
:d[1] = [1]
. -
Next element
2
, there is a subsequence ending at1
so we take its length1
, increment by one, and push2
into min-heap for2
:d[2] = [2]
. -
For the next element
3
, there is a subsequence ending at2
, so again we take its length2
, increment it by one, and push3
into min-heap for3
:d[3] = [3]
. -
Now we encounter the first
4
. A subsequence ends at3
, so we take the length3
, increment it, and push4
to the min-heap for4
:d[4] = [4]
. -
The next element is
4
again. Since we cannot extend the subsequence ending in3
anymore (as it's already been extended), we start a new subsequence with4
:d[4] = [4, 1]
. -
The final element
5
has the potential to extend the subsequence ending with the second4
. We pop1
, increment it to2
, and push it to min-heap for5
:d[5] = [2]
.
The hash map d
now looks like this: {1: [1], 2: [2], 3: [3], 4: [4, 1], 5: [2]}
.
- Looking into each min-heap, we see the following lengths:
- For
1
, the subsequence length is1
, which is less than3
. This is a problem, as we need all subsequences to be at least length3
. - For
4
, one of the subsequences has a length1
, which is also less than3
. - For
5
, the subsequence length is2
, which is less than3
.
- For
These insights infer that there are subsequences that do not meet the requirement. Therefore, the output for the array [1, 2, 3, 4, 4, 5]
would be False
since not all subsequences contain at least three numbers.
This example demonstrates the process of iterating through the numbers, trying to extend existing subsequences using a min-heap and a hashmap, and finally verifying that all subsequences have met the minimum length requirement.
Solution Implementation
1from collections import defaultdict
2import heapq
3
4class Solution:
5 def isPossible(self, nums: List[int]) -> bool:
6 # Create a dictionary to map numbers to a list of sequence lengths
7 sequence_lengths = defaultdict(list)
8
9 # Iterate through all numbers in the provided list
10 for num in nums:
11 # If there exists a sequence ending with a number one less than the current number
12 if sequence_lengths[num - 1]:
13 # Pop the shortest sequence that ends with num - 1
14 shortest_sequence = heapq.heappop(sequence_lengths[num - 1])
15 # Add the current number to this sequence (incrementing its length)
16 heapq.heappush(sequence_lengths[num], shortest_sequence + 1)
17 else:
18 # Start a new sequence with the current number (with length 1)
19 heapq.heappush(sequence_lengths[num], 1)
20
21 # Check all the (non-empty) sequences in the dictionary
22 return all(len(sequence) == 0 or (len(sequence) > 0 and sequence[0] > 2) for sequence in sequence_lengths.values())
23
1import java.util.HashMap;
2import java.util.Map;
3import java.util.PriorityQueue;
4
5class Solution {
6 public boolean isPossible(int[] nums) {
7 // HashMap to store the end element of each subsequence and a priority queue of their lengths
8 Map<Integer, PriorityQueue<Integer>> seqEndMap = new HashMap<>();
9
10 // Iterate over each number in the input array
11 for (int value : nums) {
12 // If there's a sequence to append to, which ends with the current value - 1
13 if (seqEndMap.containsKey(value - 1)) {
14 // Get the priority queue of subsequences that end with value - 1
15 PriorityQueue<Integer> lengths = seqEndMap.get(value - 1);
16 // Remove the shortest subsequence from the queue and increment length
17 int length = lengths.poll() + 1;
18 // If the current priority queue becomes empty after removal, remove the mapping
19 if (lengths.isEmpty()) {
20 seqEndMap.remove(value - 1);
21 }
22 // Append the incremented length to the current value's priority queue
23 seqEndMap.computeIfAbsent(value, k -> new PriorityQueue<>()).offer(length);
24 } else {
25 // No preceding subsequences, start a new one with length 1
26 seqEndMap.computeIfAbsent(value, k -> new PriorityQueue<>()).offer(1);
27 }
28 }
29
30 // Validate the subsequences' lengths
31 for (PriorityQueue<Integer> lengths : seqEndMap.values()) {
32 // If the shortest subsequence is less than 3, fail the check
33 if (lengths.peek() < 3) {
34 return false;
35 }
36 }
37
38 // All subsequences have a length >= 3, the split is possible
39 return true;
40 }
41}
42
1#include <vector>
2#include <unordered_map>
3#include <queue>
4
5class Solution {
6public:
7 // Function to assess if it is possible to split the given sequence
8 bool isPossible(vector<int>& nums) {
9 // A map that associates each number with a min heap that represents the different possible sequence lengths ending with that number
10 unordered_map<int, priority_queue<int, vector<int>, greater<int>>> lengthsMap;
11
12 // Iterate over each number in the provided vector
13 for (int value : nums) {
14 // If there's a sequence ending with the previous number (value - 1)
15 if (lengthsMap.count(value - 1)) {
16 // Get the min heap (priority queue) for sequences ending with value - 1
17 auto& prevQueue = lengthsMap[value - 1];
18 // Add the current value to extend a sequence, increasing its length by 1
19 lengthsMap[value].push(prevQueue.top() + 1);
20 // Pop the extended sequence length from the min heap of the previous value
21 prevQueue.pop();
22 // If there are no more sequences ending with this previous value, remove it from the map
23 if (prevQueue.empty()) {
24 lengthsMap.erase(value - 1);
25 }
26 } else {
27 // If there is no previous sequence to extend, start a new sequence with length 1
28 lengthsMap[value].push(1);
29 }
30 }
31
32 // Check all sequences to ensure they all have a length of at least 3
33 for (auto& entry : lengthsMap) {
34 auto& sequenceLengths = entry.second;
35 // If the smallest length is less than 3, return false
36 if (sequenceLengths.top() < 3) {
37 return false;
38 }
39 }
40
41 // All sequences have the required minimum length, so return true
42 return true;
43 }
44};
45
1// TypeScript has no built-in priority queue, but we can use an array and sort it.
2type PriorityQueue = number[];
3
4// Inserts an element into the priority queue (min-heap)
5function insert(q: PriorityQueue, num: number) {
6 q.push(num);
7 q.sort((a, b) => a - b); // Sort to ensure the smallest element is at the front
8}
9
10// Pops the smallest element from the priority queue
11function pop(q: PriorityQueue): number {
12 return q.shift(); // Remove and return the first element which is the smallest
13}
14
15// Function to determine if it is possible to split the sequence into consecutive subsequences of length at least 3
16function isPossible(nums: number[]): boolean {
17 // Map that links each number to a priority queue representing possible sequence lengths ending with that number
18 const lengthsMap: Record<number, PriorityQueue> = {};
19
20 // Iterate over each number in the array
21 for (const value of nums) {
22 const prevValue = value - 1;
23
24 // If there's a sequence ending with the previous number (value - 1)
25 if (lengthsMap[prevValue]) {
26 const prevQueue = lengthsMap[prevValue]; // Get the priority queue for sequences ending with prevValue
27 const smallestLength = pop(prevQueue); // Get and remove the smallest sequence length
28 insert(lengthsMap[value] || (lengthsMap[value] = []), smallestLength + 1); // Extend the sequence length by 1 and add to the queue for current value
29
30 // If the priority queue for prevValue is empty, remove it from the map
31 if (prevQueue.length === 0) {
32 delete lengthsMap[prevValue];
33 }
34 } else {
35 // If there's no previous sequence to extend, start a new sequence with length 1
36 insert(lengthsMap[value] || (lengthsMap[value] = []), 1);
37 }
38 }
39
40 // Check that all sequences have a length of at least 3
41 for (const sequences of Object.values(lengthsMap)) {
42 if (sequences[0] < 3) { // Since the array is sorted, the smallest length is at the front
43 return false; // A sequence is shorter than 3 so it's not possible to split
44 }
45 }
46
47 // All sequences have the required minimum length of 3
48 return true;
49}
50
Time and Space Complexity
The given Python code performs an operation on a list of integers nums
to determine if it is possible to split nums
into consecutive subsequences of length at least 3.
Time Complexity:
The time complexity of the code is determined by:
- The iteration through each number in the list (
for v in nums
), which takesO(n)
time wheren
is the length ofnums
. - The heap operations (heappush and heappop) which are
O(log(k))
wherek
is the number of entries in the heap at any time.
For each number in nums
, the code might do a heappush and a heappop, each of which can take up to O(log(k))
time. Note that k
can potentially be as large as n
in the worst case when there are many different sequences being formed. However, since it's observed that each number in the array can only belong to one subsequence and hence k
is related to the number of possible subsequences that can be formed which is actually smaller than n
.
The final iteration to check if each subsequence has a length of more than 2 takes O(m)
where m
is the number of distinct elements in nums
.
Putting this all together, the overall time complexity is O(n log(k))
with the note that k
tends to be smaller than n
and is related to the distribution of the input numbers.
Space Complexity:
The space complexity of the code is given by:
- The extra space used by the dictionary
d
which holds a heap for each possible terminal value of a subsequence, leading toO(m)
wherem
is the number of distinct elements innums
. - The heaps themselves, which together contain all
n
numbers, givingO(n)
.
Since both the dictionary and the heaps need to be stored, their space complexities are additive, leading to a total space complexity of O(n + m)
. In the worst case, each element is unique so m
could be as large as n
, making the space complexity O(n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
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 algomonster s3 us east 2 amazonaws com 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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!