In this article we cover another interesting coding exercise: solving a Sudoku. The algorithm we use is backtracking, which is a type of brute force search. The idea is simple but works well: we select an empty cell and we insert a number and check whether this is allowed or not. If not, we stop and try with another number till when all combinations from 1 to 9 are exhausted. If it is allowed, we go look for the next empty cell and proceed. We have found a solution when we there are no more empty cells; if no solution is found the Sudoku has no solution.

The advantage of this method is that it works for all levels and it is quite quick despite having exponential complexity. In the worst case, we would test $9^{81}$ cases, but in reality most combinations would stop the recursion much earlier. In fact, all the tests below are solved almost instantaneously. Note that we only solve for one solution and stop there – there could be other solutions as well but we won’t find them.

Let’s start with the data structure: we only have one, the board. This is a list of nine lists, each of size 9. The function check_sizes() ensures we have a valid board.

def check_sizes(board):
    if len(board) != 9:
        return False
    for i in range(9):
        if len(board[i]) != 9:
            return False
    for i in range(9):
        for j in range(9):
            if board[i][j] not in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:
                return False
    return True

We need two utility functions: find_empty() returns the first empty cell or None if there are none, while is_valid() returns True if the given number can be inserted in the specified position, of False if that action would result in a non-valid Sudoku.

def find_empty(board):
    for i in range(9):
        for j in range(9):
            if board[i][j] == 0:
                return (i, j)
    return None
def is_valid(board, num, pos):
    "Checks that it is valid to set num in pos"
    # valid rows
    for i in range(9):
        if board[pos[0]][i] == num and pos[1] != i:
            return False
    # valid columns
    for i in range(9):
        if board[i][pos[1]] == num and pos[0] != i:
            return False
    # valid boxes
    box_x = pos[1] // 3
    box_y = pos[0] // 3
    for i in range(box_y * 3, box_y * 3 + 3):
        for j in range(box_x * 3, box_x * 3 + 3):
            if board[i][j] == num and (i, j) != pos:
                return False
    return True

The counter decorator will be used to track the complexity of the algorithm; as we’ll see the number of calls do not always depend on the complexity for humans.

def counter(f):
    def inner(*args, **kwargs):
        inner.num_calls += 1
        return f(*args, **kwargs)
    inner.num_calls = 0
    return inner

We are ready to write the actual solver, which takes board and performs the brute search by inserting all numbers from 1 to 9 in the first empty cell and then calls itself. The return value is either True when there are no empty cell (assuming we start from a valid board) or False if no number can be inserted in the first empty cell without breaking the Sudoku. The function itself is quite short; it is perhaps a bit fragile, so it would be better to wrap it in another function that checks the input board first and then starts the recursion.

@counter
def solve_sudoku(board):
    empty = find_empty(board)
    if not empty:
        return True
    row, col = empty
    for num in range(1, 10):
        if is_valid(board, num, (row, col)):
            board[row][col] = num
            if solve_sudoku(board):
                return board
            board[row][col] = 0
    return False

Now some tests. Function print_board() is a utility function to pretty-print the boards used for testing, which come from the New York Times website for the easy, medium and hard levels; another website was used for the very hard and extreme levels.

def print_board(board):
    "Pretty-prints the board"
    for i in range(9):
        for j in range(9):
            number = board[i][j]
            if number == 0:
                number = ' '
            print(f' {number} ', end='')
            if j in [2, 5]:
                print(' | ', end='')
        print()
        if i in [2, 5]:
            print('-' * 32)
easy = [
    [5, 0, 0, 0, 8, 0, 1, 0, 6],
    [0, 0, 0, 0, 7, 3, 2, 8, 4],
    [4, 0, 7, 1, 2, 0, 0, 0, 3],
    [7, 0, 9, 2, 6, 0, 0, 0, 0],
    [0, 0, 3, 0, 4, 5, 0, 1, 0],
    [0, 0, 1, 0, 0, 0, 6, 2, 5],
    [0, 9, 0, 7, 0, 4, 0, 3, 0],
    [0, 7, 0, 0, 0, 2, 8, 9, 0],
    [1, 2, 5, 9, 0, 0, 0, 0, 0],
]
assert check_sizes(easy)

solve_sudoku.num_calls = 0
if solve_sudoku(easy):
    print_board(easy)
else:
    print("No solution exists.")
print(f'\n# of calls: {solve_sudoku.num_calls}')
 5  3  2  |  4  8  9  |  1  7  6 
 9  1  6  |  5  7  3  |  2  8  4 
 4  8  7  |  1  2  6  |  9  5  3 
--------------------------------
 7  5  9  |  2  6  1  |  3  4  8 
 2  6  3  |  8  4  5  |  7  1  9 
 8  4  1  |  3  9  7  |  6  2  5 
--------------------------------
 6  9  8  |  7  1  4  |  5  3  2 
 3  7  4  |  6  5  2  |  8  9  1 
 1  2  5  |  9  3  8  |  4  6  7 

# of calls: 52
medium = [
    [0, 0, 0, 6, 7, 0, 0, 2, 0],
    [7, 0, 1, 0, 0, 0, 9, 0, 0],
    [8, 0, 0, 0, 5, 3, 0, 0, 0],
    [0, 1, 0, 0, 2, 0, 0, 6, 0],
    [0, 0, 9, 0, 0, 6, 0, 0, 0],
    [3, 0, 0, 0, 0, 0, 7, 8, 0],
    [0, 0, 5, 0, 0, 0, 3, 0, 7],
    [2, 0, 0, 3, 0, 0, 5, 0, 0],
    [0, 0, 0, 0, 0, 5, 0, 4, 0],
]
assert check_sizes(medium)

