Skip to content

846. Hand of Straights (Medium)

Problem

Given an array hand of integers (each a card value) and an integer groupSize, return true if the cards can be rearranged into groups each of which is a run of groupSize consecutive values.

Example

  • hand = [1,2,3,6,2,3,4,7,8], groupSize = 3true ([1,2,3], [2,3,4], [6,7,8])
  • hand = [1,2,3,4,5], groupSize = 4false

LeetCode 846 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, try every partitioning

Recursively pick group_size indices, check they form a consecutive run, then recurse on the rest.

from itertools import combinations
def is_n_straight_hand(hand, group_size):
if len(hand) % group_size != 0:
return False
if not hand:
return True
n = len(hand)
for indices in combinations(range(n), group_size): # L1: C(n, k) groups to try
group = sorted(hand[i] for i in indices)
if all(group[i] - group[i - 1] == 1 for i in range(1, len(group))):
remaining = [hand[i] for i in range(n) if i not in set(indices)]
if is_n_straight_hand(remaining, group_size): # L2: recurse on rest
return True
return False

Each level tries C(n, k) groups; the recursion has n/k levels. Total combinatorial blow-up. Skip.

Complexity

  • Time: exponential.
  • Space: O(n)O(n) recursion.

Approach 2: Sort + per-smallest consumption (canonical greedy)

Count occurrences. Repeatedly take the smallest remaining value x; it must start a run of x, x+1, ..., x + groupSize - 1, remove one of each. If at any point you can’t, return false.

from collections import Counter
def is_n_straight_hand(hand, group_size):
if len(hand) % group_size != 0: # L1: O(1) quick reject
return False
counts = Counter(hand) # L2: O(n)
for x in sorted(counts): # L3: O(u log u) where u = distinct values
c = counts[x] # L4: O(1)
if c == 0:
continue
for k in range(group_size): # L5: O(group_size) per distinct value
if counts[x + k] < c: # L6: O(1)
return False
counts[x + k] -= c # L7: O(1)
return True

Where the time goes, line by line

Variables: n = len(hand), u = number of distinct card values, k = group_size.

LinePer-call costTimes executedContribution
L2 (Counter)O(1)O(1)nO(n)O(n)
L3 (sorted iteration)O(ulogu)O(u log u)1O(nlogn)O(n log n) ← dominates
L5-L7 (group consumption)O(groupsize)O(group_size)u timesO(nk)O(n · k)

Sorting the distinct values is O(ulogu)O(u log u) where u ≤ n, so O(nlogn)O(n log n). The inner consumption loop does O(groupsize)O(group_size) work per distinct value; total work across all distinct values is bounded by O(ngroupsize)O(n · group_size).

Complexity

  • Time: O(nlogn+nk)O(n log n + n · k), driven by L3 (sort) and L5/L6/L7 (group consumption).
  • Space: O(n)O(n) for the Counter.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Min-heap + lazy consumption

Min-heap of remaining values; pop smallest; consume one of each of the next k values.

import heapq
from collections import Counter
def is_n_straight_hand_heap(hand, group_size):
if len(hand) % group_size != 0: # L1: O(1)
return False
counts = Counter(hand) # L2: O(n)
heap = list(counts) # L3: O(u)
heapq.heapify(heap) # L4: O(u) Floyd's
while heap: # L5: outer loop
x = heap[0] # L6: O(1) peek min
if counts[x] == 0:
heapq.heappop(heap) # L7: O(log u) pop exhausted
continue
for k in range(group_size): # L8: O(group_size) per group start
if counts[x + k] == 0:
return False
counts[x + k] -= 1 # L9: O(1)
while heap and counts[heap[0]] == 0:
heapq.heappop(heap) # L10: O(log u) cleanup
return True

Where the time goes, line by line

Variables: n = len(hand), u = number of distinct card values, k = group_size.

LinePer-call costTimes executedContribution
L2 (Counter)O(1)O(1)nO(n)O(n)
L4 (heapify)O(u)O(u)1O(n)O(n)
L7, L10 (heap pops)O(logu)O(log u)up to uO(nlogn)O(n log n) ← dominates
L8-L9 (consumption)O(groupsize)O(group_size)n/group_size groupsO(nk)O(n · k)

Heap pops cost O(logu)O(log u) and happen at most u times; consumption loops cost O(groupsize)O(group_size) per group, totaling O(ngroupsize/groupsize)O(n · group_size / group_size) = O(n)O(n) consumption steps.

Complexity

  • Time: O(nlogn+nk)O(n log n + n · k), same as Approach 2.
  • Space: O(n)O(n) for Counter and heap.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
Enumerate partitionsexponentialO(n)O(n)
Sort + per-smallestO(nlogn+nk)O(n log n + n · k)O(n)O(n)
Min-heap + lazyO(nlogn+nk)O(n log n + n · k)O(n)O(n)

The greedy choice, always start the next group from the smallest remaining value, is forced: if the smallest value can’t start a group, no group can contain it, so the answer is false.

Test cases

func isNStraightHand(hand []int, groupSize int) bool {
if len(hand)%groupSize != 0 { return false }
counts := make(map[int]int)
for _, v := range hand { counts[v]++ }
keys := make([]int, 0, len(counts))
for k := range counts { keys = append(keys, k) }
sort.Ints(keys)
for _, x := range keys {
c := counts[x]
if c == 0 { continue }
for k := 0; k < groupSize; k++ {
if counts[x+k] < c { return false }
counts[x+k] -= c
}
}
return true
}
  • Greedy Exchange Arguments, proof tactics for showing that a greedy choice can be swapped into an optimal solution without making it worse.
  • Sorting as Preprocessing, order-first tactics that pay O(n log n) so adjacency, monotonic movement, or greedy choice becomes visible.