Algorithm for solving Sudoku

后端 未结 10 527
忘了有多久
忘了有多久 2020-12-02 08:13

I want to write a code in python to solve a sudoku puzzle. Do you guys have any idea about a good algorithm for this purpose. I read somewhere in net about a algorithm which

相关标签:
10条回答
  • 2020-12-02 08:29

    Here is a much faster solution based on hari's answer. The basic difference is that we keep a set of possible values for cells that don't have a value assigned. So when we try a new value, we only try valid values and we also propagate what this choice means for the rest of the sudoku. In the propagation step, we remove from the set of valid values for each cell the values that already appear in the row, column, or the same block. If only one number is left in the set, we know that the position (cell) has to have that value.

    This method is known as forward checking and look ahead (http://ktiml.mff.cuni.cz/~bartak/constraints/propagation.html).

    The implementation below needs one iteration (calls of solve) while hari's implementation needs 487. Of course my code is a bit longer. The propagate method is also not optimal.

    import sys
    from copy import deepcopy
    
    def output(a):
        sys.stdout.write(str(a))
    
    N = 9
    
    field = [[5,1,7,6,0,0,0,3,4],
             [2,8,9,0,0,4,0,0,0],
             [3,4,6,2,0,5,0,9,0],
             [6,0,2,0,0,0,0,1,0],
             [0,3,8,0,0,6,0,4,7],
             [0,0,0,0,0,0,0,0,0],
             [0,9,0,0,0,0,0,7,8],
             [7,0,3,4,0,0,5,6,0],
             [0,0,0,0,0,0,0,0,0]]
    
    def print_field(field):
        if not field:
            output("No solution")
            return
        for i in range(N):
            for j in range(N):
                cell = field[i][j]
                if cell == 0 or isinstance(cell, set):
                    output('.')
                else:
                    output(cell)
                if (j + 1) % 3 == 0 and j < 8:
                    output(' |')
    
                if j != 8:
                    output(' ')
            output('\n')
            if (i + 1) % 3 == 0 and i < 8:
                output("- - - + - - - + - - -\n")
    
    def read(field):
        """ Read field into state (replace 0 with set of possible values) """
    
        state = deepcopy(field)
        for i in range(N):
            for j in range(N):
                cell = state[i][j]
                if cell == 0:
                    state[i][j] = set(range(1,10))
    
        return state
    
    state = read(field)
    
    
    def done(state):
        """ Are we done? """
    
        for row in state:
            for cell in row:
                if isinstance(cell, set):
                    return False
        return True
    
    
    def propagate_step(state):
        """
        Propagate one step.
    
        @return:  A two-tuple that says whether the configuration
                  is solvable and whether the propagation changed
                  the state.
        """
    
                new_units = False
    
        # propagate row rule
        for i in range(N):
            row = state[i]
            values = set([x for x in row if not isinstance(x, set)])
            for j in range(N):
                if isinstance(state[i][j], set):
                    state[i][j] -= values
                    if len(state[i][j]) == 1:
                        val = state[i][j].pop()
                        state[i][j] = val
                        values.add(val)
                        new_units = True
                    elif len(state[i][j]) == 0:
                        return False, None
    
        # propagate column rule
        for j in range(N):
            column = [state[x][j] for x in range(N)]
            values = set([x for x in column if not isinstance(x, set)])
            for i in range(N):
                if isinstance(state[i][j], set):
                    state[i][j] -= values
                    if len(state[i][j]) == 1:
                        val = state[i][j].pop()
                        state[i][j] = val
                        values.add(val)
                        new_units = True
                    elif len(state[i][j]) == 0:
                        return False, None
    
        # propagate cell rule
        for x in range(3):
            for y in range(3):
                values = set()
                for i in range(3 * x, 3 * x + 3):
                    for j in range(3 * y, 3 * y + 3):
                        cell = state[i][j]
                        if not isinstance(cell, set):
                            values.add(cell)
                for i in range(3 * x, 3 * x + 3):
                    for j in range(3 * y, 3 * y + 3):
                        if isinstance(state[i][j], set):
                            state[i][j] -= values
                            if len(state[i][j]) == 1:
                                val = state[i][j].pop()
                                state[i][j] = val
                                values.add(val)
                                new_units = True
                            elif len(state[i][j]) == 0:
                                return False, None
    
        return True, new_units
    
    def propagate(state):
        """ Propagate until we reach a fixpoint """
        while True:
            solvable, new_unit = propagate_step(state)
            if not solvable:
                return False
            if not new_unit:
                return True
    
    
    def solve(state):
        """ Solve sudoku """
    
        solvable = propagate(state)
    
        if not solvable:
            return None
    
        if done(state):
            return state
    
        for i in range(N):
            for j in range(N):
                cell = state[i][j]
                if isinstance(cell, set):
                    for value in cell:
                        new_state = deepcopy(state)
                        new_state[i][j] = value
                        solved = solve(new_state)
                        if solved is not None:
                            return solved
                    return None
    
    print_field(solve(state))
    
    0 讨论(0)
  • 2020-12-02 08:34

    Using google ortools - the following will either generate a dummy sudoku array or will solve a candidate. The code is probably more verbose than required, any feedback is appreciated.

    The idea is to solve a constraint-programming problem that involves

    1. List of 81 variables with integer bounds between 1 and 9.
    2. All different constraint for row vector
    3. All different constraint for column vector
    4. All different constraint for the sub-matrices

    In addition, when trying to solve existing sudoku, we add additional constraints on variables that already have assigned value.

    from ortools.constraint_solver import pywrapcp
    import numpy as np
    
    def sudoku_solver(candidate = None):
        solver = pywrapcp.Solver("Sudoku")
    
        variables = [solver.IntVar(1,9,f"x{i}") for i in range(81)]
        if len(candidate)>0:
            candidate = np.int64(candidate)
            for i in range(81):
                val = candidate[i]
                if val !=0:
                    solver.Add(variables[i] == int(val))
    
        def set_constraints():
            for i in range(9):
                # All columns should be different
                q=[variables[j] for j in list(range(i,81,9))]
                solver.Add(solver.AllDifferent(q))
    
                #All rows should be different
                q2=[variables[j] for j in list(range(i*9,(i+1)*9))]
                solver.Add(solver.AllDifferent(q2))
    
                #All values in the sub-matrix should be different
                a = list(range(81))
                sub_blocks = a[3*i:3*(i+9):9] + a[3*i+1:3*(i+9)+1:9] + a[3*i+2:3*(i+9)+2:9]
                q3 = [variables[j] for j in sub_blocks]
                solver.Add(solver.AllDifferent(q3))
                
        set_constraints()
        db = solver.Phase(variables, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE)
        solver.NewSearch(db)    
        
        results_store =[]
        num_solutions =0
        total_solutions = 5
        while solver.NextSolution() and num_solutions<total_solutions:
            results = [j.Value() for j in variables]
            results_store.append(results)
            num_solutions +=1
            
        return results_store
    

    Solve the following sudoku

    candidate = np.array([0, 2, 0, 4, 5, 6, 0, 8, 0, 0, 5, 6, 7, 8, 9, 0, 0, 3, 7, 0, 9, 0,
           2, 0, 4, 5, 6, 2, 0, 1, 5, 0, 4, 8, 9, 7, 5, 0, 4, 8, 0, 0, 0, 0,
           0, 3, 1, 0, 6, 4, 5, 9, 7, 0, 0, 0, 5, 0, 7, 8, 3, 1, 2, 8, 0, 7,
           0, 1, 0, 5, 0, 4, 9, 7, 8, 0, 3, 0, 0, 0, 5])
    
    
    results_store = sudoku_solver(candidate)  
    
    0 讨论(0)
  • 2020-12-02 08:36

    There are four steps to solve a sudoku puzzle:

    1. Identify all possibilities for each cell (getting from the row, column and box) and try to develop a possible matrix. 2.Check for double pair, if it exists then remove these two values from all the cells in that row/column/box, wherever the pair exists If any cell is having single possiblity then assign that run step 1 again
    2. Check for each cell with each row, column and box. If the cell has one value which does not belong in the other possible values then assign that value to that cell. run step 1 again
    3. If the sudoku is still not solved, then we need to start the following assumption, Assume the first possible value and assign. Then run step 1–3 If still not solved then do it for next possible value and run it in recursion.
    4. If the sudoku is still not solved, then we need to start the following assumption, Assume the first possible value and assign. Then run step 1–3

    If still not solved then do it for next possible value and run it in recursion.

    import math
    import sys
    
    
    def is_solved(l):
        for x, i in enumerate(l):
            for y, j in enumerate(i):
                if j == 0:
                    # Incomplete
                    return None
                for p in range(9):
                    if p != x and j == l[p][y]:
                        # Error
                        print('horizontal issue detected!', (x, y))
                        return False
                    if p != y and j == l[x][p]:
                        # Error
                        print('vertical issue detected!', (x, y))
                        return False
                i_n, j_n = get_box_start_coordinate(x, y)
                for (i, j) in [(i, j) for p in range(i_n, i_n + 3) for q in range(j_n, j_n + 3)
                               if (p, q) != (x, y) and j == l[p][q]]:
                        # Error
                    print('box issue detected!', (x, y))
                    return False
        # Solved
        return True
    
    
    def is_valid(l):
        for x, i in enumerate(l):
            for y, j in enumerate(i):
                if j != 0:
                    for p in range(9):
                        if p != x and j == l[p][y]:
                            # Error
                            print('horizontal issue detected!', (x, y))
                            return False
                        if p != y and j == l[x][p]:
                            # Error
                            print('vertical issue detected!', (x, y))
                            return False
                    i_n, j_n = get_box_start_coordinate(x, y)
                    for (i, j) in [(i, j) for p in range(i_n, i_n + 3) for q in range(j_n, j_n + 3)
                                   if (p, q) != (x, y) and j == l[p][q]]:
                            # Error
                        print('box issue detected!', (x, y))
                        return False
        # Solved
        return True
    
    
    def get_box_start_coordinate(x, y):
        return 3 * int(math.floor(x/3)), 3 * int(math.floor(y/3))
    
    
    def get_horizontal(x, y, l):
        return [l[x][i] for i in range(9) if l[x][i] > 0]
    
    
    def get_vertical(x, y, l):
        return [l[i][y] for i in range(9) if l[i][y] > 0]
    
    
    def get_box(x, y, l):
        existing = []
        i_n, j_n = get_box_start_coordinate(x, y)
        for (i, j) in [(i, j) for i in range(i_n, i_n + 3) for j in range(j_n, j_n + 3)]:
            existing.append(l[i][j]) if l[i][j] > 0 else None
        return existing
    
    
    def detect_and_simplify_double_pairs(l, pl):
        for (i, j) in [(i, j) for i in range(9) for j in range(9) if len(pl[i][j]) == 2]:
            temp_pair = pl[i][j]
            for p in (p for p in range(j+1, 9) if len(pl[i][p]) == 2 and len(set(pl[i][p]) & set(temp_pair)) == 2):
                for q in (q for q in range(9) if q != j and q != p):
                    pl[i][q] = list(set(pl[i][q]) - set(temp_pair))
                    if len(pl[i][q]) == 1:
                        l[i][q] = pl[i][q].pop()
                        return True
            for p in (p for p in range(i+1, 9) if len(pl[p][j]) == 2 and len(set(pl[p][j]) & set(temp_pair)) == 2):
                for q in (q for q in range(9) if q != i and p != q):
                    pl[q][j] = list(set(pl[q][j]) - set(temp_pair))
                    if len(pl[q][j]) == 1:
                        l[q][j] = pl[q][j].pop()
                        return True
            i_n, j_n = get_box_start_coordinate(i, j)
            for (a, b) in [(a, b) for a in range(i_n, i_n+3) for b in range(j_n, j_n+3)
                           if (a, b) != (i, j) and len(pl[a][b]) == 2 and len(set(pl[a][b]) & set(temp_pair)) == 2]:
                for (c, d) in [(c, d) for c in range(i_n, i_n+3) for d in range(j_n, j_n+3)
                               if (c, d) != (a, b) and (c, d) != (i, j)]:
                    pl[c][d] = list(set(pl[c][d]) - set(temp_pair))
                    if len(pl[c][d]) == 1:
                        l[c][d] = pl[c][d].pop()
                        return True
        return False
    
    
    def update_unique_horizontal(x, y, l, pl):
        tl = pl[x][y]
        for i in (i for i in range(9) if i != y):
            tl = list(set(tl) - set(pl[x][i]))
        if len(tl) == 1:
            l[x][y] = tl.pop()
            return True
        return False
    
    
    def update_unique_vertical(x, y, l, pl):
        tl = pl[x][y]
        for i in (i for i in range(9) if i != x):
            tl = list(set(tl) - set(pl[i][y]))
        if len(tl) == 1:
            l[x][y] = tl.pop()
            return True
        return False
    
    
    def update_unique_box(x, y, l, pl):
        tl = pl[x][y]
        i_n, j_n = get_box_start_coordinate(x, y)
        for (i, j) in [(i, j) for i in range(i_n, i_n+3) for j in range(j_n, j_n+3) if (i, j) != (x, y)]:
            tl = list(set(tl) - set(pl[i][j]))
        if len(tl) == 1:
            l[x][y] = tl.pop()
            return True
        return False
    
    
    def find_and_place_possibles(l):
        while True:
            pl = populate_possibles(l)
            if pl != False:
                return pl
    
    
    def populate_possibles(l):
        pl = [[[]for j in i] for i in l]
        for (i, j) in [(i, j) for i in range(9) for j in range(9) if l[i][j] == 0]:
            p = list(set(range(1, 10)) - set(get_horizontal(i, j, l) +
                                             get_vertical(i, j, l) + get_box(i, j, l)))
            if len(p) == 1:
                l[i][j] = p.pop()
                return False
            else:
                pl[i][j] = p
        return pl
    
    
    def find_and_remove_uniques(l, pl):
        for (i, j) in [(i, j) for i in range(9) for j in range(9) if l[i][j] == 0]:
            if update_unique_horizontal(i, j, l, pl) == True:
                return True
            if update_unique_vertical(i, j, l, pl) == True:
                return True
            if update_unique_box(i, j, l, pl) == True:
                return True
        return False
    
    
    def try_with_possibilities(l):
        while True:
            improv = False
            pl = find_and_place_possibles(l)
            if detect_and_simplify_double_pairs(
                    l, pl) == True:
                continue
            if find_and_remove_uniques(
                    l, pl) == True:
                continue
            if improv == False:
                break
        return pl
    
    
    def get_first_conflict(pl):
        for (x, y) in [(x, y) for x, i in enumerate(pl) for y, j in enumerate(i) if len(j) > 0]:
            return (x, y)
    
    
    def get_deep_copy(l):
        new_list = [i[:] for i in l]
        return new_list
    
    
    def run_assumption(l, pl):
        try:
            c = get_first_conflict(pl)
            fl = pl[c[0]
                    ][c[1]]
            # print('Assumption Index : ', c)
            # print('Assumption List: ',  fl)
        except:
            return False
        for i in fl:
            new_list = get_deep_copy(l)
            new_list[c[0]][c[1]] = i
            new_pl = try_with_possibilities(new_list)
            is_done = is_solved(new_list)
            if is_done == True:
                l = new_list
                return new_list
            else:
                new_list = run_assumption(new_list, new_pl)
                if new_list != False and is_solved(new_list) == True:
                    return new_list
        return False
    
    
    if __name__ == "__main__":
        l = [
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 8, 0, 0, 0, 0, 4, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 6, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [2, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 2, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0]
        ]
        # This puzzle copied from Hacked rank test case
        if is_valid(l) == False:
            print("Sorry! Invalid.")
            sys.exit()
        pl = try_with_possibilities(l)
        is_done = is_solved(l)
        if is_done == True:
            for i in l:
                print(i)
            print("Solved!!!")
            sys.exit()
    
        print("Unable to solve by traditional ways")
        print("Starting assumption based solving")
        new_list = run_assumption(l, pl)
        if new_list != False:
            is_done = is_solved(new_list)
            print('is solved ? - ', is_done)
            for i in new_list:
                print(i)
            if is_done == True:
                print("Solved!!! with assumptions.")
            sys.exit()
        print(l)
        print("Sorry! No Solution. Need to fix the valid function :(")
        sys.exit()
    
    0 讨论(0)
  • 2020-12-02 08:37

    Here is my sudoku solver in python. It uses simple backtracking algorithm to solve the puzzle. For simplicity no input validations or fancy output is done. It's the bare minimum code which solves the problem.

    Algorithm

    1. Find all legal values of a given cell
    2. For each legal value, Go recursively and try to solve the grid

    Solution

    It takes 9X9 grid partially filled with numbers. A cell with value 0 indicates that it is not filled.

    Code

    def findNextCellToFill(grid, i, j):
            for x in range(i,9):
                    for y in range(j,9):
                            if grid[x][y] == 0:
                                    return x,y
            for x in range(0,9):
                    for y in range(0,9):
                            if grid[x][y] == 0:
                                    return x,y
            return -1,-1
    
    def isValid(grid, i, j, e):
            rowOk = all([e != grid[i][x] for x in range(9)])
            if rowOk:
                    columnOk = all([e != grid[x][j] for x in range(9)])
                    if columnOk:
                            # finding the top left x,y co-ordinates of the section containing the i,j cell
                            secTopX, secTopY = 3 *(i//3), 3 *(j//3) #floored quotient should be used here. 
                            for x in range(secTopX, secTopX+3):
                                    for y in range(secTopY, secTopY+3):
                                            if grid[x][y] == e:
                                                    return False
                            return True
            return False
    
    def solveSudoku(grid, i=0, j=0):
            i,j = findNextCellToFill(grid, i, j)
            if i == -1:
                    return True
            for e in range(1,10):
                    if isValid(grid,i,j,e):
                            grid[i][j] = e
                            if solveSudoku(grid, i, j):
                                    return True
                            # Undo the current cell for backtracking
                            grid[i][j] = 0
            return False
    

    Testing the code

    
    >>> input = [[5,1,7,6,0,0,0,3,4],[2,8,9,0,0,4,0,0,0],[3,4,6,2,0,5,0,9,0],[6,0,2,0,0,0,0,1,0],[0,3,8,0,0,6,0,4,7],[0,0,0,0,0,0,0,0,0],[0,9,0,0,0,0,0,7,8],[7,0,3,4,0,0,5,6,0],[0,0,0,0,0,0,0,0,0]]
    >>> solveSudoku(input)
    True
    >>> input
    [[5, 1, 7, 6, 9, 8, 2, 3, 4], [2, 8, 9, 1, 3, 4, 7, 5, 6], [3, 4, 6, 2, 7, 5, 8, 9, 1], [6, 7, 2, 8, 4, 9, 3, 1, 5], [1, 3, 8, 5, 2, 6, 9, 4, 7], [9, 5, 4, 7, 1, 3, 6, 8, 2], [4, 9, 5, 3, 6, 2, 1, 7, 8], [7, 2, 3, 4, 8, 1, 5, 6, 9], [8, 6, 1, 9, 5, 7, 4, 2, 3]]
    
    

    The above one is very basic backtracking algorithm which is explained at many places. But the most interesting and natural of the sudoku solving strategies I came across is this one from here

    0 讨论(0)
  • 2020-12-02 08:39

    Not gonna write full code, but I did a sudoku solver a long time ago. I found that it didn't always solve it (the thing people do when they have a newspaper is incomplete!), but now think I know how to do it.

    • Setup: for each square, have a set of flags for each number showing the allowed numbers.
    • Crossing out: just like when people on the train are solving it on paper, you can iteratively cross out known numbers. Any square left with just one number will trigger another crossing out. This will either result in solving the whole puzzle, or it will run out of triggers. This is where I stalled last time.
    • Permutations: there's only 9! = 362880 ways to arrange 9 numbers, easily precomputed on a modern system. All of the rows, columns, and 3x3 squares must be one of these permutations. Once you have a bunch of numbers in there, you can do what you did with the crossing out. For each row/column/3x3, you can cross out 1/9 of the 9! permutations if you have one number, 1/(8*9) if you have 2, and so forth.
    • Cross permutations: Now you have a bunch of rows and columns with sets of potential permutations. But there's another constraint: once you set a row, the columns and 3x3s are vastly reduced in what they might be. You can do a tree search from here to find a solution.
    0 讨论(0)
  • 2020-12-02 08:40

    Hi I've blogged about writing a sudoku solver from scratch in Python and currently writing a whole series about writing a constraint programming solver in Julia (another high level but faster language) You can read the sudoku problem from a file which seems to be easier more handy than a gui or cli way. The general idea it uses is constraint programming. I use the all different / unique constraint but I coded it myself instead of using a constraint programming solver.

    If someone is interested:

    • The old Python version: https://opensourc.es/blog/sudoku
    • The new Julia series: https://opensourc.es/blog/constraint-solver-1
    0 讨论(0)
提交回复
热议问题