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 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)   **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   **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)   **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)   **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 `'#'`.