846. Hand of Straights
Problem Description
Alice has a collection of cards, where each card has a number written on it. She wants to organize these cards into groups following two specific rules:
- Each group must contain exactly
groupSize
cards - The cards in each group must form a consecutive sequence of numbers
You're given:
- An array
hand
wherehand[i]
represents the number on the i-th card - An integer
groupSize
representing the required size of each group
Your task is to determine if it's possible to rearrange all the cards into groups that satisfy both conditions. Return true
if such an arrangement is possible, or false
otherwise.
For example, if hand = [1,2,3,6,2,3,4,7,8]
and groupSize = 3
, we can form groups [1,2,3]
, [2,3,4]
, and [6,7,8]
, so the answer would be true
.
Note that:
- All cards must be used (no cards can be left out)
- Each card can only be used once
- The groups don't need to be sorted relative to each other, but within each group, the numbers must be consecutive
Intuition
The key insight is that when we form consecutive groups, the smallest number in our remaining cards must be the start of a new group. Why? Because if we have a card with value x
that's the smallest among all remaining cards, there's no way to include it in a group unless it's the first card of that group (since all cards before it have already been used or don't exist).
This leads us to a greedy approach: always try to form a group starting from the smallest available card.
Here's the thought process:
-
First, we need to check if it's even possible to divide all cards into groups of size
groupSize
. If the total number of cards isn't divisible bygroupSize
, we can immediately returnfalse
. -
We need to track how many of each card value we have, since duplicate values can appear. A hash table (Counter) is perfect for this.
-
By sorting the cards, we can process them from smallest to largest. When we encounter a card with count > 0, we know it must be the start of a new group.
-
Once we decide to start a group with card value
x
, we must have cardsx, x+1, x+2, ..., x+groupSize-1
available. We check if each of these consecutive values exists in sufficient quantity. If any is missing or insufficient, it's impossible to form valid groups. -
As we form each group, we decrement the count of each card used. This ensures we don't reuse cards and accurately track what's still available for future groups.
The beauty of this approach is that by always starting groups with the smallest available card, we never miss a valid grouping opportunity. If a valid arrangement exists, this greedy strategy will find it.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Check Divisibility
if len(hand) % groupSize:
return False
First, we check if the total number of cards is divisible by groupSize
. If not, it's impossible to form complete groups, so we return false
immediately.
Step 2: Count Card Frequencies
cnt = Counter(hand)
We use a hash table (Counter in Python) to count how many times each card value appears in the array. This allows us to track the availability of each card value as we form groups.
Step 3: Sort and Process Cards
for x in sorted(hand):
We sort the array to process cards from smallest to largest value. This ensures we always start forming groups with the smallest available card.
Step 4: Form Groups Starting from Each Card
if cnt[x]:
for y in range(x, x + groupSize):
if cnt[y] == 0:
return False
cnt[y] -= 1
For each card value x
in sorted order:
- We check if
cnt[x] > 0
. If it is, this card hasn't been fully used yet and should start a new group - We then try to form a complete group:
[x, x+1, x+2, ..., x+groupSize-1]
- For each required card
y
in this sequence:- If
cnt[y] == 0
, the required card doesn't exist or has been exhausted, making it impossible to complete this group, so we returnfalse
- Otherwise, we decrement
cnt[y]
by 1 to mark that we've used one instance of this card
- If
Step 5: Return Success
return True
If we successfully process all cards without encountering any impossible groups, we return true
.
Time Complexity: O(n log n)
where n
is the length of the hand array, dominated by the sorting operation.
Space Complexity: O(n)
for the hash table storing card counts.
The algorithm works because of the greedy property: when we encounter the smallest unused card, it must be the start of a new group. By always forming groups starting from the smallest available card, we ensure we don't miss any valid arrangement if one exists.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with hand = [1,2,3,6,2,3,4,7,8]
and groupSize = 3
.
Step 1: Check divisibility
- We have 9 cards total, and 9 % 3 = 0, so we can potentially form 3 groups.
Step 2: Count card frequencies
cnt = {1:1, 2:2, 3:2, 4:1, 6:1, 7:1, 8:1}
Step 3: Sort the hand
sorted_hand = [1,2,2,3,3,4,6,7,8]
Step 4: Process cards from smallest to largest
Processing card 1:
cnt[1] = 1 > 0
, so card 1 starts a new group- We need cards [1, 2, 3] for this group
- Check and use:
cnt[1]
(1โ0),cnt[2]
(2โ1),cnt[3]
(2โ1) - Group formed: [1,2,3]
- Updated counts:
{1:0, 2:1, 3:1, 4:1, 6:1, 7:1, 8:1}
Processing card 2 (first occurrence):
cnt[2] = 1 > 0
, so card 2 starts a new group- We need cards [2, 3, 4] for this group
- Check and use:
cnt[2]
(1โ0),cnt[3]
(1โ0),cnt[4]
(1โ0) - Group formed: [2,3,4]
- Updated counts:
{1:0, 2:0, 3:0, 4:0, 6:1, 7:1, 8:1}
Processing card 2 (second occurrence):
cnt[2] = 0
, skip (already used)
Processing cards 3, 3, 4:
- All have
cnt = 0
, skip (already used)
Processing card 6:
cnt[6] = 1 > 0
, so card 6 starts a new group- We need cards [6, 7, 8] for this group
- Check and use:
cnt[6]
(1โ0),cnt[7]
(1โ0),cnt[8]
(1โ0) - Group formed: [6,7,8]
- Updated counts:
{1:0, 2:0, 3:0, 4:0, 6:0, 7:0, 8:0}
Processing cards 7, 8:
- Both have
cnt = 0
, skip (already used)
Step 5: Return success
All cards have been successfully grouped into [1,2,3], [2,3,4], and [6,7,8]. Return true
.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
6 # Check if total cards can be evenly divided into groups
7 if len(hand) % groupSize != 0:
8 return False
9
10 # Count frequency of each card value
11 card_count = Counter(hand)
12
13 # Process cards in ascending order
14 for card_value in sorted(hand):
15 # Skip if this card has already been used in previous groups
16 if card_count[card_value] > 0:
17 # Try to form a consecutive group starting from current card
18 for next_card in range(card_value, card_value + groupSize):
19 # If we can't find a required consecutive card, return False
20 if card_count[next_card] == 0:
21 return False
22 # Use one instance of this card for the current group
23 card_count[next_card] -= 1
24
25 # All cards successfully grouped
26 return True
27
1class Solution {
2 public boolean isNStraightHand(int[] hand, int groupSize) {
3 // Check if total cards can be evenly divided into groups
4 if (hand.length % groupSize != 0) {
5 return false;
6 }
7
8 // Sort the hand array to process cards in ascending order
9 Arrays.sort(hand);
10
11 // Create frequency map to count occurrences of each card value
12 Map<Integer, Integer> frequencyMap = new HashMap<>();
13 for (int cardValue : hand) {
14 frequencyMap.merge(cardValue, 1, Integer::sum);
15 }
16
17 // Try to form consecutive groups starting from each card
18 for (int startCard : hand) {
19 // Only process if this card is still available
20 if (frequencyMap.getOrDefault(startCard, 0) > 0) {
21 // Try to form a group of consecutive cards starting from startCard
22 for (int currentCard = startCard; currentCard < startCard + groupSize; currentCard++) {
23 // Decrement the count for current card and check if it becomes negative
24 if (frequencyMap.merge(currentCard, -1, Integer::sum) < 0) {
25 // Not enough cards to form a valid group
26 return false;
27 }
28 }
29 }
30 }
31
32 // All cards successfully grouped
33 return true;
34 }
35}
36
1class Solution {
2public:
3 bool isNStraightHand(vector<int>& hand, int groupSize) {
4 // Check if total cards can be evenly divided into groups
5 if (hand.size() % groupSize != 0) {
6 return false;
7 }
8
9 // Sort the hand to process cards in ascending order
10 sort(hand.begin(), hand.end());
11
12 // Count frequency of each card value
13 unordered_map<int, int> cardCount;
14 for (int cardValue : hand) {
15 cardCount[cardValue]++;
16 }
17
18 // Try to form consecutive groups starting from each card
19 for (int startCard : hand) {
20 // Skip if this card has already been used in previous groups
21 if (cardCount.find(startCard) == cardCount.end()) {
22 continue;
23 }
24
25 // Try to form a group of consecutive cards starting from startCard
26 for (int currentCard = startCard; currentCard < startCard + groupSize; currentCard++) {
27 // Check if the required consecutive card exists
28 if (cardCount.find(currentCard) == cardCount.end()) {
29 return false;
30 }
31
32 // Use one instance of this card for the current group
33 cardCount[currentCard]--;
34
35 // Remove the card from map if all instances are used
36 if (cardCount[currentCard] == 0) {
37 cardCount.erase(currentCard);
38 }
39 }
40 }
41
42 return true;
43 }
44};
45
1/**
2 * Determines if an array of cards can be rearranged into groups of consecutive cards
3 * @param hand - Array of integers representing cards
4 * @param groupSize - Size of each group of consecutive cards
5 * @returns true if cards can be arranged into groups, false otherwise
6 */
7function isNStraightHand(hand: number[], groupSize: number): boolean {
8 // Check if total cards can be evenly divided into groups
9 if (hand.length % groupSize !== 0) {
10 return false;
11 }
12
13 // Create frequency map to count occurrences of each card
14 const cardCountMap = new Map<number, number>();
15 for (const card of hand) {
16 cardCountMap.set(card, (cardCountMap.get(card) || 0) + 1);
17 }
18
19 // Sort cards in ascending order to process from smallest
20 hand.sort((a, b) => a - b);
21
22 // Try to form consecutive groups starting from each card
23 for (const startCard of hand) {
24 // Only process if this card is still available
25 if (cardCountMap.get(startCard)! > 0) {
26 // Try to form a group of consecutive cards starting from startCard
27 for (let currentCard = startCard; currentCard < startCard + groupSize; currentCard++) {
28 // Check if the required consecutive card exists
29 if ((cardCountMap.get(currentCard) || 0) === 0) {
30 return false;
31 }
32 // Use one instance of the current card for this group
33 cardCountMap.set(currentCard, cardCountMap.get(currentCard)! - 1);
34 }
35 }
36 }
37
38 // All cards successfully arranged into groups
39 return true;
40}
41
Time and Space Complexity
Time Complexity: O(n ร log n)
, where n
is the length of the array hand
.
The time complexity is dominated by the sorting operation sorted(hand)
, which takes O(n ร log n)
time. After sorting, we iterate through each element in the sorted array once, which takes O(n)
time. For each element where cnt[x] > 0
, we perform a loop of size groupSize
to check and decrement consecutive elements. In the worst case, we process each element once across all groups, so the total work done in the iteration is O(n)
. Therefore, the overall time complexity is O(n ร log n) + O(n) = O(n ร log n)
.
Space Complexity: O(n)
, where n
is the length of the array hand
.
The space complexity comes from two sources: the Counter
object cnt
which stores at most n
key-value pairs (in the worst case where all elements are unique), requiring O(n)
space, and the sorted array created by sorted(hand)
, which also requires O(n)
space. Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Inefficient Repeated Sorting
The current implementation sorts the entire hand
array but then iterates through it, checking each card even if it's already been used in a group. This leads to unnecessary iterations.
Problem Example:
If hand = [1,1,1,2,2,2,3,3,3]
and groupSize = 3
, after forming the first group [1,2,3]
, we still check the remaining two 1
s in the sorted array, even though their count is already 0.
Solution: Instead of sorting the entire array, sort only the unique card values:
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
card_count = Counter(hand)
# Sort only unique values, not the entire array
for card_value in sorted(card_count.keys()):
while card_count[card_value] > 0:
for next_card in range(card_value, card_value + groupSize):
if card_count[next_card] == 0:
return False
card_count[next_card] -= 1
return True
Pitfall 2: Not Handling Multiple Groups Starting with Same Value
A subtle issue can arise when multiple groups need to start with the same card value. The algorithm must properly handle forming all such groups.
Problem Example:
If hand = [1,1,2,2,3,3]
and groupSize = 3
, we need to form two groups: [1,2,3]
and [1,2,3]
. The algorithm must correctly decrement counts for all required groups.
Solution: Use a while loop to form all groups starting with the current minimum value:
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
card_count = Counter(hand)
while card_count:
# Find the minimum card value still available
min_card = min(card_count.keys())
# Form a group starting from min_card
for card in range(min_card, min_card + groupSize):
if card not in card_count:
return False
card_count[card] -= 1
if card_count[card] == 0:
del card_count[card]
return True
Pitfall 3: Using a Min-Heap Without Proper Cleanup
Some implementations use a min-heap to track the smallest available card, but forget to handle cards with zero count:
Incorrect Approach:
import heapq
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
card_count = Counter(hand)
min_heap = list(card_count.keys())
heapq.heapify(min_heap)
while min_heap:
first = min_heap[0] # Bug: might have count 0
for card in range(first, first + groupSize):
if card_count[card] == 0:
return False
card_count[card] -= 1
return True
Correct Approach:
import heapq
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
card_count = Counter(hand)
min_heap = list(card_count.keys())
heapq.heapify(min_heap)
while min_heap:
# Keep popping until we find a card with non-zero count
while min_heap and card_count[min_heap[0]] == 0:
heapq.heappop(min_heap)
if not min_heap:
break
first = min_heap[0]
for card in range(first, first + groupSize):
if card_count[card] == 0:
return False
card_count[card] -= 1
return True
Depth first search is equivalent to which of the tree traversal order?
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!