solve_sudoku.num_calls = 0
if solve_sudoku(medium):
    print_board(medium)
else:
    print("No solution exists.")
print(f'\n# of calls: {solve_sudoku.num_calls}')
 9  5  3  |  6  7  1  |  8  2  4 
 7  6  1  |  2  4  8  |  9  3  5 
 8  4  2  |  9  5  3  |  6  7  1 
--------------------------------
 5  1  8  |  7  2  9  |  4  6  3 
 4  7  9  |  8  3  6  |  1  5  2 
 3  2  6  |  5  1  4  |  7  8  9 
--------------------------------
 1  8  5  |  4  6  2  |  3  9  7 
 2  9  4  |  3  8  7  |  5  1  6 
 6  3  7  |  1  9  5  |  2  4  8 

# of calls: 8797
hard = [
    [0, 0, 7, 0, 0, 0, 4, 3, 0],
    [3, 0, 0, 8, 0, 0, 0, 5, 6],
    [5, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 7, 0, 0, 0, 0, 8, 0, 0],
    [0, 0, 0, 6, 0, 2, 0, 0, 0],
    [0, 2, 0, 3, 0, 9, 0, 6, 0],
    [0, 0, 0, 0, 6, 0, 9, 0, 4],
    [2, 0, 0, 4, 0, 1, 0, 0, 0],
    [0, 5, 0, 0, 0, 0, 3, 0, 0],
]
assert check_sizes(hard)

solve_sudoku.num_calls = 0
if solve_sudoku(hard):
    print_board(hard)
else:
    print("No solution exists.")
print(f'\n# of calls: {solve_sudoku.num_calls}')
 8  1  7  |  5  2  6  |  4  3  9 
 3  4  9  |  8  1  7  |  2  5  6 
 5  6  2  |  9  4  3  |  7  1  8 
--------------------------------
 6  7  3  |  1  5  4  |  8  9  2 
 9  8  5  |  6  7  2  |  1  4  3 
 1  2  4  |  3  8  9  |  5  6  7 
--------------------------------
 7  3  1  |  2  6  5  |  9  8  4 
 2  9  8  |  4  3  1  |  6  7  5 
 4  5  6  |  7  9  8  |  3  2  1 

# of calls: 72686
very_hard = [
    [1, 0, 0, 0, 3, 9, 0, 0, 8],
    [0, 0, 5, 0, 7, 0, 0, 0, 0],
    [0, 0, 0, 8, 0, 6, 0, 0, 5],
    [9, 0, 6, 0, 0, 3, 0, 0, 0],
    [0, 5, 1, 4, 0, 7, 2, 3, 0],
    [0, 0, 0, 6, 0, 0, 9, 0, 1],
    [7, 0, 0, 9, 0, 5, 0, 0, 0],
    [0, 0, 0, 0, 2, 0, 4, 0, 0],
    [5, 0, 0, 3, 4, 0, 0, 0, 7],
]
assert check_sizes(very_hard)

solve_sudoku.num_calls = 0
if solve_sudoku(very_hard):
    print_board(very_hard)
else:
    print("No solution exists.")
print(f'\n# of calls: {solve_sudoku.num_calls}')
 1  6  4  |  5  3  9  |  7  2  8 
 3  8  5  |  2  7  4  |  1  6  9 
 2  7  9  |  8  1  6  |  3  4  5 
--------------------------------
 9  2  6  |  1  8  3  |  5  7  4 
 8  5  1  |  4  9  7  |  2  3  6 
 4  3  7  |  6  5  2  |  9  8  1 
--------------------------------
 7  4  3  |  9  6  5  |  8  1  2 
 6  9  8  |  7  2  1  |  4  5  3 
 5  1  2  |  3  4  8  |  6  9  7 

# of calls: 3307
extreme = [
    [0, 0, 5, 4, 0, 0, 0, 0, 9],
    [0, 2, 0, 0, 6, 0, 0, 7, 0],
    [6, 0, 0, 0, 0, 5, 1, 0, 0],
    [1, 0, 0, 0, 0, 3, 7, 0, 0],
    [0, 7, 0, 0, 9, 0, 0, 4, 0],
    [0, 0, 3, 2, 0, 0, 0, 0, 6],
    [0, 0, 1, 8, 0, 0, 0, 0, 7],
    [0, 6, 0, 0, 7, 0, 0, 2, 0],
    [9, 0, 0, 0, 0, 2, 8, 0, 0],
]
assert check_sizes(extreme)

solve_sudoku.num_calls = 0
if solve_sudoku(extreme):
    print_board(extreme)
else:
    print("No solution exists.")
print(f'\n# of calls: {solve_sudoku.num_calls}')
 7  1  5  |  4  3  8  |  2  6  9 
 3  2  4  |  9  6  1  |  5  7  8 
 6  8  9  |  7  2  5  |  1  3  4 
--------------------------------
 1  9  6  |  5  4  3  |  7  8  2 
 8  7  2  |  1  9  6  |  3  4  5 
 4  5  3  |  2  8  7  |  9  1  6 
--------------------------------
 2  3  1  |  8  5  4  |  6  9  7 
 5  6  8  |  3  7  9  |  4  2  1 
 9  4  7  |  6  1  2  |  8  5  3 

# of calls: 4428