title: Check if Word Can Be Placed In Crossword
slug: check-if-word-can-be-placed-in-crossword
difficulty: medium
leetcode_id: 2018
leetcode_url: https://leetcode.com/problems/check-if-word-can-be-placed-in-crossword/
categories:
  - arrays
  - strings
patterns:
  - matrix-traversal

function_signature: "def place_word_in_crossword(board: list[list[str]], word: str) -> bool:"

test_cases:
  visible:
    - input: { board: [["#", " ", "#"], [" ", " ", "#"], ["#", "c", " "]], word: "abc" }
      expected: true
    - input: { board: [[" ", "#", "a"], [" ", "#", "c"], [" ", "#", "a"]], word: "ac" }
      expected: false
    - input: { board: [["#", " ", "#"], [" ", " ", "#"], ["#", " ", "c"]], word: "ca" }
      expected: true
  hidden:
    - input: { board: [[" "]], word: "a" }
      expected: true
    - input: { board: [["a"]], word: "a" }
      expected: true
    - input: { board: [["a"]], word: "b" }
      expected: false
    - input: { board: [[" ", " ", " "]], word: "abc" }
      expected: true
    - input: { board: [[" ", "#", " "]], word: "ab" }
      expected: false
    - input: { board: [["a", "b", "c"]], word: "abc" }
      expected: true

description: |
  You are given an `m x n` matrix `board`, representing the **current** state of a crossword puzzle. The crossword contains lowercase English letters (from solved words), `' '` to represent any **empty** cells, and `'#'` to represent any **blocked** cells.

  A word can be placed **horizontally** (left to right **or** right to left) or **vertically** (top to bottom **or** bottom to top) in the board if:

  - It does not occupy a cell containing the character `'#'`.
  - The cell each letter is placed in must either be `' '` (empty) or **match** the letter already on the `board`.
  - There must not be any empty cells `' '` or other lowercase letters **directly left or right** of the word if the word was placed **horizontally**.
  - There must not be any empty cells `' '` or other lowercase letters **directly above or below** the word if the word was placed **vertically**.

  Given a string `word`, return `true` *if* `word` *can be placed in* `board`, *or* `false` *otherwise*.

constraints: |
  - `m == board.length`
  - `n == board[i].length`
  - `1 <= m * n <= 2 * 10^5`
  - `board[i][j]` will be `' '`, `'#'`, or a lowercase English letter.
  - `1 <= word.length <= max(m, n)`
  - `word` will contain only lowercase English letters.

examples:
  - input: 'board = [["#", " ", "#"], [" ", " ", "#"], ["#", "c", " "]], word = "abc"'
    output: "true"
    explanation: 'The word "abc" can be placed vertically (top to bottom) starting from position (0, 1).'
  - input: 'board = [[" ", "#", "a"], [" ", "#", "c"], [" ", "#", "a"]], word = "ac"'
    output: "false"
    explanation: "It is impossible to place the word because there will always be a space/letter above or below it."
  - input: 'board = [["#", " ", "#"], [" ", " ", "#"], ["#", " ", "c"]], word = "ca"'
    output: "true"
    explanation: 'The word "ca" can be placed horizontally (right to left) in the last row.'

explanation:
  intuition: |
    Think of this problem like finding a valid "slot" in a crossword puzzle where you can write your word.

    In a real crossword puzzle, a valid slot is a sequence of empty or matching cells that is **bounded** on both ends — either by a `'#'` (blocked cell) or by the edge of the grid. The slot must be exactly the right length for your word, and any pre-filled letters must match.

    The key insight is that we need to check **four directions** for placing the word:
    - Left to right (horizontal)
    - Right to left (horizontal, reversed)
    - Top to bottom (vertical)
    - Bottom to top (vertical, reversed)

    Rather than searching the entire grid for every possible starting position, we can systematically find all valid "slots" in each row and column. A slot is defined as a maximal sequence of non-`'#'` cells (i.e., cells that are either empty or contain a letter).

    For each slot we find, we check if the word (or its reverse) fits perfectly: the slot length must equal the word length, and each cell must either be empty or match the corresponding letter in the word.

  approach: |
    We solve this by checking rows (horizontal placement) and columns (vertical placement) separately.

    **Step 1: Define a helper function to check slots in a line**

    - Given a line (a row or column), split it by `'#'` to get all candidate slots
    - Each slot is a maximal sequence of non-blocked cells
    - For each slot, check if the word fits (forward or backward)

    &nbsp;

    **Step 2: Check if word fits in a slot**

    - The slot length must equal the word length
    - For each position, the cell must be `' '` (empty) or match the word's letter
    - Check both the word and its reverse to handle bidirectional placement

    &nbsp;

    **Step 3: Check all rows for horizontal placement**

    - For each row, extract the cells and find all slots
    - Test if the word can fit in any slot (left-to-right or right-to-left)

    &nbsp;

    **Step 4: Check all columns for vertical placement**

    - For each column, extract the cells vertically and find all slots
    - Test if the word can fit in any slot (top-to-bottom or bottom-to-top)

    &nbsp;

    **Step 5: Return result**

    - Return `true` if any valid placement is found, `false` otherwise

  common_pitfalls:
    - title: Forgetting Bidirectional Placement
      description: |
        The word can be placed in **four** directions, not just two. A common mistake is only checking left-to-right and top-to-bottom.

        For example, if the word is "cat" and you only check forward placement, you'll miss valid positions where "tac" would fit (meaning "cat" can be placed right-to-left).

        Solution: Check both the word and its reverse, or equivalently, check the slot from both ends.
      wrong_approach: "Only checking forward direction"
      correct_approach: "Check both word and reversed word for each slot"

    - title: Not Enforcing Slot Boundaries
      description: |
        A valid placement requires the word to occupy a **complete slot** — it must be bounded by `'#'` or the grid edge on both ends.

        For example, if you have a row `[' ', ' ', ' ', '#']` and word = "ab", you cannot place "ab" at positions 0-1 because position 2 is still part of the same open slot. The slot has length 3, not 2.

        This means you can't just find any two consecutive empty cells; you must find a slot whose total length equals the word length.
      wrong_approach: "Looking for any substring of matching length"
      correct_approach: "Split by '#' to find complete slots, then check exact length match"

    - title: Ignoring Pre-filled Letters
      description: |
        Some cells may already contain letters from other words in the crossword. You must check that these letters **match** the corresponding letter in your word.

        A common bug is treating all non-`'#'` cells as empty and overwriting them.
      wrong_approach: "Treating all non-blocked cells as empty"
      correct_approach: "Check that existing letters match or cell is empty"

  key_takeaways:
    - "**Matrix traversal with constraints**: When checking placements in a grid, consider all valid directions and boundary conditions"
    - "**Slot-based thinking**: Instead of checking every starting position, identify valid slots first (bounded sequences), then verify if the word fits"
    - "**Bidirectional checking**: Many grid problems require considering both forward and backward directions — factor this into your solution"
    - "**Similar problems**: This pattern applies to other crossword/word-search problems like Word Search, Word Search II, and word-placement puzzles"

  time_complexity: "O(m * n). We iterate through each cell of the grid at most a constant number of times — once when processing rows and once when processing columns."
  space_complexity: "O(max(m, n)). We may store a row or column temporarily when checking slots, which has at most `max(m, n)` elements."

