Search code examples
pythonlistbacktracking

How would I implement backtracking here?


I've been struggling to find the right logic for my sudoku solver. So far I've created a function to take (x,y) coordinates and the number and check if it is a legal move.

Though I don't understand how I can iterate through the grid replacing every 0 with a legal number, then go back and fix numbers that make the solve incorrect.

For example sometimes I will be left with 0's on the grid because some numbers can no longer be played. I took a deeper look at it and realized that numbers played previously in the same row were legal but ruined the puzzle. Instead, if those numbers were a bit higher the puzzle would be solved.

I understand backtracking would be my friend here but, I have no clue on how to implement it. So far here is what I have.

solve.py

import numpy as np
import time
class sodoku:
    def __init__(self,grid,boxRange):
        self.grid = grid
        self.boxRange = boxRange
    def show(self):
        for row in self.grid:
            print(row)
    def solve(self):
        def possible(num,x,y):
            def box(x,y):                       
                board = np.array(self.grid)
                result = {}
                size = 3
                for i in range(len(board) // size):
                    for j in range(len(board) // size):
                        values = board[j * size:(j + 1) * size, i * size:(i + 1) * size]
                        result[i * size + j + 1] = values.flatten()
                if y <= 2 and x <= 2:
                    squareBox = result[1]
                if (y <= 5 and y > 2) and x <= 2:
                    squareBox = result[2]
                if (y <= 8 and y > 5) and x <= 2:
                    squareBox = result[3]

                if (y <= 2 ) and (x <= 5 and x > 2):
                    squareBox = result[4]
                if (y <= 5 and y > 2)and (x <= 5 and x > 2):
                    squareBox = result[5]
                if (y <= 8 and y > 5)and (x <= 5 and x > 2):
                    squareBox = result[6]
            
                if (y <= 2) and (x <= 8 and x > 5):
                    squareBox = result[7]
                if (y <= 5 and y > 2)and (x <= 8 and x > 5):
                    squareBox = result[8]
                if (y <= 8 and y > 5)and (x <= 8 and x > 5):
                    squareBox = result[9]
                return squareBox
            row = self.grid[y]
            column= [r[x] for r in self.grid]
            square = box(x,y)
            if (num not in row) and (num not in column) and (num not in square):
                return True
            else:
                return False
        y = 0
        for row in self.grid:
            x = 0
            for number in row:
                if number == 0:
                    for i in range(1,10):
                        if possible(i,x,y):
                            row[x] = i
                        elif i == 9 and possible(i,x,y) == False: pass
                        #what would I do here now

                x += 1
            y += 1
      
boxRange = "3x3"           
bxd = []
with open('board.txt', 'r') as f:
    for line in f:
        line = line.strip()
        line = line.split(' ')
        bLine = [int(x) for x in line]
        bxd.append(bLine)
# brd = [[3,0,0,2],[0,4,1,0],[0,3,2,0],[4,0,0,1]]
brd = sodoku(bxd,boxRange)
brd.show()
brd.solve()
print('-----Solved------')
brd.show()

board.txt

5 3 0 0 7 0 1 0 0
6 0 0 1 9 5 0 0 0
0 9 8 0 0 0 0 6 0
8 0 0 0 6 0 0 0 3
4 0 0 8 0 3 0 0 1
7 0 0 0 2 0 0 0 6
0 6 0 0 0 0 2 8 0
0 0 0 4 1 9 0 0 5
0 0 0 0 8 0 0 7 9

Solution

  • Recursive pseudocode backtracing sudoku solver:

    #solve will return a solved board, or None if it fails
    def solve(board):
        #case 1: board is solved
        if board.is_solved: #simple check for leftover empty spaces
            return board #board is solved. unzip the call stack
    
        pos = board.next_empty_space()
        valid = [i for i in range(1,10) if board.is_valid(pos, i)]
    
        #case 2: not solved and no more valid moves
        if not valid:
            return None #no valid moves left
    
        new_board = copy(board) #don't step on the original data in case this isn't the solution
        for value in valid:
            new_board[pos] = value
            result = solve(new_board)
    
            #case 3: one of the valid moves led to a valid solution
            if result is not None: #we found a fully solved board
                return result #here's where we unzip the call stack
    
        #case 4: none of the valid moves led to a valid solution
        return None #none of the valid moves panned out
    

    Basically you consider each empty space on the board as a branch in a tree, and sub-branches from each branch you insert a new number which is currently valid at that point in the tree. If you get to the end of a branch, and there are no more valid moves left (sub-branches) you have either successfully filled in all the blank spaces or one of the numbers is wrong. When None gets returned, and execution goes back to the caller (up a frame in the call stack), the local position in the for loop going over valid moves is what "remembers" where you're at, and what the next possible valid move should be. It's basically a depth-first tree search algorithm for a correct board state.