4

I'm relatively new to Python, and I've been working through the problems on checkio.com, some of which I'm finding very interesting.

The current problem I'm working on is a sudoku solver (if anyone needs me to write out the rules of sudoku, I'd be glad to oblige).

I've decided to try solving the problem using backtracking, and a recursive function. The function takes an initial sudoku grid input in the form of a 2d array, with zero denoting an empty cell.

Here the core part of my code:

def checkio(grid,depth=0):
    # Return the solution of the sudoku
    # or False if the current grid has no solution
    # uses backtracking
    # depth is for debugging

    for p in range(9):
        for q in range(9):
            if grid[p][q]==0:
                candidates=cand(p,q,grid)

                for x in candidates:
                    #try each candidate in turn
                    grid[p][q]=x

                    #print out information for debugging
                    print("working on cell (%s,%s) --> depth %s, candidate is %s" % (p,q,depth,x))
                    print_sudoku(grid)

                    h=checkio(grid,depth+1)
                    #h is False unless we have found a full grid
                    if h:      
                        return h

                #return false if no candidate works
                return False

    #if the grid is already full, just return it
    #if the initial input was valid, this algorithm shouldn't produce an invalid grid

    return grid                

Here are the subroutines I'm using, which appear to be working properly:

def print_sudoku(grid):
    # prints out the grid in a way that's easier to parse visually
    for x in grid:
        print(x)


def cand(i,j,grid):
    # returns a list of candidate numbers for the square (i,j)
    rowdata=[]
    for n in range(9):
        if grid[i][n]!=0:
            rowdata.append(grid[i][n])

    coldata=[]
    for n in range(9):
        if grid[n][j]!=0:
            coldata.append(grid[n][j])

    boxdata=[]
    for p in range(9):
        for q in range(9):
            if samebox(p,q,i,j) and grid[p][q]!=0:
                boxdata.append(grid[p][q])

    fulllist=range(1,10)
    cand=list(set(fulllist) - set(rowdata + coldata + boxdata))

    return cand

def samebox(ax,ay,bx,by):
    #returns true if (ax,ay) and (bx,by) are in the same box   
    return ax // 3 == bx // 3 and ay // 3 == by // 3

Here is an example function call with an input grid that is known to be (uniquely) solvable:

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

Now, I expected this code to work. But if you run the test example, it fails. By looking at the intermediate grids, we can see a problem: once the algorithm has tried a given track and got stuck (i.e. arrived at an empty square with no candidates), the grid that subsequent instances of the function (operating at a lower depth) is working with is filled with the (potentially incorrect) values left by previous instances of the function (operating at a higher depth).

I don't understand why this is happening. I thought that on each function call, a new local scope is created, so that each instance of the function works with its own grid without affecting the grids of the other instances.

Have I misunderstood something about variable scope, or am I making some other mistake? Thank you for your time.

DCopperfield
  • 143
  • 1
  • 4

2 Answers2

10

You are correct in thinking that the grid variable is local to the scope of each recursive call, but a variable is just a reference to an object.

The problem is that the grid variables in each scope all reference the same object, the list you pass in at the start.

When you modify the object referenced by grid in one scope, you are modifying it in all scopes, because the grid variable in every scope references the same object.

What you want to do is modify a copy of the grid that is passed in, keeping the original grid intact.

You can copy a deeply-nested data structure like you've got with the deepcopy() function:

from copy import deepcopy

def checkio(grid,depth=0):
    grid = deepcopy(grid)
    ...

That way when you find an invalid solution, and start backtracking at each step in the backtrack the grid is preserved in the state it was in before you went down that particular "blind alley".

Jamie Cockburn
  • 7,379
  • 1
  • 24
  • 37
4

You are passing in a mutable object. Local names are not shared, but they reference one and the same object until you assign a different object to them.

While referencing that one mutable object, changes to the object itself are then visible through all references to it (e.g. all local names in your call stack).

Create a copy before changing the grid:

grid = [row[:] for row in grid]

This binds a new list object to the local name grid, one the other locals in the stack are not bound to. Because the outer list contains more mutable lists inside, those need to be copied too (by using the whole-slice syntax).

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Adding that one line of code solved the problem! Thank you for describing so succinctly my knowledge gap - I will make sure to read about mutable objects. – DCopperfield Jul 04 '14 at 10:35