1296. Divide Array in Sets of K Consecutive Numbers
Problem Description
You are given an array of integers nums
and a positive integer k
. Your task is to determine if it's possible to divide the entire array into groups where each group contains exactly k
consecutive numbers.
For example, if you have nums = [1,2,3,3,4,4,5,6]
and k = 4
, you can divide it into two groups: [1,2,3,4]
and [3,4,5,6]
, where each group has 4 consecutive numbers. Therefore, the answer would be true
.
The key requirements are:
- Each group must have exactly
k
numbers - The numbers in each group must be consecutive (like 1,2,3 or 5,6,7)
- Every number in the array must be used exactly once
- The order of numbers in the original array doesn't matter - you can rearrange them to form the groups
Return true
if such a division is possible, otherwise return false
.
Intuition
Think about this problem step by step. If we need to form groups of k
consecutive numbers, we should start from the smallest number available and try to build a group from there.
Why start from the smallest? Because if we have a smallest number x
in our array, it can only be the starting point of a consecutive sequence. It cannot be in the middle or end of any sequence since there's no smaller number to come before it.
Once we identify that x
must start a sequence, we know the sequence must be [x, x+1, x+2, ..., x+k-1]
. We need to check if all these numbers exist in sufficient quantities.
The key insight is that we can use a greedy approach: always form sequences starting from the smallest available number. This works because:
- Every number must belong to exactly one group
- The smallest unused number can only start a new group
- Once we commit to starting a group with number
x
, the rest of the group is determined:[x, x+1, ..., x+k-1]
To implement this, we need to:
- Count how many times each number appears (using a hash table/counter)
- Process numbers in sorted order
- When we encounter a number with count > 0, we try to form a complete group starting from it
- For each group formed, we decrement the count of all
k
consecutive numbers
If at any point we can't complete a group (because a required consecutive number has count 0), we know it's impossible to divide the array as required.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Check Divisibility
First, we check if len(nums) % k != 0
. If the total number of elements isn't divisible by k
, it's impossible to divide them into groups of size k
, so we return false
immediately.
Step 2: Count Frequencies
We use a hash table (Counter in Python) to count the occurrences of each number in the array. This is stored in cnt = Counter(nums)
. For example, if nums = [1,2,3,3,4,4,5,6]
, then cnt = {1:1, 2:1, 3:2, 4:2, 5:1, 6:1}
.
Step 3: Sort and Process
We sort the array nums
to process numbers in ascending order. This ensures we always start forming groups from the smallest available number.
Step 4: Form Consecutive Groups We iterate through the sorted array:
- For each number
x
, we check ifcnt[x] > 0
(meaning this number is still available to use) - If available, we try to form a group starting from
x
:[x, x+1, x+2, ..., x+k-1]
- For each number
y
in the range fromx
tox + k - 1
:- If
cnt[y] == 0
, it means we need numbery
to complete the group but it's not available, so we returnfalse
- Otherwise, we decrement
cnt[y]
by 1 to mark that we've used one occurrence of this number
- If
Step 5: Return Result
If we successfully process all numbers without returning false
, it means we can divide the array into groups of k
consecutive numbers, so we return true
.
Example Walkthrough:
For nums = [1,2,3,3,4,4,5,6]
and k = 4
:
- Length check: 8 % 4 = 0 โ
- Count:
{1:1, 2:1, 3:2, 4:2, 5:1, 6:1}
- Process sorted array:
[1,2,3,3,4,4,5,6]
- Start with 1 (cnt[1] = 1 > 0):
- Form group [1,2,3,4]
- Update counts:
{1:0, 2:0, 3:1, 4:1, 5:1, 6:1}
- Continue with 3 (cnt[3] = 1 > 0):
- Form group [3,4,5,6]
- Update counts:
{1:0, 2:0, 3:0, 4:0, 5:0, 6:0}
- All numbers used, return
true
The time complexity is O(n log n)
due to sorting, and space complexity is O(n)
for the hash table.
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 a concrete example with nums = [3,2,1,2,3,4,3,4,5,9,10,11]
and k = 3
.
Step 1: Initial Setup
- Array length: 12, and 12 % 3 = 0 โ (divisible by k, so we can proceed)
- Count frequencies:
{1:1, 2:2, 3:3, 4:2, 5:1, 9:1, 10:1, 11:1}
- Sort the array:
[1,2,2,3,3,3,4,4,5,9,10,11]
Step 2: Form First Group
- Start with smallest number 1 (cnt[1] = 1 > 0)
- Need to form group: [1, 2, 3]
- Check and decrement:
- cnt[1] = 1 โ 0 โ
- cnt[2] = 2 โ 1 โ
- cnt[3] = 3 โ 2 โ
- First group formed successfully
Step 3: Form Second Group
- Continue processing, next available is 2 (cnt[2] = 1 > 0)
- Need to form group: [2, 3, 4]
- Check and decrement:
- cnt[2] = 1 โ 0 โ
- cnt[3] = 2 โ 1 โ
- cnt[4] = 2 โ 1 โ
- Second group formed successfully
Step 4: Form Third Group
- Next available is 3 (cnt[3] = 1 > 0)
- Need to form group: [3, 4, 5]
- Check and decrement:
- cnt[3] = 1 โ 0 โ
- cnt[4] = 1 โ 0 โ
- cnt[5] = 1 โ 0 โ
- Third group formed successfully
Step 5: Form Fourth Group
- Next available is 9 (cnt[9] = 1 > 0)
- Need to form group: [9, 10, 11]
- Check and decrement:
- cnt[9] = 1 โ 0 โ
- cnt[10] = 1 โ 0 โ
- cnt[11] = 1 โ 0 โ
- Fourth group formed successfully
Result: All numbers used, all counts are 0. Return true
.
The four groups formed are: [1,2,3], [2,3,4], [3,4,5], [9,10,11]
Note how the greedy approach works: we always start from the smallest available number, which forces us to use specific consecutive numbers. This ensures we don't miss any valid grouping opportunity.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def isPossibleDivide(self, nums: List[int], k: int) -> bool:
6 # Check if total number of elements is divisible by k
7 # If not, we cannot form groups of size k
8 if len(nums) % k != 0:
9 return False
10
11 # Count frequency of each number in the array
12 frequency_map = Counter(nums)
13
14 # Process numbers in ascending order
15 for num in sorted(nums):
16 # Skip if current number has already been used in previous groups
17 if frequency_map[num] > 0:
18 # Try to form a consecutive group starting from current number
19 for consecutive_num in range(num, num + k):
20 # Check if we have enough of the consecutive number
21 if frequency_map[consecutive_num] == 0:
22 return False
23 # Use one occurrence of this number for the current group
24 frequency_map[consecutive_num] -= 1
25
26 # All numbers have been successfully grouped
27 return True
28
1class Solution {
2 public boolean isPossibleDivide(int[] nums, int k) {
3 // Check if total number of elements is divisible by k
4 // If not, we cannot form groups of size k
5 if (nums.length % k != 0) {
6 return false;
7 }
8
9 // Sort the array to process consecutive elements
10 Arrays.sort(nums);
11
12 // Create a frequency map to count occurrences of each number
13 Map<Integer, Integer> frequencyMap = new HashMap<>();
14 for (int num : nums) {
15 frequencyMap.merge(num, 1, Integer::sum);
16 }
17
18 // Iterate through sorted array to form consecutive groups
19 for (int num : nums) {
20 // Check if current number is still available (not used in previous groups)
21 if (frequencyMap.getOrDefault(num, 0) > 0) {
22 // Try to form a consecutive group starting from current number
23 // Group will be: [num, num+1, num+2, ..., num+k-1]
24 for (int consecutive = num; consecutive < num + k; consecutive++) {
25 // Decrement the count for this consecutive number
26 // merge() returns the new value after applying the function
27 if (frequencyMap.merge(consecutive, -1, Integer::sum) < 0) {
28 // If count becomes negative, the required number doesn't exist
29 return false;
30 }
31 }
32 }
33 }
34
35 // All groups formed successfully
36 return true;
37 }
38}
39
1class Solution {
2public:
3 bool isPossibleDivide(vector<int>& nums, int k) {
4 // Check if total number of elements is divisible by k
5 // If not, we cannot form groups of size k
6 if (nums.size() % k != 0) {
7 return false;
8 }
9
10 // Sort the array to process consecutive elements
11 sort(nums.begin(), nums.end());
12
13 // Count frequency of each number
14 unordered_map<int, int> frequencyMap;
15 for (int num : nums) {
16 frequencyMap[num]++;
17 }
18
19 // Iterate through sorted array to form consecutive groups
20 for (int num : nums) {
21 // Skip if current number has already been used in previous groups
22 if (frequencyMap.find(num) != frequencyMap.end()) {
23 // Try to form a group starting from current number
24 for (int consecutive = num; consecutive < num + k; consecutive++) {
25 // Check if the consecutive number exists
26 if (frequencyMap.find(consecutive) == frequencyMap.end()) {
27 return false; // Cannot form a valid group
28 }
29
30 // Decrement the count of the consecutive number
31 frequencyMap[consecutive]--;
32
33 // Remove from map if count becomes zero
34 if (frequencyMap[consecutive] == 0) {
35 frequencyMap.erase(consecutive);
36 }
37 }
38 }
39 }
40
41 // All numbers have been successfully grouped
42 return true;
43 }
44};
45
1/**
2 * Determines if an array can be divided into groups of k consecutive numbers
3 * @param nums - Array of integers to be divided
4 * @param k - Size of each group
5 * @returns true if the array can be divided into groups of k consecutive numbers, false otherwise
6 */
7function isPossibleDivide(nums: number[], k: number): boolean {
8 // Check if total number of elements is divisible by k
9 if (nums.length % k !== 0) {
10 return false;
11 }
12
13 // Create a frequency map to count occurrences of each number
14 const frequencyMap = new Map<number, number>();
15 for (const num of nums) {
16 frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
17 }
18
19 // Sort the array in ascending order
20 nums.sort((a, b) => a - b);
21
22 // Try to form consecutive groups starting from the smallest available number
23 for (const startNum of nums) {
24 // Skip if this number has already been used in previous groups
25 if (frequencyMap.get(startNum)! > 0) {
26 // Try to form a group of k consecutive numbers starting from startNum
27 for (let currentNum = startNum; currentNum < startNum + k; currentNum++) {
28 // Check if the current number is available
29 if ((frequencyMap.get(currentNum) || 0) === 0) {
30 return false;
31 }
32 // Decrement the count for the current number
33 frequencyMap.set(currentNum, frequencyMap.get(currentNum)! - 1);
34 }
35 }
36 }
37
38 // All numbers have been successfully grouped
39 return true;
40}
41
Time and Space Complexity
Time Complexity: O(n ร log n + n ร k)
The time complexity consists of two main parts:
sorted(nums)
takesO(n ร log n)
time wheren
is the length of the array- The nested loop iterates through each element in sorted order (
n
iterations), and for each element wherecnt[x] > 0
, it performsk
operations to check and decrement consecutive elements, resulting inO(n ร k)
in the worst case
Since the reference answer states O(n ร log n)
, this suggests that k
is treated as a constant or small value compared to n
. When k
is much smaller than n
, the sorting dominates and the complexity simplifies to O(n ร log n)
.
Space Complexity: O(n)
The space complexity comes from:
- The
Counter
objectcnt
which stores at mostn
key-value pairs (one for each unique element innums
), requiringO(n)
space - The sorted list created by
sorted(nums)
also requiresO(n)
space
Therefore, the overall space complexity is O(n)
, which matches the reference answer.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Inefficient Repeated Sorting
The current implementation sorts the entire array but then checks frequency_map[num] > 0
for each element. This means we're iterating through many numbers that have already been consumed in previous group formations, leading to unnecessary iterations.
Problem Example:
If nums = [1,1,1,2,2,2,3,3,3]
and k = 3
, after forming the first group [1,2,3]
, we still iterate through the remaining two 1's, two 2's, and two 3's in the sorted array, checking their frequencies each time.
Solution: Instead of sorting the original array, sort only the unique keys from the frequency map and process each unique number only once:
def isPossibleDivide(self, nums: List[int], k: int) -> bool:
if len(nums) % k != 0:
return False
frequency_map = Counter(nums)
# Sort only unique numbers instead of the entire array
for num in sorted(frequency_map.keys()):
count = frequency_map[num]
if count > 0:
# Form 'count' groups starting from this number
for consecutive_num in range(num, num + k):
if frequency_map[consecutive_num] < count:
return False
frequency_map[consecutive_num] -= count
return True
Pitfall 2: Not Handling Multiple Groups Starting from Same Number
When a number appears multiple times and serves as the starting point for multiple groups, the code processes each occurrence separately in the sorted array. This can be confusing and inefficient.
Problem Example:
If nums = [1,1,2,2,3,3]
and k = 3
, the number 1 appears twice and both occurrences need to start separate groups [1,2,3]
.
Better Approach: Process all groups starting from the same number together by using the frequency count:
def isPossibleDivide(self, nums: List[int], k: int) -> bool:
if len(nums) % k != 0:
return False
frequency_map = Counter(nums)
while frequency_map:
# Find the minimum number available
min_num = min(frequency_map.keys())
min_count = frequency_map[min_num]
# Form min_count groups starting from min_num
for i in range(k):
current_num = min_num + i
if current_num not in frequency_map or frequency_map[current_num] < min_count:
return False
frequency_map[current_num] -= min_count
if frequency_map[current_num] == 0:
del frequency_map[current_num]
return True
This approach processes all groups starting from the smallest available number at once, making the logic clearer and potentially more efficient for datasets with many duplicate values.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!