Smarter Sudoku Solver

By Alexander Radu

My smarter algorithm explained below aims to cut down on the run time and solve much more difficult sudoku puzzles in a reasonable amount of time.

In [0]:
import sys

f = open(sys.argv[1], 'r')
#f = open('SudokuBoards.txt', 'r')
reading = f.read().strip().split('\n')

boardName = sys.argv[3]
#boardName = 'name,Easy-NYTimes,unsolved'

f.close()

Cliques

Each sublist in cliques dictates the indices of nine squares where each digit must be unique within its respective sublist. The first set of sublists denotes the rows of the sudoku board. The second set of sublists denotes the columns of the sudoku board. The third set of sublists denotes the subgrids of the sudoku board.

In [0]:
cliques = [[0, 1, 2, 3, 4, 5, 6, 7, 8],   \
    [9, 10, 11, 12, 13, 14, 15, 16, 17],  \
    [18, 19, 20, 21, 22, 23, 24, 25, 26], \
    [27, 28, 29, 30, 31, 32, 33, 34, 35], \
    [36, 37, 38, 39, 40, 41, 42, 43, 44], \
    [45, 46, 47, 48, 49, 50, 51, 52, 53], \
    [54, 55, 56, 57, 58, 59, 60, 61, 62], \
    [63, 64, 65, 66, 67, 68, 69, 70, 71], \
    [72, 73, 74, 75, 76, 77, 78, 79, 80], \
    [0, 9, 18, 27, 36, 45, 54, 63, 72],   \
    [1, 10, 19, 28, 37, 46, 55, 64, 73],  \
    [2, 11, 20, 29, 38, 47, 56, 65, 74],  \
    [3, 12, 21, 30, 39, 48, 57, 66, 75],  \
    [4, 13, 22, 31, 40, 49, 58, 67, 76],  \
    [5, 14, 23, 32, 41, 50, 59, 68, 77],  \
    [6, 15, 24, 33, 42, 51, 60, 69, 78],  \
    [7, 16, 25, 34, 43, 52, 61, 70, 79],  \
    [8, 17, 26, 35, 44, 53, 62, 71, 80],  \
    [0, 1, 2, 9, 10, 11, 18, 19, 20],     \
    [3, 4, 5, 12, 13, 14, 21, 22, 23],    \
    [6, 7, 8, 15, 16, 17, 24, 25, 26],    \
    [27, 28, 29, 36, 37, 38, 45, 46, 47], \
    [30, 31, 32, 39, 40, 41, 48, 49, 50], \
    [33, 34, 35, 42, 43, 44, 51, 52, 53], \
    [54, 55, 56, 63, 64, 65, 72, 73, 74], \
    [57, 58, 59, 66, 67, 68, 75, 76, 77], \
    [60, 61, 62, 69, 70, 71, 78, 79, 80]]

Reading the File

The desired sudoku board was isolated and then interpreted into a list of length 81. Each blank was replaced with a zero, and each given number was kept as it was given. The list of integers was then stored in the variable, board.

In [19]:
for number in range(len(reading)):
    if reading[number].strip() == boardName:
        reading = reading[number + 1: number + 10]
        
        break

reading = ','.join(reading).replace('_', '0')
reading = reading.split(',')
board = [int(element) for element in reading]

print('The Processed Sudoku Board: ' + str(board))
print('The Length of the List: ' + str(len(board)))
The Processed Sudoku Board: [0, 0, 4, 1, 0, 0, 5, 2, 7, 2, 1, 3, 7, 0, 0, 0, 0, 0, 0, 0, 7, 6, 2, 4, 0, 0, 0, 3, 5, 0, 2, 7, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 8, 7, 5, 0, 4, 0, 0, 0, 6, 0, 1, 3, 4, 7, 2, 0, 1, 0, 0, 5, 0, 0, 3, 1, 0, 6, 2, 0, 0, 9, 9, 0, 0, 0, 0, 0, 1, 8, 0]
The Length of the List: 81

Grouping the Neighbors

Each square of the sudoku board must contain a number that is different from those of its neighbors. The neighbors of each square can be accessed from the neighbors dictionary with the key being the index of the square and the value being a set with the indices of each neighbor.

In [20]:
neighbors = {}

for index in range(len(board)):
    if board[index] == 0:
        unique = set()
        
        for cluster in cliques:
            for number in cluster:
                if index == number:
                    unique.update(cluster)
        
        neighbors[index] = unique

