Search code examples
pythonalgorithmmethodssolverbacktracking

Sudoko Solver Backtracking Algorithm Not Working


I am fairly new to programming and I have recently started working on a sudoko solver, everything works fine aside from the solving algorithm itself, I used a bit of help from the internet, but because I wrote the methods myself, I can't find an accurate solution to my code, any help would be greatly appreciated!

The problem: The code runs until it cannot enter a value into one of the empty (0) index's and rather than backtracking, it just stops.

If you could let me know what i'm doing wrong, or suggest possible ways to improve the code altogether, anything would be a huge help!

My code is here:

    import time
    start_time = time.time()

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


    def printBoard():
        print("Suduko Board")
        for row in grid:
            for elem in row:
                print(elem, end=' ')
            print()

    def checkInRow(grid,row, num):
        for i in range(0,9):
            if(grid[row][i] == num):
                return True
        return False

    def checkInCol(grid,col, num):
        for i in range(0,9):
            if(grid[i][col] == num):
                return True
        return False

    def checkInBox(grid, row, col, num):
        #This will give the starting point for the box
        boxX = (row // 3) * 3
        boxY = (col // 3) * 3   
        for i in range(3):
            for j in range(3):
                if((grid[boxY + j][boxX + i]) == num):
                    return True
        return False

    def findNextEmpty(grid, num):
        for row in range(9):
            for col in range(9):
                if(grid[row][col] == num):
                    return row, col
        return None


    def checkSafe(grid, row, col, num): #Returns true if safe returns false if unsafe
        return not checkInRow(grid,row,num) and not checkInCol(grid,col,num) and not checkInBox(grid,row,col,num)


    #PROBLEM IS HERE \/ 

    def solveSudoko(grid, i,j):
        if not findNextEmpty(grid,0):
            return True
        else:
            i = findNextEmpty(grid,0)[0] # Finds Row #
            j = findNextEmpty(grid,0)[1] # Finds Col #
            print(i, j)
            for value in range(1,10):
                if checkSafe(grid,i,j,value) == True:
                    grid[i][j] = value

                    printBoard()
                    print("--- %s seconds ---" % (time.time() - start_time))

                    if(solveSudoko(grid,i,j)):
                        return True

                    grid[i][j] = 0

            return False



    #print(str(value) + " can go in grid[" + str(i) + "]" + "[" + str(j) + "]")


    printBoard()

    solveSudoko(grid,0,0)

Solution

  • Problem Explanation

    I believe the problem is that your grid is a list i.e. mutable. That means if you call solveSudoko(grid,i,j) it does not make a copy of the grid but you have just one grid in memory. So after solveSudoko ran into a problem it can't roll back since you have changed the original grid. Possible solutions are working with a non mutable data types like tuples or a bit ugly but also possible is to make a copy of the grid.

    Example:

    grid = [1,2,3]
    def test(grid):
        grid[0] = 0
    test(grid)
    print(grid)
    ---> [0,2,3]
    

    Fast But Ugly Solution

    You probably could get away with adding an import copy at the beginning of your code and replacing

                        if(solveSudoko(grid,i,j)):
    

    by

                        if(solveSudoko(copy.deepcopy(grid),i,j)):
    

    Example:

    grid = [1,2,3]
    def test(grid):
        grid[0] = 0
    test(copy.deepcopy(grid))
    print(grid)
    ---> [1,2,3]
    

    Notice that using copy.copy instead of copy.deepcopy would also work in the example since integers are immutable but with a list of lists as in your code you need copy.deepcopy.

    Good Solution

    Example:

    grid = (1,2,3)
    def test(grid):
        grid = (0,2,3)
    test(grid)
    print(grid)
    ---> (1,2,3)
    

    The problem with tuples is getting a new grid is not fun. I would probably use a little class managing the grid for me:

    class Grid:
        def __init__(self, grid):
            assert type(grid) == tuple
            self.grid = grid
    
        @staticmethod
        def gird_from_list(list_):
            return Grid(tuple(list_[i][j] for i in range(len(list_)) for j in range(len(list_[0]))))
    
        def __repr__(self):
            return "\n".join(str(self.grid[i*9:(i+1)*9]) for i in range(9))
    
        def set(self, i, j, value):
            return Grid(tuple(value if i+j*9==k else x for k,x in enumerate(self.grid)))
    
    tuple_grid = Grid.gird_from_list(gird)
    tuple_grid2 = tuple_grid.set(8,8,0)
    

    Here grid_from_list is just to convert your list into my format and __repr__ is giving you a nice representation as a 2d grid even though I am acutally only keeping a 1d tuple. In case you didn't know __repr__ is always called when you print an instance of the class.

    Now printing tuple_grid would yield

    (7, 8, 0, 4, 0, 0, 1, 2, 0)
    (6, 0, 0, 0, 7, 5, 0, 0, 9)
    (0, 0, 0, 6, 0, 1, 0, 7, 8)
    (0, 0, 7, 0, 4, 0, 2, 6, 0)
    (0, 0, 1, 0, 5, 0, 9, 3, 0)
    (9, 0, 4, 0, 6, 0, 0, 0, 5)
    (0, 7, 0, 3, 0, 0, 0, 1, 2)
    (1, 2, 0, 0, 0, 7, 4, 0, 0)
    (0, 4, 9, 2, 0, 6, 0, 0, 7)
    

    And printing tuple_grid2 would yield

    (7, 8, 0, 4, 0, 0, 1, 2, 0)
    (6, 0, 0, 0, 7, 5, 0, 0, 9)
    (0, 0, 0, 6, 0, 1, 0, 7, 8)
    (0, 0, 7, 0, 4, 0, 2, 6, 0)
    (0, 0, 1, 0, 5, 0, 9, 3, 0)
    (9, 0, 4, 0, 6, 0, 0, 0, 5)
    (0, 7, 0, 3, 0, 0, 0, 1, 2)
    (1, 2, 0, 0, 0, 7, 4, 0, 0)
    (0, 4, 9, 2, 0, 6, 0, 0, 0)
    

    So the gird does not change when you call set but gives you an entirely new gird with the new value.