diff --git a/backtracking/crossword_puzzle_solver.py b/backtracking/crossword_puzzle_solver.py index e702c7e52153..e510909cfc41 100644 --- a/backtracking/crossword_puzzle_solver.py +++ b/backtracking/crossword_puzzle_solver.py @@ -6,29 +6,28 @@ def is_valid( ) -> bool: """ Check if a word can be placed at the given position. + A cell is valid if it is empty or already contains the correct letter + (enabling crossing/intersection between words). - >>> puzzle = [ - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''] - ... ] + >>> puzzle = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] >>> is_valid(puzzle, 'word', 0, 0, True) True - >>> puzzle = [ - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''] - ... ] >>> is_valid(puzzle, 'word', 0, 0, False) True + >>> puzzle2 = [['w', '', ''], ['o', '', ''], ['r', '', ''], ['d', '', '']] + >>> is_valid(puzzle2, 'word', 0, 0, True) + True + >>> is_valid(puzzle2, 'cat', 0, 0, True) + False """ - for i in range(len(word)): - if vertical: - if row + i >= len(puzzle) or puzzle[row + i][col] != "": - return False - elif col + i >= len(puzzle[0]) or puzzle[row][col + i] != "": + rows, cols = len(puzzle), len(puzzle[0]) + for i, ch in enumerate(word): + r, c = (row + i, col) if vertical else (row, col + i) + if r >= rows or c >= cols: + return False + cell = puzzle[r][c] + if cell not in ("", ch): return False return True @@ -37,95 +36,87 @@ def place_word( puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool ) -> None: """ - Place a word at the given position. - - >>> puzzle = [ - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''] - ... ] + Place a word at the given position in the puzzle. + + >>> puzzle = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] >>> place_word(puzzle, 'word', 0, 0, True) >>> puzzle [['w', '', '', ''], ['o', '', '', ''], ['r', '', '', ''], ['d', '', '', '']] """ - for i, char in enumerate(word): + for i, ch in enumerate(word): if vertical: - puzzle[row + i][col] = char + puzzle[row + i][col] = ch else: - puzzle[row][col + i] = char + puzzle[row][col + i] = ch def remove_word( - puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool + puzzle: list[list[str]], + word: str, + row: int, + col: int, + vertical: bool, + snapshot: list[list[str]], ) -> None: """ - Remove a word from the given position. - - >>> puzzle = [ - ... ['w', '', '', ''], - ... ['o', '', '', ''], - ... ['r', '', '', ''], - ... ['d', '', '', ''] - ... ] - >>> remove_word(puzzle, 'word', 0, 0, True) + Remove a word from the puzzle, restoring only cells that were empty + before placement. Cells shared with crossing words are preserved. + + >>> puzzle = [['w', 'o', 'r', 'd'], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> snap = [['', 'o', 'r', 'd'], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> remove_word(puzzle, 'word', 0, 0, False, snap) >>> puzzle - [['', '', '', ''], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']] + [['', 'o', 'r', 'd'], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']] """ for i in range(len(word)): - if vertical: - puzzle[row + i][col] = "" - else: - puzzle[row][col + i] = "" + r, c = (row + i, col) if vertical else (row, col + i) + if snapshot[r][c] == "": + puzzle[r][c] = "" def solve_crossword(puzzle: list[list[str]], words: list[str]) -> bool: """ Solve the crossword puzzle using backtracking. + Words are tried longest-first to prune the search space early. + Intersections between words (shared letters) are supported. - >>> puzzle = [ - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''] - ... ] - - >>> words = ['word', 'four', 'more', 'last'] - >>> solve_crossword(puzzle, words) + >>> puzzle = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> solve_crossword(puzzle, ['word', 'four', 'more', 'last']) True - >>> puzzle = [ - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''], - ... ['', '', '', ''] - ... ] - >>> words = ['word', 'four', 'more', 'paragraphs'] - >>> solve_crossword(puzzle, words) + >>> puzzle2 = [['', '', '', ''], ['', '', '', ''], + ... ['', '', '', ''], ['', '', '', '']] + >>> solve_crossword(puzzle2, ['word', 'four', 'more', 'paragraphs']) False """ + if not words: + return True + + remaining = sorted(words, key=len, reverse=True) + word, rest = remaining[0], remaining[1:] + for row in range(len(puzzle)): for col in range(len(puzzle[0])): - if puzzle[row][col] == "": - for word in words: - for vertical in [True, False]: - if is_valid(puzzle, word, row, col, vertical): - place_word(puzzle, word, row, col, vertical) - words.remove(word) - if solve_crossword(puzzle, words): - return True - words.append(word) - remove_word(puzzle, word, row, col, vertical) - return False - return True + for vertical in (True, False): + if is_valid(puzzle, word, row, col, vertical): + snapshot = [r[:] for r in puzzle] + place_word(puzzle, word, row, col, vertical) + if solve_crossword(puzzle, rest): + return True + remove_word(puzzle, word, row, col, vertical, snapshot) + + return False if __name__ == "__main__": PUZZLE = [[""] * 3 for _ in range(3)] WORDS = ["cat", "dog", "car"] - if solve_crossword(PUZZLE, WORDS): print("Solution found:") for row in PUZZLE: - print(" ".join(row)) + print(" ".join(cell or "." for cell in row)) else: - print("No solution found:") + print("No solution found.")