print('The Neighbors for Each Square: ' + str(neighbors))
print('Number of Blanks: ' + str(len(neighbors)))
The Neighbors for Each Square: {0: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 18, 19, 20, 27, 36, 45, 54, 63, 72}, 1: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 18, 19, 20, 28, 37, 46, 55, 64, 73}, 4: {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 21, 22, 23, 31, 40, 49, 58, 67, 76}, 5: {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 21, 22, 23, 32, 41, 50, 59, 68, 77}, 13: {3, 4, 5, 9, 10, 11, 12, 13, 14, 15, 16, 17, 21, 22, 23, 31, 40, 49, 58, 67, 76}, 14: {3, 4, 5, 9, 10, 11, 12, 13, 14, 15, 16, 17, 21, 22, 23, 32, 41, 50, 59, 68, 77}, 15: {6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 24, 25, 26, 33, 42, 51, 60, 69, 78}, 16: {6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 24, 25, 26, 34, 43, 52, 61, 70, 79}, 17: {6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 24, 25, 26, 35, 44, 53, 62, 71, 80}, 18: {0, 1, 2, 9, 10, 11, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 36, 45, 54, 63, 72}, 19: {0, 1, 2, 9, 10, 11, 18, 19, 20, 21, 22, 23, 24, 25, 26, 28, 37, 46, 55, 64, 73}, 24: {6, 7, 8, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 33, 42, 51, 60, 69, 78}, 25: {6, 7, 8, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 34, 43, 52, 61, 70, 79}, 26: {6, 7, 8, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 35, 44, 53, 62, 71, 80}, 29: {2, 11, 20, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 45, 46, 47, 56, 65, 74}, 32: {5, 14, 23, 27, 28, 29, 30, 31, 32, 33, 34, 35, 39, 40, 41, 48, 49, 50, 59, 68, 77}, 33: {6, 15, 24, 27, 28, 29, 30, 31, 32, 33, 34, 35, 42, 43, 44, 51, 52, 53, 60, 69, 78}, 34: {7, 16, 25, 27, 28, 29, 30, 31, 32, 33, 34, 35, 42, 43, 44, 51, 52, 53, 61, 70, 79}, 35: {8, 17, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 42, 43, 44, 51, 52, 53, 62, 71, 80}, 36: {0, 9, 18, 27, 28, 29, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 54, 63, 72}, 37: {1, 10, 19, 27, 28, 29, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 64, 73}, 38: {2, 11, 20, 27, 28, 29, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 56, 65, 74}, 39: {3, 12, 21, 30, 31, 32, 36, 37, 38, 39, 40, 41, 42, 43, 44, 48, 49, 50, 57, 66, 75}, 41: {5, 14, 23, 30, 31, 32, 36, 37, 38, 39, 40, 41, 42, 43, 44, 48, 49, 50, 59, 68, 77}, 45: {0, 9, 18, 27, 28, 29, 36, 37, 38, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 63, 72}, 47: {2, 11, 20, 27, 28, 29, 36, 37, 38, 45, 46, 47, 48, 49, 50, 51, 52, 53, 56, 65, 74}, 48: {3, 12, 21, 30, 31, 32, 39, 40, 41, 45, 46, 47, 48, 49, 50, 51, 52, 53, 57, 66, 75}, 49: {4, 13, 22, 30, 31, 32, 39, 40, 41, 45, 46, 47, 48, 49, 50, 51, 52, 53, 58, 67, 76}, 51: {6, 15, 24, 33, 34, 35, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 60, 69, 78}, 57: {3, 12, 21, 30, 39, 48, 54, 55, 56, 57, 58, 59, 60, 61, 62, 66, 67, 68, 75, 76, 77}, 59: {5, 14, 23, 32, 41, 50, 54, 55, 56, 57, 58, 59, 60, 61, 62, 66, 67, 68, 75, 76, 77}, 60: {6, 15, 24, 33, 42, 51, 54, 55, 56, 57, 58, 59, 60, 61, 62, 69, 70, 71, 78, 79, 80}, 62: {8, 17, 26, 35, 44, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 69, 70, 71, 78, 79, 80}, 63: {0, 9, 18, 27, 36, 45, 54, 55, 56, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74}, 66: {3, 12, 21, 30, 39, 48, 57, 58, 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 75, 76, 77}, 69: {6, 15, 24, 33, 42, 51, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 78, 79, 80}, 70: {7, 16, 25, 34, 43, 52, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 78, 79, 80}, 73: {1, 10, 19, 28, 37, 46, 54, 55, 56, 63, 64, 65, 72, 73, 74, 75, 76, 77, 78, 79, 80}, 74: {2, 11, 20, 29, 38, 47, 54, 55, 56, 63, 64, 65, 72, 73, 74, 75, 76, 77, 78, 79, 80}, 75: {3, 12, 21, 30, 39, 48, 57, 58, 59, 66, 67, 68, 72, 73, 74, 75, 76, 77, 78, 79, 80}, 76: {4, 13, 22, 31, 40, 49, 57, 58, 59, 66, 67, 68, 72, 73, 74, 75, 76, 77, 78, 79, 80}, 77: {5, 14, 23, 32, 41, 50, 57, 58, 59, 66, 67, 68, 72, 73, 74, 75, 76, 77, 78, 79, 80}, 80: {8, 17, 26, 35, 44, 53, 60, 61, 62, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80}}
Number of Blanks: 43

