title: Valid Sudoku
slug: valid-sudoku
difficulty: medium
leetcode_id: 36
leetcode_url: https://leetcode.com/problems/valid-sudoku/
categories:
  - arrays
  - hash-tables
patterns:
  - matrix-traversal

description: |
  Determine if a `9 x 9` Sudoku board is valid. Only the filled cells need to be validated **according to the following rules**:

  1. Each row must contain the digits `1-9` without repetition.
  2. Each column must contain the digits `1-9` without repetition.
  3. Each of the nine `3 x 3` sub-boxes of the grid must contain the digits `1-9` without repetition.

  **Note:**

  - A Sudoku board (partially filled) could be valid but is not necessarily solvable.
  - Only the filled cells need to be validated according to the mentioned rules.

constraints: |
  - `board.length == 9`
  - `board[i].length == 9`
  - `board[i][j]` is a digit `1-9` or `'.'`

examples:
  - input: 'board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]'
    output: "true"
    explanation: "The board satisfies all three rules: no row, column, or 3x3 sub-box contains duplicate digits."
  - input: 'board = [["8","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]'
    output: "false"
    explanation: "The top-left 3x3 sub-box contains two 8's (at positions [0][0] and [3][0]), making it invalid."

explanation:
  intuition: |
    Think of this problem as a **triple bookkeeping challenge**. Imagine you're a Sudoku referee checking whether a partially filled board follows the rules — you need to keep track of which numbers have appeared in each row, each column, and each 3x3 box.

    The key insight is that you don't need to solve the Sudoku — you only need to verify that no rule is currently broken. This means checking for **duplicates** in three different contexts simultaneously.

    Picture walking through the board cell by cell. For each filled cell, you ask three questions:
    - "Have I seen this number in this row before?"
    - "Have I seen this number in this column before?"
    - "Have I seen this number in this 3x3 box before?"

    If the answer to any of these is "yes," the board is invalid. If you finish scanning all cells without finding a conflict, the board is valid.

    The clever part is figuring out which 3x3 box a cell belongs to. For a cell at position `(row, col)`, its box index can be computed as `(row // 3, col // 3)`. This maps the 9x9 grid into a 3x3 grid of boxes.

  approach: |
    We solve this using **Hash Sets for Tracking**:

    **Step 1: Initialise tracking structures**

    - `rows`: A list of 9 sets, where `rows[i]` tracks digits seen in row `i`
    - `cols`: A list of 9 sets, where `cols[j]` tracks digits seen in column `j`
    - `boxes`: A list of 9 sets, where `boxes[k]` tracks digits seen in box `k`

    &nbsp;

    **Step 2: Iterate through every cell**

    - For each cell at `(row, col)`, skip if it contains `'.'`
    - Calculate the box index: `box_idx = (row // 3) * 3 + (col // 3)`
    - This formula maps the 3x3 sub-grids to indices 0-8

    &nbsp;

    **Step 3: Check for duplicates**

    - If the digit is already in `rows[row]`, `cols[col]`, or `boxes[box_idx]`, return `False`
    - Otherwise, add the digit to all three sets

    &nbsp;

    **Step 4: Return the result**

    - If we complete the iteration without finding duplicates, return `True`

  common_pitfalls:
    - title: Incorrect Box Index Calculation
      description: |
        The trickiest part is mapping `(row, col)` to the correct box index. A common mistake is using `row // 3 + col // 3`, which doesn't uniquely identify boxes.

        For example, cell `(0, 3)` and cell `(1, 0)` would both map to box index `1` with the wrong formula, but they're actually in different boxes.

        The correct formula is `(row // 3) * 3 + (col // 3)`:
        - `row // 3` gives the box row (0, 1, or 2)
        - `col // 3` gives the box column (0, 1, or 2)
        - Combining them: `box_row * 3 + box_col` gives indices 0-8
      wrong_approach: "box_idx = row // 3 + col // 3"
      correct_approach: "box_idx = (row // 3) * 3 + (col // 3)"

    - title: Checking Empty Cells
      description: |
        Empty cells (containing `'.'`) should be skipped entirely. A common error is forgetting to check for empty cells, which might cause issues if `'.'` gets added to your tracking sets.

        Always check `if cell == '.'` and `continue` before processing.
      wrong_approach: "Processing all cells including empty ones"
      correct_approach: "Skip cells containing '.'"

    - title: Using Lists Instead of Sets
      description: |
        Using lists with `in` checks results in O(n) lookup time per check. With sets, membership testing is O(1) on average.

        While this doesn't change the overall O(81) = O(1) complexity for a fixed 9x9 board, using sets is the idiomatic and efficient approach for duplicate detection.
      wrong_approach: "rows = [[] for _ in range(9)]"
      correct_approach: "rows = [set() for _ in range(9)]"

  key_takeaways:
    - "**Hash sets for duplicate detection**: When checking for duplicates across multiple dimensions, use separate sets for each dimension"
    - "**2D to 1D index mapping**: The formula `(row // 3) * 3 + (col // 3)` is a common pattern for mapping 2D sub-grids to unique indices"
    - "**Validation vs solving**: This problem only validates current state — it doesn't require backtracking or solving the puzzle"
    - "**Fixed-size optimisation**: Since the board is always 9x9, the complexity is technically O(1), but the algorithm generalises to larger grids"

  time_complexity: "O(81) = O(1). We iterate through each of the 81 cells exactly once. For a general `n x n` board, this would be O(n^2)."
  space_complexity: "O(81) = O(1). We use 27 sets (9 rows + 9 columns + 9 boxes), each storing at most 9 digits. For a general `n x n` board, this would be O(n^2)."

