Python sudoku backtracking

Question:

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)

e.g. 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")
Asked By: styrix358

||

Answers:

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]
Answered By: Khoa Nguyen

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).

Answered By: styrix358

1- Create rows, cols, and boxes (3×3 units) array of dictionaries to store which indices of each row, col, and boxes have numbers.

2- Take a screenshot of the board. Run a for-loop and mark which points include numbers.

3- Call the recursive backtrack function.

4- Always in recursive functions define the base case to exit out of the recursion.

5- Start a for-loop to see which coordinate is ".". If you see ".", apply steps:[6,7,8,9]

6- Now we need to insert a valid number here. A valid number is a number that does not exist in the current row, col, and box.

7- If you find a valid number, insert it into the board and update rows, cols, and boxes.

8- After we inserted the valid point, we call backtrack function for the next ".".

9- When calling the backtrack, decide at which point you are. If you are in the last column, your next backtrack function will start from the next row and column 0. But if you are in the middle of row, you just increase the column parameter for next backtrack function.

10- If in step 5 your value is not ".", that means there is already a valid number here so call next backtracking depending on where position is. If you are in the last column, your next backtrack function will start from the next row and column 0. But if you are in the middle of row, you just increase the column parameter for next backtrack function.

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
Answered By: Yilmaz
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.