|
| 1 | +# [Problem 37: Sudoku Solver](https://leetcode.com/problems/sudoku-solver/description/?envType=daily-question) |
| 2 | + |
| 3 | +## Initial thoughts (stream-of-consciousness) |
| 4 | +This is a classic backtracking problem: fill each '.' with digits '1'..'9' while respecting row/column/3x3-box constraints. A naive backtracking that tries digits 1..9 for each empty cell will work but can be slow for harder puzzles. To speed up, maintain which digits are already used in each row/column/box so candidate checks are O(1). Additionally, use heuristics like choosing the empty cell with the fewest candidates (minimum remaining values, MRV) to prune the search faster. Bitmasks are a compact, fast way to represent used digits for rows/cols/boxes and to compute available candidates quickly. |
| 5 | + |
| 6 | +## Refining the problem, round 2 thoughts |
| 7 | +Refine to an implementation that: |
| 8 | +- Precomputes used digits for each row, column, and box as 9-bit masks (bits 0..8 map to digits 1..9). |
| 9 | +- Builds a list of empty cell coordinates. |
| 10 | +- Uses recursive DFS with MRV: at each step, scan the empties to pick the cell with the smallest number of candidate digits; if some cell has 0 candidates, backtrack immediately. |
| 11 | +- For candidate iteration, extract low bit repeatedly (mask & -mask) to iterate candidates efficiently. |
| 12 | +- Modify the input board in place (LeetCode requirement) and stop once a solution is found. |
| 13 | + |
| 14 | +Complexity: worst-case exponential in number of empties (m), roughly O(9^m), but MRV + constraint propagation prunes heavily. Space complexity: O(m) for recursion and empties list plus O(1) for bitmasks. |
| 15 | + |
| 16 | +## Attempted solution(s) |
| 17 | +```python |
| 18 | +class Solution: |
| 19 | + def solveSudoku(self, board: list[list[str]]) -> None: |
| 20 | + """ |
| 21 | + Modify board in-place to fill the Sudoku solution. |
| 22 | + """ |
| 23 | + # Helper: get box index |
| 24 | + def box_index(r: int, c: int) -> int: |
| 25 | + return (r // 3) * 3 + (c // 3) |
| 26 | + |
| 27 | + # Bit representation: bit 0 -> digit '1', bit 1 -> '2', ..., bit 8 -> '9' |
| 28 | + rows = [0] * 9 |
| 29 | + cols = [0] * 9 |
| 30 | + boxes = [0] * 9 |
| 31 | + empties = [] |
| 32 | + |
| 33 | + # Initialize masks and empties list |
| 34 | + for r in range(9): |
| 35 | + for c in range(9): |
| 36 | + ch = board[r][c] |
| 37 | + if ch == '.': |
| 38 | + empties.append((r, c)) |
| 39 | + else: |
| 40 | + d = int(ch) - 1 |
| 41 | + bit = 1 << d |
| 42 | + rows[r] |= bit |
| 43 | + cols[c] |= bit |
| 44 | + boxes[box_index(r, c)] |= bit |
| 45 | + |
| 46 | + FULL_MASK = (1 << 9) - 1 # 0x1FF, bits for digits 1..9 |
| 47 | + |
| 48 | + # DFS with MRV (choose empty cell with minimal candidates) |
| 49 | + def dfs() -> bool: |
| 50 | + # If no empties left, solved |
| 51 | + if not empties: |
| 52 | + return True |
| 53 | + |
| 54 | + # Find the empty cell with the fewest candidates |
| 55 | + best_i = -1 |
| 56 | + best_count = 10 |
| 57 | + best_mask = 0 |
| 58 | + for i, (r, c) in enumerate(empties): |
| 59 | + if board[r][c] != '.': |
| 60 | + continue # already filled by earlier step |
| 61 | + mask = ~(rows[r] | cols[c] | boxes[box_index(r, c)]) & FULL_MASK |
| 62 | + cnt = mask.bit_count() |
| 63 | + if cnt == 0: |
| 64 | + return False # dead end |
| 65 | + if cnt < best_count: |
| 66 | + best_count = cnt |
| 67 | + best_i = i |
| 68 | + best_mask = mask |
| 69 | + if cnt == 1: |
| 70 | + break # can't get better than 1 |
| 71 | + |
| 72 | + # best_i should be valid |
| 73 | + r, c = empties[best_i] |
| 74 | + bidx = box_index(r, c) |
| 75 | + |
| 76 | + # Try each candidate digit (iterate bits) |
| 77 | + mask = best_mask |
| 78 | + while mask: |
| 79 | + bit = mask & -mask # lowest set bit |
| 80 | + d = (bit.bit_length() - 1) # 0-based digit index |
| 81 | + # place digit |
| 82 | + board[r][c] = str(d + 1) |
| 83 | + rows[r] |= bit |
| 84 | + cols[c] |= bit |
| 85 | + boxes[bidx] |= bit |
| 86 | + |
| 87 | + # recurse |
| 88 | + if dfs(): |
| 89 | + return True |
| 90 | + |
| 91 | + # backtrack |
| 92 | + board[r][c] = '.' |
| 93 | + rows[r] &= ~bit |
| 94 | + cols[c] &= ~bit |
| 95 | + boxes[bidx] &= ~bit |
| 96 | + |
| 97 | + mask &= mask - 1 # clear lowest set bit |
| 98 | + |
| 99 | + return False |
| 100 | + |
| 101 | + # Kick off DFS |
| 102 | + dfs() |
| 103 | +``` |
| 104 | +- Notes: |
| 105 | + - Approach: Depth-first search (backtracking) with bitmasking for quick validity checks and MRV (choose the empty with fewest candidates) for pruning. |
| 106 | + - Time complexity: worst-case exponential, O(9^m) where m is the number of empty cells. Practical performance is far better due to constraint propagation and MRV. |
| 107 | + - Space complexity: O(m) for the recursion stack and empties list, plus O(1) for the 27 masks (rows/cols/boxes). |
| 108 | + - Implementation details: |
| 109 | + - Use integer bitmasks for rows/cols/boxes to make candidate computations and updates O(1). |
| 110 | + - Use mask & -mask and bit_length() to iterate over possible digits quickly. |
| 111 | + - Modify the input board in-place as required. The dfs stops when a valid filling is found. |
0 commit comments