Finding Valid Numbers

The vaidMoves() method updates the possible dictionary with numbers that could be placed in each square of the sudoku board.

In [21]:
possible = {}
digits = {1, 2, 3, 4, 5, 6, 7, 8, 9}

def validMoves():
    for key in neighbors:
        used = set()

        for location in neighbors[key]:
            used.add(board[location])

        possible[key] = digits.difference(used)

validMoves()
print('Possible Numbers: ' + str(possible))
Possible Numbers: {0: {8, 6}, 1: {8, 9, 6}, 4: {8, 9}, 5: {8, 9, 3}, 13: {8, 9, 5}, 14: {8, 9, 5}, 15: {9, 4, 6}, 16: {9, 4, 6}, 17: {8, 4, 6}, 18: {8, 5}, 19: {8, 9}, 24: {9, 3}, 25: {9, 3}, 26: {8, 1}, 29: {8, 9, 6}, 32: {8, 1, 9}, 33: {9, 4, 6}, 34: {9, 4, 6}, 35: {4, 6}, 36: {1, 6}, 37: {9, 2, 6}, 38: {9, 6}, 39: {9, 4}, 41: {1, 9}, 45: {8, 7}, 47: {8, 9}, 48: {8, 9, 5}, 49: {8, 9, 5}, 51: {9, 2}, 57: {8, 9, 3}, 59: {8, 9, 3}, 60: {3, 6}, 62: {6}, 63: {8, 5}, 66: {8, 4, 5}, 69: {4, 7}, 70: {4}, 73: {6}, 74: {5, 6}, 75: {3, 4, 5}, 76: {4, 5}, 77: {3, 5, 7}, 80: {2, 4, 6}}

Next Logical Step

The step() method modifies the inputted sudoku board to replace empty squares with only one valid number possibility with that number possibility.

In [22]:
def step():
    for key in possible:
        if len(possible[key]) == 1:
            board[key] = possible[key].pop()
            del neighbors[key]

step()
print('Number of Blanks: ' + str(len(neighbors)))
Number of Blanks: 40

Algorithm Descprition

For easier puzzles, using the next logical step may solve the entire board. On the other hand, for harder puzzles, this method may be done at first to optimize the solution, but a different algorithm needs to be used to completely solve the sudoku board.

In [25]:
while board.count(0):
    validMoves()
    step()

print('Solved: ' + str(board))
Solved: [6, 9, 4, 1, 8, 3, 5, 2, 7, 2, 1, 3, 7, 9, 5, 4, 6, 8, 5, 8, 7, 6, 2, 4, 9, 3, 1, 3, 5, 8, 2, 7, 1, 6, 9, 4, 1, 2, 6, 4, 3, 9, 8, 7, 5, 7, 4, 9, 8, 5, 6, 2, 1, 3, 4, 7, 2, 9, 1, 8, 3, 5, 6, 8, 3, 1, 5, 6, 2, 7, 4, 9, 9, 6, 5, 3, 4, 7, 1, 8, 2]

Presenting the Answer

The answer variable will contain the solved sudoku board in the string format.

In [28]:
column = 0
answer = ''

for item in board:
    answer += str(item)
    
    if column == 8:
        column = 0
        answer += '\n'

    else:
        answer += ','
        column += 1

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

Writing the File

The solved version of the sudoku board is written in the output file in a manner that is fairly appealing to the user's eye. The numbers are organized in a nine by nine square that closely resembles the structure of an actual sudoku board.

In [0]:
g = open(sys.argv[2], 'w')
#g = open('TestOutput.txt', 'w')
g.write(response.strip())

g.close()

Conclusion

Overall, writing the smarter sudoku algorithm directed me to explore a variety of methods that Python has for sets and the usefulness of this data structure.

Naive Algorithm Backtracks / Smart Algorithm Backtracks

The calculated ratios comparing my naive sudoku algorithm to my smarter sudoku algorithm are the following:

  • Board A7-1: 19805704 / 9468251 = 2.0918
  • Board A8-1: 155373524 / 71468125 = 2.1740

My smarter algorithm tends to take half as many backtracks as my naive algorithm.