solutions:
  - approach_name: Slot-Based Matching
    is_optimal: true
    code: |
      def place_word_in_crossword(board: list[list[str]], word: str) -> bool:
          m, n = len(board), len(board[0])

          def can_place(slot: list[str], word: str) -> bool:
              """Check if word can be placed in slot (forward or backward)."""
              if len(slot) != len(word):
                  return False
              # Check forward placement
              forward = all(
                  cell == ' ' or cell == ch
                  for cell, ch in zip(slot, word)
              )
              # Check backward placement
              backward = all(
                  cell == ' ' or cell == ch
                  for cell, ch in zip(slot, reversed(word))
              )
              return forward or backward

          def check_line(line: list[str]) -> bool:
              """Check all slots in a line (row or column)."""
              # Split line by '#' to get all slots
              slot = []
              for cell in line:
                  if cell == '#':
                      # End of current slot, check it
                      if can_place(slot, word):
                          return True
                      slot = []
                  else:
                      slot.append(cell)
              # Check final slot after last '#' (or if no '#' at all)
              return can_place(slot, word)

          # Check all rows (horizontal placement)
          for row in board:
              if check_line(row):
                  return True

          # Check all columns (vertical placement)
          for col in range(n):
              column = [board[row][col] for row in range(m)]
              if check_line(column):
                  return True

          return False
    explanation: |
      **Time Complexity:** O(m * n) — We process each cell twice (once for rows, once for columns), and the word matching is O(word length) which is bounded by max(m, n).

      **Space Complexity:** O(max(m, n)) — We store column data temporarily and maintain slots during iteration.

      This approach elegantly handles all four directions by recognizing that checking a slot forward and backward covers both horizontal directions (for rows) and both vertical directions (for columns).

  - approach_name: Direct Position Checking
    is_optimal: false
    code: |
      def place_word_in_crossword(board: list[list[str]], word: str) -> bool:
          m, n = len(board), len(board[0])
          k = len(word)

          def is_valid_start(r: int, c: int, dr: int, dc: int) -> bool:
              """Check if position (r, c) is a valid start for direction (dr, dc)."""
              # Previous cell must be '#' or out of bounds
              pr, pc = r - dr, c - dc
              if 0 <= pr < m and 0 <= pc < n and board[pr][pc] != '#':
                  return False
              return True

          def is_valid_end(r: int, c: int, dr: int, dc: int) -> bool:
              """Check if position after word is valid (boundary or '#')."""
              er, ec = r + dr * k, c + dc * k
              if 0 <= er < m and 0 <= ec < n and board[er][ec] != '#':
                  return False
              return True

          def can_place_at(r: int, c: int, dr: int, dc: int) -> bool:
              """Check if word can be placed starting at (r, c) in direction (dr, dc)."""
              if not is_valid_start(r, c, dr, dc):
                  return False
              if not is_valid_end(r, c, dr, dc):
                  return False

              # Check each letter of the word
              for i in range(k):
                  nr, nc = r + dr * i, c + dc * i
                  # Must be within bounds
                  if not (0 <= nr < m and 0 <= nc < n):
                      return False
                  cell = board[nr][nc]
                  # Cell must be empty or match the letter
                  if cell != ' ' and cell != word[i]:
                      return False
              return True

          # Four directions: right, left, down, up
          directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

          # Try every starting position and direction
          for r in range(m):
              for c in range(n):
                  for dr, dc in directions:
                      if can_place_at(r, c, dr, dc):
                          return True
          return False
    explanation: |
      **Time Complexity:** O(m * n * k) — For each cell, we check 4 directions, and each check takes O(k) where k is the word length.

      **Space Complexity:** O(1) — Only uses constant extra space.

      This brute-force approach checks every possible starting position and direction. While correct, it's less elegant than the slot-based approach because it explicitly handles boundary conditions at every step. The slot-based approach naturally handles boundaries by splitting on `'#'`.
