Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Decide whether the hand can be split into groups of `groupSize` consecutive cards. The brute force repeatedly sorts and peels a run off the front in O(n² log n). The greedy is forced, not clever: count the cards, then always start the next group at the smallest remaining card and consume `groupSize` consecutive values from it — there is no smaller card to absorb the minimum, so it must lead a group. With a TreeMap (Java) or a heap + hash map (Python) that's O(n log n) time.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you spot that the smallest remaining card is *forced* to begin a group (so there's no search, just bookkeeping), and whether you pick the right data structure to fetch the running minimum and decrement duplicate counts efficiently.
Where candidates slip
Trying to backtrack over which cards join which group as if there's a choice, or de-duplicating the hand and losing the count of repeated cards so two groups that need the same value collide.
How to stand out
State the forced-anchor argument out loud — 'the minimum can't sit in the middle or end of any consecutive run, so it must lead one' — then pick a TreeMap / heap to make 'smallest remaining' O(log n) and note the len % groupSize early-out.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewSplit hand into runs of groupSize consecutivelen % groupSize == 0?NoYesExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Sort, Then Peel a Run Off the Front
1Hint 1Explore hintHide hint
Sort the cards. The smallest card has nothing smaller to pair with, so it must start a group. What group is it forced into?
2Hint 2Explore hintHide hint
Remove a run of groupSize consecutive values starting at that smallest card. If any value in the run is missing, fail.
3Hint 3Explore hintHide hint
Repeat: find the new smallest remaining card and peel another run. Keep going until the hand is empty or a run can't be completed.
A · Core idea
Sort the hand, then repeatedly take the smallest remaining card, claim a run of groupSize consecutive values starting there, and remove those cards. If a needed value is absent, the partition is impossible.
Why the smallest card anchors a group — the core observation. Consider the minimum value m still in the hand. In any valid partition, m lives in some group, and that group is a run of consecutive integers. m cannot be the second-or-later element of its run, because that would require m − 1 to also be present and unused — but m is the smallest card left, so nothing smaller exists. Therefore m is forced to be the first element of its group. There is no decision to make: the group containing m must be exactly [m, m+1, …, m+groupSize−1].
Why peeling that exact run is safe. Since the group for m is forced, removing [m, …, m+groupSize−1] can never be a mistake — we're not choosing among options, we're executing the only legal move. After removal, the new minimum of what remains is again forced to anchor its own group, and the argument repeats. This is what makes the greedy exact: every step is mandatory, so there's nothing to backtrack over.
Why this implementation is slow. Conceptually the algorithm is perfect; the naive realisation is wasteful. Sorting once is O(n log n), but if each round re-sorts or linearly scans a remaining list to find and delete the next minimum and its run, you pay O(n) (or worse) per round across O(n) rounds — landing around O(n² log n) with list deletions. The logic is right; the bookkeeping needs a better data structure.
Why we keep the idea but change the container. Nothing about the forced-anchor argument changes — we just need 'give me the current minimum' and 'decrement a card's count' to each be cheap. That's exactly what a TreeMap (ordered keys) or a min-heap of distinct values plus a hash map of counts provides, dropping the cost to O(n log n).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
hand = [1, 2, 3, 6, 2, 3, 4, 7, 8], groupSize = 3min = 1
peel [1,2,3]
remaining: 2,3,4,6,7,8trueC · Code with teaching comments
Reference implementation previewpublic boolean isNStraightHand(int[] hand, int groupSize) { if (hand.length % groupSize != 0) return false; TreeMap<Integer, Integer> count = new TreeMap<>(); for (int c : hand) count.merge(c, 1, Integer::sum); while (!count.isEmpty()) {Explore the full codeHide the full code
D · Why it works and complexityO(n² log n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The whole problem has zero search: the minimum is always forced to lead a group, so there's never a choice to explore or undo. Recognising 'forced move' versus 'genuine choice' is the difference between an exponential backtracking attempt and a clean linear-ish greedy. The only engineering question left is how fast you can serve the running minimum.
ComplexityO(n² log n) time, O(n) space — Each of up to n/groupSize rounds finds the current minimum and removes a run; if you re-sort or rescan a list of size O(n) each round, that's O(n) per round across O(n) rounds with sorting overhead — O(n² log n) in the naive form.
E · Edge cases and pitfalls3De-duplicating the hand and losing duplicate counts — two groups needing the same value then collide.Explore edge casesHide edge cases
Pitfalls
- De-duplicating the hand and losing duplicate counts — two groups needing the same value then collide.
- Re-sorting the whole remaining list every round instead of using an ordered structure.
- Skipping the len % groupSize check and discovering infeasibility only after wasted work.
Approach 2 of 2 · Optimal
Greedy — Forced Anchor via Heap + Count Map (Optimal)
1Hint 1Explore hintHide hint
Keep the forced-anchor insight: the smallest remaining card must start a group. The only thing to optimise is fetching that smallest card repeatedly.
2Hint 2Explore hintHide hint
Use a hash map for counts and a min-heap of the distinct card values. The heap top is always the current smallest card.
3Hint 3Explore hintHide hint
Take the heap's minimum, consume groupSize consecutive values by decrementing counts. When a card's count hits zero, it must be the current heap minimum to pop — if a smaller card is orphaned, fail.
A · Core idea
Build a count map value → frequency and a min-heap of the distinct values. While the heap is non-empty, peek the minimum first — the forced anchor — and consume groupSize consecutive values first, first+1, …, first+groupSize−1, decrementing each count. If any required value is absent (count zero), return false. When a card's count reaches zero, it must be the current heap minimum for us to pop it; if it isn't, a smaller card has been stranded and we fail.
Why the heap, not just sorting once. The forced-anchor argument says we always need the current smallest remaining card, and 'remaining' shrinks unevenly as duplicates get consumed at different rates. A heap (or TreeMap) gives that running minimum in O(log n) without re-sorting. Each of the n cards is consumed exactly once, and we touch the heap a logarithmic amount per distinct value, so the whole thing is O(n log n).
Why the 'count hit zero must be the min' check is the subtle correctness guard. We always consume runs starting from the global minimum. Suppose while filling a run we decrement some card c to zero, but c is not currently the smallest value in the heap — meaning some value d < c still has a positive count. That d was skipped: it should have anchored its own group before we ever consumed c, yet here it sits with cards left while c is exhausted. No future group can ever include d (every later anchor is ≥ the current first ≥ d, and a run starting at ≥ first that reaches d would need d to be in the middle of a run, impossible since d is below the anchor). So d is orphaned and the answer is false. Detecting 'a card emptied out of order' is exactly that orphan check.
Why this greedy is provably optimal — the exchange/contradiction argument. Claim: if any valid partition exists, the forced-anchor greedy finds one. Suppose, for contradiction, the greedy fails on a hand that does admit a valid partition P. Look at the first step where greedy claims the run [m, …, m+groupSize−1] for the minimum m. In P, the card m also lives in some group, and as argued m must be the first element of that group — so P's group for m is also [m, …, m+groupSize−1], identical to greedy's choice. Thus greedy's move matches a valid partition exactly; remove that group from both and the same argument applies to the new minimum, by induction. So greedy never diverges from some valid partition, contradicting the assumption that it fails where one exists. The 'no choice for the minimum' fact is what makes the exchange trivial: there's only one legal group for the smallest card, so greedy and the optimal solution are forced into the same first move.
Why this beats the brute force. Same forced-anchor logic, but 'fetch the minimum' drops from an O(n) rescan to an O(log n) heap op, collapsing O(n² log n) to O(n log n) while keeping O(n) space for the counts.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
hand = [1, 2, 3, 6, 2, 3, 4, 7, 8], groupSize = 3Interactive diagram
Why the minimum is forced to lead
min = 1The smallest remaining card has no smaller neighbour, so it can only be the first element of its run — there's no choice to search.
trueC · Code with teaching comments
D · Why it works and complexityO(n log n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The greedy is correct precisely because there is no greedy choice — the minimum has a single legal home, so 'greedy' and 'optimal' make the identical move at every step. When you can prove a step is forced (not merely locally attractive), you get an exact algorithm for free and skip the usual exchange-argument gymnastics. The only craft left is serving the running minimum cheaply: heap or TreeMap.
ComplexityO(n log n) time, O(n) space — Building the count map is O(n) and heapifying distinct keys is O(n). Each card is consumed once, and heap pops/peeks cost O(log n), so total work is O(n log n) time; the map and heap hold up to n entries → O(n) space.
E · Edge cases and pitfalls6hand = [1, 1, 2, 2, 3, 3], groupSize = 3 — count {1:2,2:2,3:2}; min 1 → consume 1,2,3 (counts→1,1,1); 1 still positive so re-anchor at 1 → consume 1,2,3 → all empty → true (two groups [1,2,3]).Explore edge casesHide edge cases
Edge cases handled
hand = [1, 1, 2, 2, 3, 3], groupSize = 3count {1:2,2:2,3:2}; min 1 → consume 1,2,3 (counts→1,1,1); 1 still positive so re-anchor at 1 → consume 1,2,3 → all empty → true (two groups [1,2,3]).hand = [1, 2, 4, 5], groupSize = 2min 1 → consume 1,2; min 4 → consume 4,5 → true. Two runs of length 2.hand = [8, 10, 12], groupSize = 3min 8 → needs 8,9,10 but count[9]==0 → return false.
Pitfalls
- Popping a card from the heap when its count hits zero without confirming it's the current minimum — masks an orphaned smaller card.
- Using count[card] without ensuring the key exists (in Java, treating a null Integer as 0) — NullPointerException.
- Forgetting that an anchor with count > 1 must start multiple groups — loop while its count remains positive.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic boolean isNStraightHand(int[] hand, int groupSize) { if (hand.length % groupSize != 0) return false; TreeMap<Integer, Integer> count = new TreeMap<>(); for (int c : hand) count.merge(c, 1, Integer::sum); while (!count.isEmpty()) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Losing duplicate counts by deduping
Two cards of value 2 belong to two different groups. Storing values in a set instead of counting frequencies makes the second group silently miss its 2.
Explore the code comparisonHide the code comparison
Losing duplicate counts by deduping
Two cards of value 2 belong to two different groups. Storing values in a set instead of counting frequencies makes the second group silently miss its 2.
Don't write this
values = set(hand) # drops duplicate counts
for first in sorted(values):
for card in range(first, first + groupSize):
if card not in values:
return FalseWrite this instead
count = Counter(hand) # keeps how many of each value
# consume by decrementing count[card], removing only at zero2Popping the heap minimum out of order
When a card empties but isn't the current minimum, a smaller card was skipped and can never be placed. Popping it anyway hides that orphan and returns a false true.
Explore the code comparisonHide the code comparison
Popping the heap minimum out of order
When a card empties but isn't the current minimum, a smaller card was skipped and can never be placed. Popping it anyway hides that orphan and returns a false true.
Don't write this
if count[card] == 0:
heapq.heappop(min_heap) # pops regardless of positionWrite this instead
if count[card] == 0:
if card != min_heap[0]:
return False
heapq.heappop(min_heap)3Skipping the divisibility early-out
If len(hand) isn't a multiple of groupSize, no partition can exist; without this guard you do full work and may even return a wrong answer on edge inputs.
Explore the code comparisonHide the code comparison
Skipping the divisibility early-out
If len(hand) isn't a multiple of groupSize, no partition can exist; without this guard you do full work and may even return a wrong answer on edge inputs.
Don't write this
TreeMap<Integer,Integer> count = new TreeMap<>();
// ... build and group without checking divisibilityWrite this instead
if (hand.length % groupSize != 0) return false;
TreeMap<Integer,Integer> count = new TreeMap<>();
// ... then build and groupRevise
Carry the pattern into the interview
Pattern to remember
Greedy 'smallest card must lead a group' + ordered counts
Code pattern to memorize
if len(hand) % groupSize: return False
count = Counter(hand)
while count:
start = min(remaining keys) # heap / TreeMap gives this in O(log n)
for c in range(start, start + groupSize):
if count[c] == 0: return False
count[c] -= 1
return TrueRules to remember
- 1Fail fast: if
len(hand) % groupSize != 0the split is impossible — returnFalsebefore doing any work. - 2The smallest remaining card has no smaller neighbour, so it must be the start of a group — this is forced, the heart of the greedy.
- 3From that smallest card, consume exactly
groupSizeconsecutive values; a missing value mid-run means failure. - 4Use an ordered structure for 'give me the current minimum fast':
TreeMapin Java, a min-heap of keys + hash map of counts in Python. - 5Decrement counts, not a list — duplicates of the same card belong to different groups, so never dedupe them away.
- 6Only pop the minimum from the heap when its count hits zero, and only if it really is the current minimum — otherwise a smaller card was orphaned.
Use this pattern when
- Partition a multiset into runs of consecutive (or fixed-step) values
- An element's smallest member is forced to anchor its group
- You repeatedly need the current minimum among remaining counts
- Counts of duplicates matter and groups are size-fixed
Choose another pattern when
- Groups need not be consecutive (arbitrary subsets) → this forced-anchor logic doesn't apply
- Order within a group is free and values can repeat freely → it's a counting/parity problem, not a run partition
- You must output the actual groups under tight memory → still doable but watch the bookkeeping
- Group size varies per group → the fixed-window consume breaks
Interview takeaway
Hand of Straights is the cleanest 'the minimum is forced to anchor a group' greedy: because nothing is smaller than the current minimum, it has no choice but to lead, which removes all decision-making. The same forced-anchor idea drives Divide Array in Sets of K Consecutive Numbers — recognise it whenever a multiset must break into consecutive runs.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical 'forced anchor' greedy: the smallest remaining element must lead its group, so there is no choice to search — only fast access to the running minimum (TreeMap or heap + count map). Recognise it for partitioning a multiset into consecutive runs; it's the same engine as Divide Array in Sets of K Consecutive Numbers.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Divide Array in Sets of K Consecutive NumbersComing soonIdentical problem with different wording — same forced-anchor greedy on counts.
- Split Array into Consecutive SubsequencesComing soonRuns of length ≥ 3 (not fixed) — greedy that prefers extending an existing run, tracked with two count maps.
- Task SchedulerComing soonGreedy scheduling with a max-heap of counts — sibling 'process by frequency' heap problem.
- Reorganize StringComing soonGreedy placement driven by a heap of character counts — same heap-of-counts toolkit.