From 5a8be7cd3c712b94bf8fdd596b289a76ca8957b1 Mon Sep 17 00:00:00 2001 From: Anubhavpandey27 <61093307+Anubhavpandey27@users.noreply.github.com> Date: Tue, 17 Oct 2023 02:07:36 +0530 Subject: [PATCH 1/6] Create Sudoku_Solver Each of the digits 1-9 must occur exactly once in each row. Each of the digits 1-9 must occur exactly once in each column. Each of the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid. The '.' character indicates empty cells. --- data_structures/arrays/Sudoku_Solver | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 data_structures/arrays/Sudoku_Solver diff --git a/data_structures/arrays/Sudoku_Solver b/data_structures/arrays/Sudoku_Solver new file mode 100644 index 000000000000..f83dc0e91072 --- /dev/null +++ b/data_structures/arrays/Sudoku_Solver @@ -0,0 +1,44 @@ +class Solution: + def solveSudoku(self, board: List[List[str]]) -> None: + n = 9 + + + def isValid(row, col, ch): + row, col = int(row), int(col) + + for i in range(9): + + if board[i][col] == ch: + return False + if board[row][i] == ch: + return False + + if board[3*(row//3) + i//3][3*(col//3) + i%3] == ch: + return False + + return True + + def solve(row, col): + if row == n: + return True + if col == n: + return solve(row+1, 0) + + if board[row][col] == ".": + for i in range(1, 10): + if isValid(row, col, str(i)): + board[row][col] = str(i) + + if solve(row, col + 1): + return True + else: + board[row][col] = "." + return False + else: + return solve(row, col + 1) + + + + solve(0, 0) + + #do upvote if it helps. From 5d9209c1e5b0e2eae823cdfd3d3b90b50d7d3959 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:42:39 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- data_structures/arrays/Sudoku_Solver | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/data_structures/arrays/Sudoku_Solver b/data_structures/arrays/Sudoku_Solver index f83dc0e91072..4ba8c06b683a 100644 --- a/data_structures/arrays/Sudoku_Solver +++ b/data_structures/arrays/Sudoku_Solver @@ -1,34 +1,34 @@ class Solution: def solveSudoku(self, board: List[List[str]]) -> None: n = 9 - - + + def isValid(row, col, ch): row, col = int(row), int(col) - + for i in range(9): - + if board[i][col] == ch: return False if board[row][i] == ch: return False - + if board[3*(row//3) + i//3][3*(col//3) + i%3] == ch: return False - + return True - + def solve(row, col): if row == n: return True if col == n: return solve(row+1, 0) - + if board[row][col] == ".": for i in range(1, 10): if isValid(row, col, str(i)): board[row][col] = str(i) - + if solve(row, col + 1): return True else: @@ -36,9 +36,9 @@ class Solution: return False else: return solve(row, col + 1) - - - + + + solve(0, 0) - + #do upvote if it helps. From 5bba168e26795ff279eb26ac02dd258fd913f274 Mon Sep 17 00:00:00 2001 From: Anubhavpandey27 <61093307+Anubhavpandey27@users.noreply.github.com> Date: Tue, 17 Oct 2023 02:14:12 +0530 Subject: [PATCH 3/6] Rename Sudoku_Solver to sudoku_solver.py --- data_structures/arrays/{Sudoku_Solver => sudoku_solver.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data_structures/arrays/{Sudoku_Solver => sudoku_solver.py} (100%) diff --git a/data_structures/arrays/Sudoku_Solver b/data_structures/arrays/sudoku_solver.py similarity index 100% rename from data_structures/arrays/Sudoku_Solver rename to data_structures/arrays/sudoku_solver.py From 3a0970cb9ea63cbb7bba4446f1f5b93d364765bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:44:48 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- data_structures/arrays/sudoku_solver.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py index 4ba8c06b683a..14c5d82f9973 100644 --- a/data_structures/arrays/sudoku_solver.py +++ b/data_structures/arrays/sudoku_solver.py @@ -2,18 +2,16 @@ class Solution: def solveSudoku(self, board: List[List[str]]) -> None: n = 9 - def isValid(row, col, ch): row, col = int(row), int(col) for i in range(9): - if board[i][col] == ch: return False if board[row][i] == ch: return False - if board[3*(row//3) + i//3][3*(col//3) + i%3] == ch: + if board[3 * (row // 3) + i // 3][3 * (col // 3) + i % 3] == ch: return False return True @@ -22,7 +20,7 @@ def solve(row, col): if row == n: return True if col == n: - return solve(row+1, 0) + return solve(row + 1, 0) if board[row][col] == ".": for i in range(1, 10): @@ -37,8 +35,7 @@ def solve(row, col): else: return solve(row, col + 1) - - solve(0, 0) - #do upvote if it helps. + +# do upvote if it helps. From c225e8587bac8df76eafa2523928b052d05942c4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 17 Oct 2023 19:03:33 +0200 Subject: [PATCH 5/6] Update sudoku_solver.py --- data_structures/arrays/sudoku_solver.py | 254 ++++++++++++++++++++---- 1 file changed, 217 insertions(+), 37 deletions(-) diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py index 14c5d82f9973..07c12848c4d5 100644 --- a/data_structures/arrays/sudoku_solver.py +++ b/data_structures/arrays/sudoku_solver.py @@ -1,41 +1,221 @@ -class Solution: - def solveSudoku(self, board: List[List[str]]) -> None: - n = 9 - - def isValid(row, col, ch): - row, col = int(row), int(col) - - for i in range(9): - if board[i][col] == ch: - return False - if board[row][i] == ch: - return False - - if board[3 * (row // 3) + i // 3][3 * (col // 3) + i % 3] == ch: - return False - - return True - - def solve(row, col): - if row == n: - return True - if col == n: - return solve(row + 1, 0) - - if board[row][col] == ".": - for i in range(1, 10): - if isValid(row, col, str(i)): - board[row][col] = str(i) - - if solve(row, col + 1): - return True - else: - board[row][col] = "." +""" +Please do not modify this file! It is published at https://norvig.com/sudoku.html with +only minimal changes to work with modern versions of Python. If you have improvements, +please make them in a separate file. +""" +import random +import time + + +def cross(items_a, items_b): + "Cross product of elements in A and elements in B." + return [a + b for a in items_a for b in items_b] + + +digits = "123456789" +rows = "ABCDEFGHI" +cols = digits +squares = cross(rows, cols) +unitlist = ( + [cross(rows, c) for c in cols] + + [cross(r, cols) for r in rows] + + [cross(rs, cs) for rs in ("ABC", "DEF", "GHI") for cs in ("123", "456", "789")] +) +units = {s: [u for u in unitlist if s in u] for s in squares} +peers = {s: set(sum(units[s], [])) - {s} for s in squares} + + +def test(): + "A set of unit tests." + assert len(squares) == 81 + assert len(unitlist) == 27 + assert all(len(units[s]) == 3 for s in squares) + assert all(len(peers[s]) == 20 for s in squares) + assert units["C2"] == [ + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2", "I2"], + ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"], + ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"], + ] + # fmt: off + assert peers["C2"] == { + "A2", "B2", "D2", "E2", "F2", "G2", "H2", "I2", "C1", "C3", + "C4", "C5", "C6", "C7", "C8", "C9", "A1", "A3", "B1", "B3" + } + # fmt: on + print("All tests pass.") + + +def parse_grid(grid): + """Convert grid to a dict of possible values, {square: digits}, or + return False if a contradiction is detected.""" + ## To start, every square can be any digit; then assign values from the grid. + values = {s: digits for s in squares} + for s, d in grid_values(grid).items(): + if d in digits and not assign(values, s, d): + return False ## (Fail if we can't assign d to square s.) + return values + + +def grid_values(grid): + "Convert grid into a dict of {square: char} with '0' or '.' for empties." + chars = [c for c in grid if c in digits or c in "0."] + assert len(chars) == 81 + return dict(zip(squares, chars)) + + +def assign(values, s, d): + """Eliminate all the other values (except d) from values[s] and propagate. + Return values, except return False if a contradiction is detected.""" + other_values = values[s].replace(d, "") + if all(eliminate(values, s, d2) for d2 in other_values): + return values + else: + return False + + +def eliminate(values, s, d): + """Eliminate d from values[s]; propagate when values or places <= 2. + Return values, except return False if a contradiction is detected.""" + if d not in values[s]: + return values ## Already eliminated + values[s] = values[s].replace(d, "") + ## (1) If a square s is reduced to one value d2, then eliminate d2 from the peers. + if len(values[s]) == 0: + return False ## Contradiction: removed last value + elif len(values[s]) == 1: + d2 = values[s] + if not all(eliminate(values, s2, d2) for s2 in peers[s]): + return False + ## (2) If a unit u is reduced to only one place for a value d, then put it there. + for u in units[s]: + dplaces = [s for s in u if d in values[s]] + if len(dplaces) == 0: + return False ## Contradiction: no place for this value + elif len(dplaces) == 1: + # d can only be in one place in unit; assign it there + if not assign(values, dplaces[0], d): return False - else: - return solve(row, col + 1) + return values + + +def display(values): + "Display these values as a 2-D grid." + width = 1 + max(len(values[s]) for s in squares) + line = "+".join(["-" * (width * 3)] * 3) + for r in rows: + print( + "".join( + values[r + c].center(width) + ("|" if c in "36" else "") for c in cols + ) + ) + if r in "CF": + print(line) + print() + + +def solve(grid): + return search(parse_grid(grid)) + + +def some(seq): + "Return some element of seq that is true." + for e in seq: + if e: + return e + return False + + +def search(values): + "Using depth-first search and propagation, try all possible values." + if values is False: + return False ## Failed earlier + if all(len(values[s]) == 1 for s in squares): + return values ## Solved! + ## Chose the unfilled square s with the fewest possibilities + n, s = min((len(values[s]), s) for s in squares if len(values[s]) > 1) + return some(search(assign(values.copy(), s, d)) for d in values[s]) + + +def solve_all(grids, name="", showif=0.0): + """Attempt to solve a sequence of grids. Report results. + When showif is a number of seconds, display puzzles that take longer. + When showif is None, don't display any puzzles.""" + + def time_solve(grid): + start = time.monotonic() + values = solve(grid) + t = time.monotonic() - start + ## Display puzzles that take long enough + if showif is not None and t > showif: + display(grid_values(grid)) + if values: + display(values) + print("(%.5f seconds)\n" % t) + return (t, solved(values)) + + times, results = zip(*[time_solve(grid) for grid in grids]) + n = len(grids) + if n > 1: + print( + "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)." + % (sum(results), n, name, sum(times) / n, n / sum(times), max(times)) + ) + + +def solved(values): + "A puzzle is solved if each unit is a permutation of the digits 1 to 9." + + def unitsolved(unit): + return {values[s] for s in unit} == set(digits) + + return values is not False and all(unitsolved(unit) for unit in unitlist) + + +def from_file(filename, sep="\n"): + "Parse a file into a list of strings, separated by sep." + return open(filename).read().strip().split(sep) # noqa: SIM115 + + +def random_puzzle(assignments=17): + """Make a random puzzle with N or more assignments. Restart on contradictions. + Note the resulting puzzle is not guaranteed to be solvable, but empirically + about 99.8% of them are solvable. Some have multiple solutions.""" + values = {s: digits for s in squares} + for s in shuffled(squares): + if not assign(values, s, random.choice(values[s])): + break + ds = [values[s] for s in squares if len(values[s]) == 1] + if len(ds) >= assignments and len(set(ds)) >= 8: + return "".join(values[s] if len(values[s]) == 1 else "." for s in squares) + return random_puzzle(assignments) ## Give up and make a new puzzle + + +def shuffled(seq): + "Return a randomly shuffled copy of the input sequence." + seq = list(seq) + random.shuffle(seq) + return seq - solve(0, 0) +grid1 = ( + "003020600900305001001806400008102900700000008006708200002609500800203009005010300" +) +grid2 = ( + "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......" +) +hard1 = ( + ".....6....59.....82....8....45........3........6..3.54...325..6.................." +) -# do upvote if it helps. +if __name__ == "__main__": + test() + # solve_all(from_file("easy50.txt", '========'), "easy", None) + # solve_all(from_file("top95.txt"), "hard", None) + # solve_all(from_file("hardest.txt"), "hardest", None) + solve_all([random_puzzle() for _ in range(99)], "random", 100.0) + for puzzle in (grid1, grid2): # , hard1): # Takes 22 sec to solve on my M1 Mac. + display(parse_grid(puzzle)) + start = time.monotonic() + solve(puzzle) + t = time.monotonic() - start + print("Solved: %.5f sec" % t) From 5e09306e2a17b42362fc159550c9f30ffb7fe3fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:04:24 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- data_structures/arrays/sudoku_solver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py index 07c12848c4d5..8d38bd7295ea 100644 --- a/data_structures/arrays/sudoku_solver.py +++ b/data_structures/arrays/sudoku_solver.py @@ -154,8 +154,7 @@ def time_solve(grid): return (t, solved(values)) times, results = zip(*[time_solve(grid) for grid in grids]) - n = len(grids) - if n > 1: + if (n := len(grids)) > 1: print( "Solved %d of %d %s puzzles (avg %.2f secs (%d Hz), max %.2f secs)." % (sum(results), n, name, sum(times) / n, n / sum(times), max(times))