title: Hand of Straights
slug: hand-of-straights
difficulty: medium
leetcode_id: 846
leetcode_url: https://leetcode.com/problems/hand-of-straights/
categories:
  - arrays
  - hash-tables
  - sorting
patterns:
  - slug: greedy
    is_optimal: true
  - slug: heap
    is_optimal: false

function_signature: "def is_n_straight_hand(hand: list[int], group_size: int) -> bool:"

test_cases:
  visible:
    - input: { hand: [1, 2, 3, 6, 2, 3, 4, 7, 8], group_size: 3 }
      expected: true
    - input: { hand: [1, 2, 3, 4, 5], group_size: 4 }
      expected: false
  hidden:
    - input: { hand: [1], group_size: 1 }
      expected: true
    - input: { hand: [1, 2, 3], group_size: 1 }
      expected: true
    - input: { hand: [1, 2, 3], group_size: 3 }
      expected: true
    - input: { hand: [1, 2, 4], group_size: 3 }
      expected: false
    - input: { hand: [1, 1, 2, 2, 3, 3], group_size: 3 }
      expected: true
    - input: { hand: [1, 2, 3, 4, 5, 6], group_size: 2 }
      expected: false
    - input: { hand: [8, 10, 12], group_size: 3 }
      expected: false

description: |
  Alice has some number of cards and she wants to rearrange the cards into groups so that each group is of size `groupSize`, and consists of `groupSize` consecutive cards.

  Given an integer array `hand` where `hand[i]` is the value written on the i<sup>th</sup> card and an integer `groupSize`, return `true` if she can rearrange the cards, or `false` otherwise.

constraints: |
  - `1 <= hand.length <= 10^4`
  - `0 <= hand[i] <= 10^9`
  - `1 <= groupSize <= hand.length`

examples:
  - input: "hand = [1,2,3,6,2,3,4,7,8], groupSize = 3"
    output: "true"
    explanation: "Alice's hand can be rearranged as [1,2,3], [2,3,4], [6,7,8]."
  - input: "hand = [1,2,3,4,5], groupSize = 4"
    output: "false"
    explanation: "Alice's hand cannot be rearranged into groups of 4 because 5 is not divisible by 4."

explanation:
  intuition: |
    Imagine you're organising a deck of cards into runs of consecutive numbers, like arranging cards in a hand of rummy.

    The key insight is that **the smallest card in any valid arrangement must start a group**. Why? Because no smaller card exists to precede it in a consecutive sequence. So if the smallest card is `3`, you must form a group starting at `3` (i.e., `3, 4, 5` for `groupSize = 3`).

    Think of it like this: you're forced to "use up" the smallest remaining card first. Once you commit to starting a group with that card, you must find the next `groupSize - 1` consecutive cards to complete the group. If any of those cards are missing, the arrangement is impossible.

    This greedy approach works because:
    - Every card must belong to exactly one group
    - The smallest card has no choice — it must start a group
    - By always processing the smallest unused card, we systematically build all possible groups

  approach: |
    We solve this using a **Greedy Approach with Hash Map Counting**:

    **Step 1: Check divisibility**

    - If `len(hand)` is not divisible by `groupSize`, return `false` immediately
    - We need exactly `n / groupSize` complete groups

    &nbsp;

    **Step 2: Count card frequencies**

    - Use a hash map to count occurrences of each card value
    - This allows O(1) lookups and decrements

    &nbsp;

    **Step 3: Sort unique card values**

    - Sort the unique card values (or use a min-heap)
    - This ensures we always process the smallest available card first

    &nbsp;

    **Step 4: Greedily form groups**

    - For each smallest card value with count > 0:
      - Attempt to form a group starting at this value
      - For each of the next `groupSize` consecutive values:
        - If the count is 0 (card unavailable), return `false`
        - Decrement the count of each card used
    - If all groups formed successfully, return `true`

    &nbsp;

    The greedy choice of always starting from the smallest available card guarantees correctness because that card has no other valid placement.

  common_pitfalls:
    - title: Forgetting the Divisibility Check
      description: |
        Before any complex logic, check if `len(hand) % groupSize == 0`.

        For example, with `hand = [1,2,3,4,5]` and `groupSize = 4`, it's impossible to form complete groups of 4 from 5 cards. This quick check avoids unnecessary computation.
      wrong_approach: "Skipping the divisibility check and processing all cards"
      correct_approach: "Return false immediately if total cards aren't divisible by groupSize"

    - title: Not Processing Cards in Sorted Order
      description: |
        If you try to form groups starting from arbitrary cards, you might use up cards needed for smaller sequences.

        For example, with `hand = [1,2,3,2,3,4]` and `groupSize = 3`, if you greedily grab `[2,3,4]` first, you're left with `[1,2,3]` which works. But if you tried `[1,2,3]` and `[2,3,4]` in different orderings without tracking properly, you could miss valid arrangements or incorrectly report failure.

        By always starting groups from the **smallest available card**, you ensure deterministic and correct grouping.
      wrong_approach: "Processing cards in arbitrary order"
      correct_approach: "Sort cards and always start groups from the smallest value"

    - title: Using Cards Multiple Times
      description: |
        Each card can only belong to one group. When forming a group, you must decrement the count for each card used.

        A common bug is checking if a card exists but forgetting to reduce its count, leading to the same card being "used" in multiple groups.
      wrong_approach: "Checking card existence without decrementing counts"
      correct_approach: "Decrement count immediately after using each card"

  key_takeaways:
    - "**Greedy with constraints**: When elements have no flexibility in placement (smallest must start a group), greedy works"
    - "**Hash map for frequency tracking**: Counting occurrences enables efficient lookups and updates in O(1)"
    - "**Sort to establish processing order**: Sorting unique values ensures we always handle the most constrained element first"
    - "**Early termination**: Simple checks like divisibility can save significant computation"

  time_complexity: "O(n log n). Sorting the unique card values dominates. The grouping phase visits each card at most once, contributing O(n)."
  space_complexity: "O(n). The hash map stores counts for up to `n` unique card values."

