Hand of Straights

Hand of Straights looks like a card game puzzle. But it is really a test of one idea. Can you always make the safest move first and trust it? That habit is called greedy thinking. The interviewer wants to see if you can spot the one move that can never go wrong.

🎯 The Problem

You get a hand of cards and a group size, and must split the hand into valid groups.

  • Each card is just a number.
  • Every group must have exactly the group-size many cards.
  • The cards in each group must be consecutive numbers, like 1, 2, 3.
  • Every card must be used.
  • Return true if you can split the whole hand this way, else false.

Let us say the hand is [1, 2, 3, 6, 2, 3, 4, 7, 8] and the group size is 3. You can make [1, 2, 3], then [2, 3, 4], then [6, 7, 8]. Every card is used. Every group has three consecutive numbers. So the answer is true.

Input: hand = [1, 2, 3, 6, 2, 3, 4, 7, 8], groupSize = 3
Output: true
Explanation: groups are [1,2,3], [2,3,4], [6,7,8]

Here is the first quick check. If the total number of cards does not divide evenly by the group size, you can stop right away. The answer is false. You can never finish if the cards do not split cleanly.

Here is the shape of the problem. We have repeated numbers, and we want consecutive runs.

Hand: 1 2 3 6 2 3 4 7 8

Group size = 3

Need groups of 3 consecutive numbers

Every card must be used

Answer: true or false

🐒 Approach 1: Try Every Grouping (Brute Force)

Try every possible way of splitting the cards into runs.

The idea:

  • Pick group-size many cards. Check if they form a consecutive run.
  • Remove them. Try again with what is left.
  • If you get stuck, go back and try a different grouping.

Why it is weak:

  • The number of ways to pick groups explodes as the hand grows.
  • You end up trying a huge number of combinations.
  • The back-and-forth retry logic is also hard to write correctly.

Here is a backtracking version of the brute-force idea:

