1

I'm so confused by backtracking because when the recursive call returns, won't you replace the solution found by replacing the grid back to zero. So even if you find the solution would it not be erased because after calling the solve function you are canceling what you did by replacing the value back to zero. I get the fact that you are backtracking but on the final recursive call that contains all the correct values are you not just replacing everything to 0?

# grid = .....    # defined as a global value,
                  # a list of 9 lists, 9-long each
def solve():
    global grid
    for y in range (0, 9):
        for x in range (0, 9):
            if grid[y][x] == 0:
                for n in range(1,10):
                    if possible(y, x, n):
                        grid[y][x] = n
                        solve()
                        grid[y][x] = 0
                return

    # edit: missed this final line:
    print (np.matrix(grid))

This was the code on Computerphile video by Prof. Thorsten Altenkirch.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
sam202252012
  • 165
  • 5
  • 16
  • related: [the worst-case valid sudoku puzzle for simple backtracking brute force algorithm?](https://stackoverflow.com/q/24682039/849891), and the links therein, e.g. https://en.wikipedia.org/wiki/Sudoku_solving_algorithms#Backtracking has a dynamic GIF picture showing the same thing as this algorithm in this question. – Will Ness Dec 25 '20 at 15:34
  • Don't use `global` here (or in general). It's unnecessary and makes the code much more brittle and hard to reason about, with no benefit. Functions should be black boxes that operate only on internal state, not reach outside of themselves to mess with external state. Use _parameters_ and _return values_ to interface with the outside world from a function. – ggorlen Aug 14 '21 at 19:54

3 Answers3

1

This is weird code, but should work with some adaptations:

def solve():
    global grid
    for y in range(0, 9):
        for x in range(0, 9):
            if grid[y][x] == 0:
                for n in range(1,10):
                    if possible(y, x, n):
                        grid[y][x] = n
                        if solve():
                            return True  # return without reset
                        grid[y][x] = 0
                return False  # exhausted all options
            return True  # this is the deepest and last call with no more zeroes
user2390182
  • 72,016
  • 6
  • 67
  • 89
  • yes i agree it is quite unusual but his method works I ran it but I have no idea how it works, because when the method runs wouldn't it just run grid [y][x] = 0 at the end or the last recursive call – sam202252012 Dec 17 '20 at 15:32
  • Is this the code you used: https://gist.github.com/ekapope/b2d69354a0addd8a9470e70fbf6b343f ? – user2390182 Dec 17 '20 at 15:41
  • yes, it is the one from the computerphile video of soduku solver. I'm guessing there is multiple instance of it in github. Yes i used the same one, just confirmed the code – sam202252012 Dec 17 '20 at 15:43
  • Because you did not show the `print` and `input` lines at the bottom of the function that are only ever reached at the deepest recursive call. They output the correct solution and afterwards, the grid will be reset. – user2390182 Dec 17 '20 at 15:43
  • Oh my gosh, Thank you soooo much. I can't believe i missed this. I feel like an idiot lol xD – sam202252012 Dec 17 '20 at 15:45
  • 1
    Ha, this "Professor" would not get his anti-pattern infested shit past our company's code review :D – user2390182 Dec 17 '20 at 15:46
  • So to be clear, It won't go into the if statement which checks if it has zero in it. Therefore at the deepest call it just prints the whole grid and the gets rest right? – sam202252012 Dec 17 '20 at 15:47
  • 1
    Yes, exactly. As long as there are `0`, the function returns beforehand. Only for the full grid, the print happens. Still, if you inspect the grid after having provided some input, it will have all its zeroes back.# – user2390182 Dec 17 '20 at 15:49
  • Alright perfect, thank you so much, God Bless! – sam202252012 Dec 18 '20 at 01:34
  • you miss the point of the code entirely. it is not to print the first solution, but to print _all_ solutions. which your re-write destroys, and only does the former, IIANM (I'm not a Pythonista). in the original code, you can replace the `print` with `yield True` for instance, and so will have the ability to iterate through all the solutions, on each yield having the access to the global -- *changing* -- solution in `grid`. and no, it is not "weird", it is beautiful. you just haven't seen this thing before. Prolog is full of mystique, Haskellers will drown you in "monad" talks, but all it took – Will Ness Dec 18 '20 at 16:02
  • here, was a bunch of [*nested loops*](https://stackoverflow.com/search?q=user%3A849891+nested+loops), recursively built! Simplicity in its finest, beautiful and deep. and BTW there's a whole tag dedicated to it on SO, which I've edited in into the question. check it out. :) – Will Ness Dec 18 '20 at 16:03
  • @WillNess Ok, I understand. And yet, accumulating side-effects like console output is not the most intuitive use for such a pattern. And yetter still, a sudoku with more than one solution might be an interesting thing in CS, but should be torn to shreds in the real world :P – user2390182 Dec 18 '20 at 16:14
  • but the point of the exercise is surely not to solve a Sudoku puzzle -- for which this code is a very inefficient and unsophisticated solver in the extreme -- but to demonstrate [tag:recursive-backtracking] in action. BTW you know that famous Pythonista, Norvig, was first a famous common-lisper, and in his famous book PAIP he shows a CL implementation of (kind-of) Prolog which does essentially this same inside-out trick, producing solutions at the deepest level of nested looping (implemented there as flat-mapping). and, that `yield True` which I mentioned takes care of that console problem. – Will Ness Dec 18 '20 at 16:17
  • No, I totally agree on all the points you make. Note that my answer was written at a point when the OP had not yet shown the `print` at the bottom of the function (letting one assume its purpose was to mutate the grid into a solved state) which left the function truly pointless as it would have left the grid ultimately unchanged no matter how many solutions it had seen come and go along the way. – user2390182 Dec 18 '20 at 16:23
  • yeah, I get that. I too was lost at first looking at it. :) btw the pattern is beautiful but the code is truly bad: each solve() enumerates all the xs and ys _from the start_ anew!!!!&%#&^^## it should be split into a main and a worker solve(x,y), and pass the current (x,y) so that the recursively invoked instance will continue right from that point. (or something like that...) the prof's explanation is also [*off.*](https://stackoverflow.com/questions/65343403/how-does-recursive-backtracking-work-computerphile-sodoku-solver#comment115551646_65343403) – Will Ness Dec 18 '20 at 16:27
  • Yeah, algorithmically it is really evil :D I do watch a lot of numberphile and computerphile and I was quite surprised seeing this, as it would not take much to improve upon this without losing readability. – user2390182 Dec 18 '20 at 16:35
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226142/discussion-between-schwobaseggl-and-will-ness). – user2390182 Dec 18 '20 at 16:56
1

Here is part of my code:

vlist = PossibleValueAtPosition(row,col) # find possible value at location (row, col)

for v in vlist:             # try each possible value

    puzzle[row][col] = v

if SolvePuzzle(n+1)==True:  # n=81 means all filled then end loop

    return True             # if get a solution, you return True

    puzzle[row][col] = 0        # if above return true, this line will never run

    return False                # return False for each fail attemp

Main program should like this

if SolvePuzzle(0)==True:

    print(puzzle)

else:

    print('No solution!')
Stidgeon
  • 2,673
  • 8
  • 20
  • 28
0

It's not the final recursive call that contains all the correct values, but (each of) the deepest. Yes, this code enumerates all the solutions to the puzzle with the given board grid, not just the first solution.

For each (y,x) place, if it's empty, we try to place there each of the numbers from 1 through 9 in turn. If the placement was possible on the board as it is so far, we recurse with the changed grid board.

At the deepest level of recursion there were no empty (y,x) places on the board. Therefore we slide through to the print statement. (It could also be replaced by yield True for example, to turn it into a generator. On each next value we'd get from that generator, we'd have a complete solution -- in the changed grid. And when the generator would get exhausted, the grid would be again in its original state.)

When all the numbers from 1 through 9 have been tried, the current invocation has run its course. But the one above it in the recursion chain is waiting to continue its work trying to fill its (y,x) position. We must let it work on the same board it had before it invoked this invocation of solve(). And the only change on the board this invocation did was to change its (y,x) position's value from 0 to 1 through 9. So we must change it back to 0.

This means that the code could be restructured a little bit too, as

def solve():
    global grid
    for y in range (0, 9):
        for x in range (0, 9):     # for the first
            if grid[y][x] == 0:    # empty slot found:
                for n in range(1,10):  # try 1..9
                    if possible(y, x, n):
                        grid[y][x] = n
                        solve()        # and recurse
                # (move it here)
                grid[y][x] = 0     # restore
                return             # and return
    # no empty slots were found:
    #   we're at the deepest level of recursion and
    #   there are no more slots to fill:
    yield True     # was: print (np.matrix(grid))

Each invocation works only on one (y,x) location, the first empty position that it found by searching anew from the start on the changed board. This search is done by the first two nested loops on y and on x. That is a bit redundant; we know all the positions before this (y,x) are already filled. The code would be better restructured to pass the starting position (y,x) as a parameter to solve.

The paradigm of recursive backtracking is beautiful. Prolog is full of mystique, Haskell will dazzle you with cryptic monads talk (monads are actually just interpretable nestable data), but all it takes here are some nested loops, recursively created!

The paradigm is beautiful, but this code, not so much. A code's visual structure should reflect its true computational structure, but this code gives you an impression that the y-x- loops are the nested loops at work to create the backtracking structure, and they are not (they just implement a one-off linear search for the next empty space in the top-down left-to-right order).

That role is fulfilled by the n in range(1,10) loops. The y-x- loops should be stopped and exited explicitly when the empty space is found, to truly reflect in the code structure what is going on computationally, to make it apparent that the n in range(1,10) loop is not nested inside the y-x- loops, but comes in play after they finish their job.

Another problem is that it just assumes the validity of the numbers given to us in the grid before the very first call to solve(). That validity is never actually checked, only the validity of the numbers which we are placing in the empty cells is checked.

(note: previous versions of this answer were based on an erroneous reading of the code. there were some valid parts in them too. you can find them on the revisions list here).

Will Ness
  • 70,110
  • 9
  • 98
  • 181