846. Hand of Straights - Explanation
Description
You are given an integer array hand where hand[i] is the value written on the ith card and an integer groupSize.
You want to rearrange the cards into groups so that each group is of size groupSize, and card values are consecutively increasing by 1.
Return true if it's possible to rearrange the cards in this way, otherwise, return false.
Example 1:
Input: hand = [1,2,4,2,3,5,3,4], groupSize = 4
Output: trueExplanation: The cards can be rearranged as [1,2,3,4] and [2,3,4,5].
Example 2:
Input: hand = [1,2,3,3,4,5,6,7], groupSize = 4
Output: falseExplanation: The closest we can get is [1,2,3,4] and [3,5,6,7], but the cards in the second group are not consecutive.
Constraints:
1 <= hand.length <= 10000 <= hand[i] <= 10001 <= groupSize <= hand.length
Topics
Recommended Time & Space Complexity
You should aim for a solution as good or better than O(nlogn) time and O(n) space, where n is the size of the input array.
Hint 1
It is observed that to form a group, the minimum value should be the starting value of the group. Additionally, the minimum value in the array serves as the starting value for one or more groups based on its frequency. Can you think of an efficient way to determine the frequencies of array elements? Maybe a specific data structure can be useful here.
Hint 2
We can use a hash map to store the elements along with their frequencies. Additionally, we sort the given array. Then, we iterate through the sorted array and try to form groups by decrementing the frequency count. If we fail to form a group at any step, we immediately return false. Can you think why this works?
Hint 3
Sorting ensures we start with the smallest available value, while the hash map helps track element availability using frequency counts. At each step, we pick the smallest available value x and attempt to form a group from x to x + groupSize - 1. If all elements are present based on their frequency counts, we decrement their counts as we iterate. If we successfully form all groups, we return true; otherwise, we return false.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Hash Maps - Using frequency maps to count occurrences and track available elements
- Sorting - Processing elements in sorted order to greedily form consecutive groups
- Greedy Algorithms - Always starting groups from the smallest available element to ensure valid consecutive sequences
- Heaps (Priority Queues) - Efficiently retrieving and removing the minimum element when forming groups
1. Sorting
Intuition
We are given a hand of cards and a group size. The goal is to check whether we can divide all cards into groups of size groupSize such that:
- each group consists of consecutive numbers
- every card is used exactly once
A simple and intuitive way to approach this is:
- always try to form groups starting from the smallest available card
- once we start a group at a number
x, we must also havex + 1,x + 2, ...,x + groupSize - 1
Sorting the hand helps because it ensures we always process cards in increasing order, which naturally enforces the consecutive requirement.
A frequency map count helps us track how many times each card is still available.
Algorithm
- If the total number of cards is not divisible by
groupSize:- return
falseimmediately (grouping is impossible)
- return
- Count the frequency of each card value using a map.
- Sort the hand in increasing order.
- Iterate through each card value in the sorted hand:
- If the current card is already used up (its count is
0), skip it - Otherwise, try to form a group starting from this card
- If the current card is already used up (its count is
- To form a group starting at
num:- For every value from
numtonum + groupSize - 1:- If that value does not exist in the count map, return
false - Otherwise, decrement its count by
1
- If that value does not exist in the count map, return
- For every value from
- If all cards are successfully grouped without failure:
- return
true
- return
class Solution:
def isNStraightHand(self, hand, groupSize):
if len(hand) % groupSize:
return False
count = Counter(hand)
hand.sort()
for num in hand:
if count[num]:
for i in range(num, num + groupSize):
if not count[i]:
return False
count[i] -= 1
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
2. Heap
Intuition
We need to split the cards into groups of size groupSize, where each group is made of consecutive numbers, and every card is used exactly once.
A good strategy is to always start building a group from the smallest card value that is still available.
If we can always extend that smallest value into a full consecutive group, then the hand is valid.
To do this efficiently:
- we store frequencies of each card in a map
count - we keep a min-heap of the available card values so we can always get the current smallest value quickly
When we start a group from first (the smallest value in the heap), we must use:first, first+1, ..., first+groupSize-1
If at any point a required value is missing, grouping is impossible.
The heap also helps ensure we remove values in the correct order when their count reaches zero.
Algorithm
- If the total number of cards is not divisible by
groupSize, returnfalse. - Build a frequency map
countwherecount[x]is how many times cardxappears. - Put all distinct card values into a min-heap
minH. - While the heap is not empty:
- Let
firstbe the smallest available value (minH[0]) - Try to build one group starting at
first
- Let
- For every value
ifromfirsttofirst + groupSize - 1:- If
iis not incount, returnfalse(missing a needed card) - Decrease
count[i]by1because we use one card of valuei - If
count[i]becomes0:- it must be the smallest value currently in the heap
- otherwise, we are trying to remove numbers out of order, which means grouping breaks
- pop it from the heap
- If
- If we process all groups successfully, return
true
class Solution:
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize:
return False
count = {}
for n in hand:
count[n] = 1 + count.get(n, 0)
minH = list(count.keys())
heapq.heapify(minH)
while minH:
first = minH[0]
for i in range(first, first + groupSize):
if i not in count:
return False
count[i] -= 1
if count[i] == 0:
if i != minH[0]:
return False
heapq.heappop(minH)
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
3. Ordered Map
Intuition
We want to check if the cards can be divided into groups of size groupSize, where each group consists of consecutive numbers and every card is used exactly once.
Instead of explicitly forming each group, we can think in terms of how many groups are currently “open” and waiting to be extended.
As we process card values in increasing order:
- some groups may already be open and expect the current number next
- the current number can either:
- extend existing open groups
- or start new groups
The key idea is:
- if there are
open_groupswaiting for the current number, we must have at least that many cards of the current value - any extra cards beyond extending existing groups will start new groups
- each group must close exactly after
groupSizeconsecutive numbers
A queue is used to remember how many new groups were started at each step, so we can close them after groupSize steps.
Algorithm
- If the total number of cards is not divisible by
groupSize, returnfalse. - Count the frequency of each card using a map.
- Iterate over the card values in sorted order.
- Maintain:
open_groups: number of groups currently waiting to be extendedlast_num: previous card value processed- a queue
qto track how many groups were started at each value
- For each card value
num:- If there are open groups and
numis not consecutive tolast_num, returnfalse - If
open_groups > count[num], returnfalse(not enough cards to extend groups)
- If there are open groups and
- Calculate how many new groups start at
num:new_groups = count[num] - open_groups- Push
new_groupsinto the queue
- Update:
last_num = numopen_groups = count[num]
- If the queue size reaches
groupSize:- pop from the queue and subtract that value from
open_groups - this closes groups that have reached length
groupSize
- pop from the queue and subtract that value from
- After processing all numbers:
- return
trueonly ifopen_groups == 0
- return
class Solution:
def isNStraightHand(self, hand, groupSize):
if len(hand) % groupSize != 0:
return False
count = Counter(hand)
q = deque()
last_num, open_groups = -1, 0
for num in sorted(count):
if ((open_groups > 0 and num > last_num + 1) or
open_groups > count[num]
):
return False
q.append(count[num] - open_groups)
last_num = num
open_groups = count[num]
if len(q) == groupSize:
open_groups -= q.popleft()
return open_groups == 0Time & Space Complexity
- Time complexity:
- Space complexity:
4. Hash Map
Intuition
We need to split the hand into groups of size groupSize, where each group is made of consecutive numbers and every card is used exactly once.
This approach uses only a frequency map and a simple rule:
- Any valid group must start at the beginning of a consecutive run
- So for a given number
num, we first walk left to find the earliest possible start of its run
(we keep moving left whilestart - 1still exists in the hand) - Once we know a possible start, we try to repeatedly form consecutive groups starting from that start:
- while there are still cards at
start, we build a group:start,start+1, ...,start+groupSize-1 - decrement counts as we use cards
- while there are still cards at
By always forming groups from the earliest start in a run, we avoid skipping needed smaller cards and ensure groups remain consecutive.
Algorithm
- If
len(hand)is not divisible bygroupSize, returnfalse. - Build a frequency map
countfor all card values. - For each card value
numin the hand:- Find the earliest start of the consecutive run containing
num:- set
start = num - while
count[start - 1] > 0, decrementstart
- set
- Find the earliest start of the consecutive run containing
- For every possible
startup tonum:- While
count[start] > 0:- Try to create one full group starting at
start - For each
ifromstarttostart + groupSize - 1:- If
count[i] == 0, returnfalse(missing a needed card) - Otherwise decrement
count[i]
- If
- Try to create one full group starting at
- Move
startforward by1and continue
- While
- If all groups are formed successfully, return
true.
class Solution:
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
count = Counter(hand)
for num in hand:
start = num
while count[start - 1]:
start -= 1
while start <= num:
while count[start]:
for i in range(start, start + groupSize):
if not count[i]:
return False
count[i] -= 1
start += 1
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Forgetting to Check Divisibility First
If the total number of cards is not divisible by groupSize, it is impossible to form complete groups. Skipping this initial check wastes time processing the array and may lead to subtle bugs where the algorithm appears to succeed but produces an invalid grouping.
Not Starting Groups from the Smallest Available Card
Groups must be formed starting from the smallest available card value to ensure consecutive sequences work correctly. Starting from an arbitrary card can leave smaller cards stranded without enough consecutive neighbors to form a valid group. Always process cards in sorted order or use a min-heap to find the smallest available value.
Failing to Decrement Counts Properly
When forming a group, each card in the consecutive sequence must have its count decremented. A common bug is forgetting to decrement or decrementing the wrong key, which causes cards to be reused or leaves cards unused. Additionally, when a card's count reaches zero, it must be handled correctly to avoid checking for cards that no longer exist.
Sign in to join the discussion