Facebook Pixel

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:

  1. Each group must contain exactly groupSize cards
  2. The cards in each group must form a consecutive sequence of numbers

You're given:

  • An array hand where hand[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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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 by groupSize, we can immediately return false.

  2. 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.

  3. 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.

  4. Once we decide to start a group with card value x, we must have cards x, 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.

  5. 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.

Learn more about Greedy and Sorting patterns.

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 return false
    • Otherwise, we decrement cnt[y] by 1 to mark that we've used one instance of this card

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 Evaluator

Example 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 1s 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More