solutions:
  - approach_name: Hash Set Tracking
    is_optimal: true
    code: |
      def is_valid_sudoku(board: list[list[str]]) -> bool:
          # Initialise sets for each row, column, and 3x3 box
          rows = [set() for _ in range(9)]
          cols = [set() for _ in range(9)]
          boxes = [set() for _ in range(9)]

          for row in range(9):
              for col in range(9):
                  digit = board[row][col]

                  # Skip empty cells
                  if digit == '.':
                      continue

                  # Calculate which 3x3 box this cell belongs to
                  box_idx = (row // 3) * 3 + (col // 3)

                  # Check if digit already exists in row, column, or box
                  if digit in rows[row]:
                      return False
                  if digit in cols[col]:
                      return False
                  if digit in boxes[box_idx]:
                      return False

                  # Add digit to all three tracking sets
                  rows[row].add(digit)
                  cols[col].add(digit)
                  boxes[box_idx].add(digit)

          # No duplicates found
          return True
    explanation: |
      **Time Complexity:** O(1) — We always iterate through exactly 81 cells.

      **Space Complexity:** O(1) — We use a fixed number of sets (27 total) with at most 9 elements each.

      This solution uses hash sets to efficiently track which digits have been seen in each row, column, and 3x3 box. The key insight is computing the box index from the cell coordinates using integer division.

  - approach_name: Single Set with Encoded Keys
    is_optimal: true
    code: |
      def is_valid_sudoku(board: list[list[str]]) -> bool:
          seen = set()

          for row in range(9):
              for col in range(9):
                  digit = board[row][col]

                  if digit == '.':
                      continue

                  # Create unique keys for row, column, and box
                  row_key = (digit, 'row', row)
                  col_key = (digit, 'col', col)
                  box_key = (digit, 'box', row // 3, col // 3)

                  # Check if any key already exists
                  if row_key in seen or col_key in seen or box_key in seen:
                      return False

                  # Add all three keys
                  seen.add(row_key)
                  seen.add(col_key)
                  seen.add(box_key)

          return True
    explanation: |
      **Time Complexity:** O(1) — Same iteration through 81 cells.

      **Space Complexity:** O(1) — Single set with at most 243 entries (81 cells x 3 keys each).

      This alternative uses a single set with encoded tuples to distinguish between row, column, and box constraints. The tuple structure ensures uniqueness: `('5', 'row', 0)` means "digit 5 in row 0". This approach is more compact in code but uses slightly more memory per entry due to tuple overhead.

  - approach_name: Bitmask Tracking
    is_optimal: true
    code: |
      def is_valid_sudoku(board: list[list[str]]) -> bool:
          # Use integers as bitmasks (bit i represents digit i)
          rows = [0] * 9
          cols = [0] * 9
          boxes = [0] * 9

          for row in range(9):
              for col in range(9):
                  digit = board[row][col]

                  if digit == '.':
                      continue

                  # Convert digit to bit position (1-9 -> bits 1-9)
                  bit = 1 << int(digit)
                  box_idx = (row // 3) * 3 + (col // 3)

                  # Check if bit is already set in any mask
                  if rows[row] & bit:
                      return False
                  if cols[col] & bit:
                      return False
                  if boxes[box_idx] & bit:
                      return False

                  # Set the bit in all three masks
                  rows[row] |= bit
                  cols[col] |= bit
                  boxes[box_idx] |= bit

          return True
    explanation: |
      **Time Complexity:** O(1) — Same iteration through 81 cells.

      **Space Complexity:** O(1) — Uses 27 integers instead of 27 sets.

      This solution uses bitmasks for more memory-efficient tracking. Each integer represents a set of digits using bits: bit `i` being set means digit `i` has been seen. Bitwise AND (`&`) checks membership, and bitwise OR (`|=`) adds elements. This approach is faster in practice due to CPU-level bit operations.
