简体   繁体   中英

Python sudoku backtracking

I was trying out the backtracking algorithm with an easy example (sudoku). I first tried another approach where more possibilities are canceled, but after I got the same error I switched to an easier solution.

  1. look for the first unsolved spot
  2. fill in every number between 1 and 9 and backtrack the new field if it is valid

When I run it and output the non-valid fields I can see that when the algorithm goes out of a recursion call the spot that was in that recursion call is still a 9 (so the algorithm couldn't find anything for that spot)

eg the first two lines look something like this (it's trying to solve an empty field):

[1, 2, 3, 4, 6, 9, 9, 9, 9]

[9, 9, 9, 9, 9, 9, 9, 0, 0]

I thought it was a reference error and inserted [e for e in field] in the backtracking call so that the old field doesn't get altered although that didn't seem to help.

Here is my code:


    for i in range(9):
        a = [field[i][j] for j in range(9) if field[i][j] != 0]
        if len(a) != len(set(a)):
            return False

    for i in range(9):
        a = [field[j][i] for j in range(9) if field[j][i] != 0]
        if len(a) != len(set(a)):
            return False

    for x in range(3):
        for y in range(3):
            a = []
            for addX in range(3):
                for addY in range(3):
                    spot = field[x * 3 + addX][y * 3 + addY]
                    if spot != 0:
                        a.append(spot)
            if len(a) != len(set(a)):
                return False

    return True

def findEmpty(field):

    for i in range(9):
        for j in range(9):
            if field[i][j] == 0:
                return i, j


def backtracking(field):

    find = findEmpty(field)
    if not find:
        return True, field
    else:
        x, y = find
 
    for i in range(1, 10):
        print(f"Trying {i} at {x} {y}")
        field[x][y] = i
        if isValid(field):
            s = backtracking([e for e in field])
            if s[0]:
                return s
        else:
            print("Not valid")
            for row in field:
                print(row)

    return False, None


field = [[0, 0, 0, 0, 1, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 0, 0]]

solution = backtracking(field)
if solution[0]:
    print("There was a solution. The field is:")
    for row in solution[1]:
        print(row)
else:
    print("No solution was found")

Okay, so based on what I can see in the logs, what happens is that when the code gets to 9 and still does not get an answer, it will backtrack, but keeps the value at 9.

So what happens is, every single time the program backtracks, it leaves the value at 9 and then go to the previous value, which might also go to 9, which is invalid as the value we backtracked from is already a 9. This causes a cycle where the program would backtrack straight to the start and make most slots 9, as you can see in your example.

So the solution would be to add a few lines to backtrack() as below. In short, that extra 2 lines checks if the invalid answer is a 9, if it is, it is resetted to a 0 and backtracks to the previous value until it gets a valid answer.

def backtracking(field):
    find = findEmpty(field)
    if not find:
        return True, field
    else:
        x, y = find

    for i in range(1, 10):
        print(f"Trying {i} at {x} {y}")
        field[x][y] = i
        if isValid(field):
            s = backtracking(field)
            if s[0]:
                return s
        else:
            print("Not valid")
            if field[x][y] == 9:
                field[x][y] = 0
            for row in field:
                print(row)

    return False, None

Solution it gave:

[2, 3, 4, 5, 1, 6, 7, 8, 9]
[1, 5, 6, 7, 8, 9, 2, 3, 4]
[7, 8, 9, 2, 3, 4, 1, 5, 6]
[3, 1, 2, 4, 5, 7, 6, 9, 8]
[4, 6, 5, 1, 9, 8, 3, 2, 7]
[8, 9, 7, 3, 6, 2, 4, 1, 5]
[5, 2, 8, 6, 4, 1, 9, 7, 3]
[6, 7, 3, 9, 2, 5, 8, 4, 1]
[9, 4, 1, 8, 7, 3, 5, 6, 2]

I did some research and apparently it really is a reference error. For me importing pythons copy library and assigning each new field saying f = copy.deepcopy(field) fixes the issue (this also works for the complex example).

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        n=len(board)
        # create state variables,keep track of rows, cols and boxes
        rows=[{} for _ in range(n)]
        cols=[{} for _ in range(n)]
        boxes=[{} for _ in range(n)]
       # get the initial state of the grid
        for r in range(n):
            for c in range(n):
                if board[r][c]!='.':
                    val=board[r][c]
                    box_id=self.get_box_id(r,c)
                    boxes[box_id][val]=True
                    rows[r][val]=True
                    cols[c][val]=True
        # this backtracking just tells if shoul move to the next cell or not
        self.backtrack(board,boxes,rows,cols,0,0)
        
    def backtrack(self,board,boxes,rows,cols,r,c):
        # base case. If I hit the last row or col, means all digits were correct so far
        if r>=len(board) or c>=len(board[0]):
            return True
        # situation when cell is empty, fill it with correct value
        if board[r][c]==".":
            for num in range(1,10):
                box_id=self.get_box_id(r,c)
                box=boxes[box_id]
                row=rows[r]
                col=cols[c]
                str_num=str(num)
                # check rows, cols and boxes make sure str_num is not used before
                if self.is_valid(box,col,row,str_num):
                    board[r][c]=str_num
                    boxes[box_id][str_num]=True
                    cols[c][str_num]=True
                    rows[r][str_num]=True
                    # if I am the last col and I placed the correct val, move to next row. So first col of the next row
                    if c==len(board)-1:
                        if self.backtrack(board,boxes,rows,cols,r+1,0):
                            return True
                    # if I am in col between 0-8, move to the col+1, in the same row
                    else:
                        if self.backtrack(board,boxes,rows,cols,r,c+1):
                            return True
                    # If I got a wrong value, then backtrack. So clear the state that you mutated
                    del box[str_num]
                    del row[str_num]
                    del col[str_num]
                    board[r][c]="."
       # if cell is not empty just call the next backtracking
        else:
            if c==len(board)-1:
                if self.backtrack(board,boxes,rows,cols,r+1,0):
                    return True
            else:
                if self.backtrack(board,boxes,rows,cols,r,c+1):
                    return True
        return False
                
    def is_valid(self,box,row,col,num):
        if num in box or num in row or num in col:
            return False
        else:
            return True
        
        
    # a helper to get the id of the 3x3 sub grid, given row and column
    def get_box_id(self,r,c):
        row=(r//3)*3
        col=c//3
        return row+col

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM