Source code for dedoku.backtracking

"""Opt-in brute-force completion.

Backtracking is **not** a logical technique, and this module is
deliberately kept outside :mod:`dedoku.techniques`: the default pipeline
never touches it. It exists for the explicit ``hybrid`` and
``backtracking`` solving modes, where the caller chooses to trade
explainability for a guaranteed answer.

The search runs over the grid's *current candidates*, so every candidate
already removed by sound logic narrows it — in hybrid mode the brute-force
tail is typically tiny.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .grid import Grid

__all__ = ["complete_with_backtracking"]


[docs] def complete_with_backtracking(grid: Grid) -> bool: """Complete ``grid`` by depth-first search over its candidates. Solved cells are taken as fixed; unsolved cells only try digits still among their candidates. On success the found values are placed on the grid (with normal propagation); on failure the grid is left untouched. :param grid: The board to complete in place. :type grid: Grid :returns: ``True`` when a completion was found and applied, ``False`` when no completion exists for the current board state. :rtype: bool """ unsolved = [cell for cell in grid.cells if not cell.is_solved] if not unsolved: return grid.is_valid() rows = [0] * 9 cols = [0] * 9 boxes = [0] * 9 for cell in grid.cells: if cell.value is not None: bit = 1 << cell.value rows[cell.row_index] |= bit cols[cell.column_index] |= bit boxes[cell.subgrid.index] |= bit allowed = { cell: sum(1 << digit for digit in cell.candidates) for cell in unsolved } values: dict = {} def recurse(remaining: list) -> bool: if not remaining: return True # Most-constrained cell first keeps the search shallow. best_index = -1 best_options = 10 for index, cell in enumerate(remaining): used = (rows[cell.row_index] | cols[cell.column_index] | boxes[cell.subgrid.index]) options = bin(allowed[cell] & ~used).count("1") if options < best_options: best_options, best_index = options, index if options <= 1: break if best_options == 0: return False cell = remaining[best_index] rest = remaining[:best_index] + remaining[best_index + 1:] used = (rows[cell.row_index] | cols[cell.column_index] | boxes[cell.subgrid.index]) mask = allowed[cell] & ~used for digit in range(1, 10): bit = 1 << digit if not mask & bit: continue rows[cell.row_index] |= bit cols[cell.column_index] |= bit boxes[cell.subgrid.index] |= bit values[cell] = digit if recurse(rest): return True del values[cell] rows[cell.row_index] ^= bit cols[cell.column_index] ^= bit boxes[cell.subgrid.index] ^= bit return False if not recurse(unsolved): return False for cell in unsolved: if not cell.is_solved: # propagation may have solved it already cell.set_value(values[cell]) return True