hand_of_straights_brute_force.py
from collections import Counter
def is_n_straight_hand(hand, group_size):
count = Counter(hand)
def dfs(groups_left):
if groups_left == 0:
return True
start = min(num for num in count if count[num] > 0)
for num in range(start, start + group_size):
if count[num] == 0:
return False
count[num] -= 1
ok = dfs(groups_left - 1)
for num in range(start, start + group_size):
count[num] += 1
return ok
return len(hand) % group_size == 0 and dfs(len(hand) // group_size)

⚑ Approach 2: Greedy With a Sorted Count Map (Better)

The idea in one line: the smallest leftover card must start a group, so build runs from the smallest upward.

The idea:

  • Look at the smallest leftover card. No card is smaller, so nothing can sit before it in a run.
  • The only group it can belong to is one that starts with it.
  • So take the smallest card and build the one run it can start.

How it works:

  • Use a count map that stores each card number and how many copies remain.
  • Do not move cards around. Just lower the count as you use them.
  • Walk the card numbers in sorted order. For each number still left, treat it as a group start.
  • Try to take one of each of the next numbers in the run. If any is gone, return false.

Why it is safe:

  • You always start from the smallest leftover card.
  • There is never a better first move, so the greedy choice can never go wrong.
  • The cost is mostly the sorting, so O(n log n).

Here is the sorted-count-map code:

hand_of_straights_sorted_count.py
from collections import Counter
def is_n_straight_hand(hand, group_size):
count = Counter(hand)
for start in sorted(count):
while count[start] > 0:
for num in range(start, start + group_size):
if count[num] == 0:
return False
count[num] -= 1
return True

🧭 Approach 3: Greedy With a Min-Heap (Alternative)

The idea in one line: keep the distinct card values in a min-heap so the smallest is always on top.

The idea:

  • A min-heap is a structure where the smallest item is always on top.
  • Pull the smallest leftover card from the top each time.
  • It still starts the next group, same greedy rule as before.

How it works:

  • Build a count map and a min-heap of the distinct values.
  • Take the heap top as the group start. Lower the count of each value in the run.
  • When a value’s count hits zero, it must be the current heap top or a hole would be left behind, so check that and pop it.

Why it is an alternative:

  • Same O(n log n) time as the sorted map. The heap replaces the sort.
  • It reads cleanly when you only need the running smallest, not a full sorted list.
  • This is the version shown in the Python code below.

Here is a dry run of the greedy steps on our example. We sort the unique numbers, then build runs from the smallest.

Counts: 1:1 2:2 3:2 4:1 6:1 7:1 8:1

Start at 1 -> take 1,2,3

Counts: 2:1 3:1 4:1 6:1 7:1 8:1

Start at 2 -> take 2,3,4

Counts: 6:1 7:1 8:1

Start at 6 -> take 6,7,8

All counts zero -> true

Steps to Solve

  1. If the total card count does not divide evenly by the group size, return false right away.
  2. Build a count map that stores each card number and how many copies you have.
  3. Get the card numbers in sorted order.
  4. Walk through the sorted numbers. For each number that still has a count above zero, treat it as the start of a group.
  5. For that start number, try to remove one of each of the next numbers in the run, up to the group size.
  6. If any needed number is missing or runs out, return false.
  7. If you finish using every card, return true.

This Python version uses Counter for the counts and a heap to always pull the smallest leftover card.

hand_of_straights.py
from collections import Counter
import heapq
def is_straight_hand(hand, group_size):
if len(hand) % group_size != 0: # cannot split evenly
return False
count = Counter(hand) # value -> remaining count
min_heap = list(count.keys()) # all distinct card values
heapq.heapify(min_heap) # smallest value sits on top
while min_heap:
start = min_heap[0] # smallest leftover card
for k in range(start, start + group_size):
if count[k] == 0: # a needed number is missing
return False
count[k] -= 1 # use one copy of k
if count[k] == 0: # this value is now used up
if k != min_heap[0]: # but it is not the current top
return False # so a hole would be left behind
heapq.heappop(min_heap) # remove the used-up smallest
return True
hand = [1, 2, 3, 6, 2, 3, 4, 7, 8]
group_size = 3
print(is_straight_hand(hand, group_size))

The output of the above code will be:

True

Let us walk through the Python version line by line, because the heap part is the tricky bit.

First we check len(hand) % group_size. If the cards do not divide evenly, we return False at once. This saves all the later work.

Then count = Counter(hand) builds the map from each card value to how many we have. Counter does the counting for us in one line.

Next min_heap = list(count.keys()) and heapq.heapify(min_heap) give us a min heap of the distinct values. A min heap is a structure where the smallest item is always on top. So min_heap[0] is the smallest leftover card at any moment.

Inside the loop, start = min_heap[0] grabs that smallest card. It must start a group. We then loop k from start up to start + group_size. For each k we check count[k]. If it is zero, a number in the run is missing, so we return False. Otherwise we do count[k] -= 1 to use one copy.

The careful part is when count[k] becomes zero. That value is now used up. If it is the current top of the heap, we pop it with heapq.heappop. But if it is some larger value that ran out before the top did, that means a hole opened in the middle. We can never fill that hole later. So we return False. That single check keeps the greedy choice correct.

⏱️ Time and Space Complexity

The brute force tries many groupings, so its time grows in a way that becomes unusable fast. The greedy version sorts the values once and then sweeps through them. So its cost is mostly the sorting. We write that as O(n log n). The extra space is the count map, which holds at most one entry per distinct card.

Approach Time Complexity Space Complexity
Brute force (try all groupings) Exponential O(n)
Greedy with sorted count map O(n log n) O(n)
Greedy with a min-heap O(n log n) O(n)

Tip

The whole trick is one sentence. The smallest leftover card must start a group. Say that out loud in the interview and the greedy choice becomes obvious.

🧩 Key Takeaways

  • βœ… If the total cards do not divide evenly by the group size, the answer is false at once.
  • βœ… The smallest leftover card must always start a group, because nothing smaller can come before it.
  • βœ… Use a count map so you lower counts instead of moving cards around.
  • βœ… Build each run from the start card up to the group size, and fail if any number is missing.
  • βœ… This greedy choice is always safe, so the answer comes in O(n log n) time.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the very first thing you should check in Hand of Straights?

    Why: If the number of cards does not divide evenly by the group size, you can never finish, so the answer is false right away.

  2. 2

    Why must the smallest leftover card start a group?

    Why: Nothing is smaller than the smallest leftover card, so no run can place it anywhere except at the start.

  3. 3

    What data structure helps us use cards without physically moving them?

    Why: A count map stores how many copies of each value remain, so we just lower counts as we use cards.

  4. 4

    What is the time complexity of the greedy solution?

    Why: We sort or order the distinct values once and sweep through them, so the cost is dominated by O(n log n).

πŸš€ What’s Next?