solutions:
  - approach_name: Greedy with Hash Map
    is_optimal: true
    code: |
      from collections import Counter

      def is_n_straight_hand(hand: list[int], group_size: int) -> bool:
          # Quick check: total cards must be divisible by group size
          if len(hand) % group_size != 0:
              return False

          # Count frequency of each card value
          card_count = Counter(hand)

          # Process cards in sorted order (smallest first)
          for card in sorted(card_count):
              # If this card has remaining copies, it must start a group
              count = card_count[card]
              if count > 0:
                  # Try to form 'count' groups starting at this card
                  for i in range(group_size):
                      # Need 'count' copies of each consecutive card
                      if card_count[card + i] < count:
                          return False  # Not enough cards to complete groups
                      card_count[card + i] -= count

          return True
    explanation: |
      **Time Complexity:** O(n log n) — Sorting unique values takes O(k log k) where k ≤ n, and we process each card once.

      **Space Complexity:** O(n) — Hash map stores up to n entries.

      We count card frequencies, then iterate through sorted values. When a card has remaining copies, we greedily form as many groups as possible starting from that card. If any consecutive card is missing, we return false.

  - approach_name: Min-Heap Approach
    is_optimal: false
    code: |
      from collections import Counter
      import heapq

      def is_n_straight_hand(hand: list[int], group_size: int) -> bool:
          if len(hand) % group_size != 0:
              return False

          card_count = Counter(hand)
          # Min-heap of unique card values
          min_heap = list(card_count.keys())
          heapq.heapify(min_heap)

          while min_heap:
              # Get smallest card (must start a group)
              smallest = min_heap[0]

              # Form one group starting at smallest
              for i in range(group_size):
                  card = smallest + i
                  if card_count[card] == 0:
                      return False  # Card unavailable

                  card_count[card] -= 1
                  # Remove from heap if exhausted
                  if card_count[card] == 0:
                      # Only remove if it's the heap minimum
                      if card != min_heap[0]:
                          return False  # Gap in sequence
                      heapq.heappop(min_heap)

          return True
    explanation: |
      **Time Complexity:** O(n log n) — Heap operations for each card removal.

      **Space Complexity:** O(n) — Hash map and heap storage.

      This approach uses a min-heap to always access the smallest card. We form one group at a time, removing cards from the heap when exhausted. The constraint that we can only pop the heap minimum ensures consecutive sequences are valid. This is slightly less efficient than the hash map approach but demonstrates an alternative technique.
