Artificial intelligence or quantum computing to solve Sudoku problems?

As an interesting board game, after the 100th anniversary of Sudoku's birth, how did it become one of the focal points of computing research? Explore how to use artificial intelligence or quantum computers to create an intelligent Sudoku solver from scratch.

Before delving deeper, let’s learn about history

Mark Bloch said: "History is called the mother of disciplines." So, let's talk about how the famous Sudoku game was born. This story can be traced back to the end of the 19th century and originated in France. The French daily "Le Siecle" (Le Siecle) released a 9x9-sized guessing game that requires arithmetic operations instead of logical operations. Its numbers are two digits instead of 1-9. The nature of its game is similar to Sudoku, that is, adding the numbers in the horizontal, column and diagonal lines will also get the same number. In 1979, the retired architect and puzzler Howard Garns is considered the creator of the modern Sudoku game, which was first published in Dell Magazine under the name of a digital place. In 1986, a Japanese puzzle company called Nikoli published this puzzle for the first time under the name of Sudoku.

In the framework of solving the problem of Sudoku game

Sudoku is a real example of the constraint satisfaction problem (CSP) because the variable set, domain set and constraint set are all limited. We have to enter a number between 1-9 in a 9x9 table, so that each row, column, and 3x3 sub-table contains only one number. There is another variation of Sudoku, namely Diagonal Sudoku, which prescribes a set of additional constraints in each diagonal of the table diagonal, and each number must have exactly one characteristic. We know that the constraints satisfy the domain, the optimal solution must satisfy all constraints, or more specifically, it should obey the rules of the game. The optimal solution will satisfy all constraints in the set, thus solving the problem.

Computationally, you can use non-deterministic polynomial time (NP) to solve the constraints of solving Sudoku, because some very special brute force algorithms can be used to solve the constraints, and the validity of the solution set can also be tested in polynomial time, where the input The problem is related to a set of solutions for the length of the polynomial. The fully solved Sudoku is an example of a Latin grid (as Euler said, the nxn array is filled with n different symbols). The Sudoku problem can be considered as a graphics coloring problem, where we only need to use 9 colors to color graphics, and the naked letters can be considered as partial colors.

Use artificial intelligence algorithm sets to satisfy constraints

The basic principle of computational science is the ability to rely on logic to satisfy certain constraints. When solving Sudoku problems, we must train the solver to find some specific winning modes in addition to the basic rules. Therefore, the problem is that the system not only blindly follows the rules, but also makes some decisions while considering its short-term and long-term effects. These patterns are called heuristics. Similar to expert players who chanced upon game knowledge and skills, only understanding the basic rules does not make them game experts. Therefore, when we develop algorithms and solve problems, we must keep in mind useful heuristics, and we should also include them in the program to make it smarter and more useful when we win.

For our Sudoku Solver, we will enter a sequence of 81 numbers as a string and use'. '(Period) indicates unresolved numbers. To solve this problem, we replace "." with all possible numbers that can be placed in the cell.

According to the limitations of Sudoku, we cannot use a number multiple times in a row, column or 3x3 sub-square near any cell. In the case of diagonal Sudoku, we must also consider the same constraints. We first replace the periods with all possible numbers 1 to 9. We use the following grid_values ​​function to do this programmatically.

# For the sake of caluclation we take rows as alphaneumeric and columns as numeric. 
rows = 'ABCDEFGHI' 
columns = '123456789'
boxes = [r + c for r in rows for c in columns]  #every possible cell combination in the grid.

def grid_values(grid):
  """
  Take in the Unsolved Sudoku Sequence and replaces the unsolved boxes initially with all
  possible values which can get into that cell. Lastly returns a dictionary containing the 
  values at all cell positions along with cells. 
  """
  values = []
  every_digits = '123456789'
  for n in grid:
    if c == '.':   # replacing every unsolved value with every possible value initially.
      values.append(every_digits)
    else:          # if already solved, causing it no change. 
      values.append(c)
   assert len(values) == 81
   return dict(zip(boxes, values)) #returning the sudoku grid with all possible cell values. 

First assign all possible values ​​in all unresolved cells.

Now, we have replaced the unresolved cells with all possible numbers between 1 and 9. From the basic rules of Sudoku, we know that if the numbers have been used in the row, column and 3x3 subfield, we can’t Use it twice. So let's eliminate them, if we encounter them in the unresolved grid, we have initially filled them with all possible numbers. So let's take a look at how to use the elimination python method to eliminate those irrelevant numbers from unresolved units.

columns_reversed = columns[::-1] #reversing the columns for calculating the Diagonal Units.

def make_combinations(m, n):
  """
  Takes in input of generally iterables and creates all possible combintation out of them.
  args:
   a: string iterable
   b: string iterable
   return : list of all possible combination. 
  """
  return [x + y for x in m for y in n]

row_units = [make_combinations(r, columns) for r in rows]
column_units = [make_combinations(rows, c) for c in columns]
sub_square_units = [make_combinations(m, n) for m in ('ABC', 'DEF', 'GHI') 
                    for n in ('123','456','789')]
diagonal_1_units = [[rows[i]+columns[i] for i in range(len(rows))]]
diagonal_2_units = [[rows[i]+columns_reversed[i] for i in range(len(rows))]]
diagonal_units = diagonal_1_units + diagonal_2_units
all_units = row_units + column_units + square_units + diagonal_units
units = dict((b, [u for u in all_units if b in u]) for b in boxes)
peers = dict((b, set(sum(units[b], [])) - {b}) for b in boxes)

def eliminate(values):
  """
  Eliminate the redundant numbers from the unsolved cells if the number already appeared once 
  in the peer of the current cell. 
  What we do here is we erase that redundant number from the unsolved value cells if appeared once. 
  """
  solved_cells = [box for box in values.keys() if len(values[box]) == 1] # cell is solved if there's only one digit
  for box in solved_cells:
    value_at_cell = values[box]   # retrieve the current value at that cell.
    for peer in peers[box]:       # check for the cell's peers if the value appears again.
      values[peer] = values[peer].replace(value_at_cell, '')
   return values   # return the modified values dictionary.

Therefore, while satisfying these constraints, sometimes we will encounter some cells that can only place one number, but except for this number, other numbers are no longer feasible for this particular cell. We have to fill in these contents first. There is a proper solution. We call this the "only choice", and it is the simplest heuristic method for solving Sudoku grid cells.

def only_choice(values):
  """
  If in order to satisfy the constraints of the Sudoku Puzzle there is only a single viable option
  we fill in the Cell with that option only and thereby obtain a solve for the cell. 
  
  """
  for unit in all_units:     #searching across all the vicinity of the cell.
    for digit in '123456789':  
      to_be_filled = [cell for cell in unit if unit in values[unit]]
      if len(to_be_filled) == 1:  # if there exists only a single cell in the unit which is not solved
        values[to_be_filled[0]] = digit  # We fill in the cell with its proper answer.    
  return values

In the process surrounding constraint satisfaction so far, the following situation may arise: there will be two unresolved pixels in a cell (considering rows, columns and 3x3 subsquares), of which only two specific remaining numbers can be allocated. Therefore, these two numbers can be effectively deleted from the possible numbers on other cells in the same cell. This heuristic method is called naked twins. The implementation of this algorithm specifically made a deep copy of the grid values, and checked the feasibility of naked twins, that is, whether there are two unresolved pixels that can only accept two specific values. If feasible, it will continue to merge Delete the cells in the same cell of these two values ​​from the other two values. We use the nude_twins function shown below to implement it programmatically:

def naked_twins(values):
	"""
	If there are two unsolved cells in a same unit exist such that it can only be filled by only 
	two specific digits, then those two digits can be safely removed from all other cells in the same unit.
	"""
	twins_possible = [unit for unit in values.keys() if len(values[unit]) == 2]  
	twins = [[unit1, unit2] for unit1 in twins_possible for unit2 in peers[unit1] 
					 if set(values[unit1]) == (set(values[unit2]))]    #confimed Naked Twins
	for twin in twins:
		unit1 = twin[0] 
		unit2 = twin[2]
		peers1 = set(peers[unit1])  
		peers2 = set(peers[unit2])
		common_peers = peers1 & peers2  #finding the intersection between the peers of the two naked twin element
		for peer in common_peers:
			if len(values[peer]) >  1:
				for value in values[unit1]:
					values[peer] = values[peer].replace(val, ''))  # Erasing the values. 
	return values

Now, we try to reduce the problem as much as possible by repeatedly applying these three constraint satisfaction algorithms and checking whether it is stuck and cannot be reduced further. We do this programmatically by using the reduce_puzzle function. What we have to do is to call the first three functions in the for loop, and terminate the function when the number of resolved units in the input and output sequence of the grid value is the same, which means that it cannot be further reduced. Only the constraint satisfaction algorithm .

def reduce_puzzle(values):
	"""
	Applying the 4 Constraint Satisfaction Algorithms until it is not further reducible. 
	Checking if the Number of Solved Cells between the iteration.
	"""
	solved_values = [unit for unit in values.keys() if len(values[unit]) == 1] # considering solved cells
	stuck = False #boolean flag to determine the end of loop
	while not stuck:
		prev_solved_values = len([unit for unit in values.keys() if len(values[unit]) == 1]) #checkpoint 1
		values = eliminate(values) # applying Elimination CSP
		values = only_choice(values) # applying Only Choice CSP
		values = naked_twins(values)  # applying Naked Twins CSP
		after_solved_values = len([unit for unit in values.keys() if len(values[unit]) == 1])
		stuck = after_solved_values == prev_solved_values  # Getting out of loop is the number of solved cell is still the same as the previous iteration.
		
		if len([unit for unit in values.keys() if len(values[unit]) == 0]):
			return False   # if there's problems in the internal representation of the sudoku grid return False. 
	return values			# return the reduced grid values. 	

If the Sudoku grid is still not solved by the constraint satisfaction problem, part of the solution will reach the output, and some of the cells will still be assigned to some possible values. In this case, what we have to do is use the search tree to search for the best set of numbers in those locations. We use the depth-first search (DFS) algorithm to traverse the search tree. So, basically, using DFS, we created several instances with the same grid and tried different possible assignments for each unresolved unit. We recursively ask the CSP algorithm to reduce the grid based on the search results. We implement it programmatically as follows:

def search(values):
	"""
	Recursive Depth-First Search: If the sudoku grid is not further reducible by constraint satisfaction
	a few of the cells will be left with different options and with DFS with search for the optimal 
	values for those yet-unsolved cells.  
	"""
	values = reduce_puzzle(values) # We call the Reduction Function to reduce the puzzle further based on the search results across iterations. 
	if values is False:
		return False
	if all(len(values[b]) == 1 for b in boxes):
		print("Sudoku Problem Solved!")
		return values
	m, n = min((len(values[b]), b) for b in boxes if len(values[b]) > 1)
	for value in values[n]:
		new_sudoku = values.copy()
		new_sudoku[n] = value
		attempted = search(new_sudoku)
		if attempted:
			return attempted		

We use the display sudoku function to display the input string sequence as a two-dimensional 9x9 Sudoku grid:

def display(values):
    """
    Display the values as a 2-D grid.
    Input: The sudoku in dictionary form
    """
    width = 1 + max(len(values[b]) for b in boxes)
    line = '+'.join(['-' * (width * 3)] * 3)
    for r in rows:
        print(''.join(values[r + c].center(width) + ('|' if c in '36' else '')
                      for c in cols))
        if r in 'CF':
            print(line)
    return

In order to solve the Sudoku sequence, we call the above function as follows:

if __name__ == "__main__":
    diag_sudoku_grid = '2.............62....1....7...6..8...3...9...7...6..4...4....8....52.............3'
    values = grid_values(diag_sudoku_grid)
    values = reduce_puzzle(values)
    values = search(values)
    display(values)

The output is shown below, where a set of algorithms has successfully calculated the answer.

A quantum approach to solving Sudoku as a constraint satisfaction problem

Now, we will try to solve a simple Sudoku grid using "quantum simulated annealing". First of all, what is simulated annealing? For such optimization problems, our idea is to use some suboptimal heuristic algorithms and obtain the optimal set of heuristic algorithms to obtain the optimal solution. We use the DWave AQC model (quantum computing for diabetes) here to sample the best solution that meets the constraints discussed earlier.

Use DWave Kerberos hybrid sampler:

In this example, we are using the hybrid solver included with DWave. It runs a parallel search to find the best heuristic. It is a hybrid solver because it uses both quantum computing and classical computing features. It is also a decomposition sampler, using asynchronous workflow in processing. It is included in the Ocean SDK software package of DWave Systems. To start local development, make sure that Python 3.5+ is installed on your system, and then issue the following command.

python -m pip install --upgrade pip
pip install dwave-ocean-sdk

Use Binary Quadratic Model (BQM) for calculation

We cannot construct constraints that are directly ready to be fed to Quantum Computers, we need an intermediate representation to feed them. This is why we will use BQM. Fortunately, DWave Ocean SDK already provides a tool called "composition" that can be used to reduce constraint satisfaction problems to BQM. First of all, as the name suggests, the binary quadratic model itself is an equation system, it is quadratic and expressed in binary. Due to the higher complexity of calculations, Quantum computers use these calculations to greatly speed up the development process. Therefore, in the game, we decided to use the combination tool of dimod, which will return a binary quadratic model, which is the smallest for each of the k combinations of its input variables and internal variables.

We first import the necessary software packages from dwave-ocean-sdk and perform some integrity checks before actually reading into Sudoku Grid.

import dimod  
import math 
import sys
import dimod.generators.constraints import combinations
from hybrid.reference import KerberosSampler

def prettify(row, col, digit):
    return "{row}, {col}_{digit}".format(row, col, digit)

def read_matrix(filename):
    with open(filename, 'r') as f:
        all_lines = f.read()
    lines = []
    for line in all_lines:
        new_line = line.rstrip()
        if new_line:
            new_line = list(map(int, new_line.split(' ')))
            lines.append(new_line)
    return lines 


def sanity_check(matrix):
    n = len(matrix)
    m = int(math.sqrt(n))
    unique_digits = set(range(1, 1+n))

    for row in matrix:
        if set(row) != unique_digits:
            print("Error in row", row)
            return false
    for j in range(n):
        col = [matrix[i][j] for i in range(n)]
        if set(col) != unique_digits:
            print("Error in column", col)

    subsquare_idx = [(i, j) for i in range(m) for j in range(m)]
    for r_scalar in range(m):
        for c_scalar in range(m):
            subsquare = [matrix[i + r_scalar * m ][j + c_scalar * m] for i, j in subsquare_idx]
            if set(subsquare) != unique_digits:
                print('Error in sub-square', subsquare)
                return True

    return True

Now, we use the combination of all available variables of Sudoku Grid's row, column, and sub-square index, and use the combination tool to create a binary quadratic model.

def main():

    if len(sys.argv) > 1:
        filename = sys.argv[1]
        
    matrix = read_matrix(filename)
    n = len(matrix)
    m = int(math.sqrt(n))
    digits = range(1, n+1)

    bqm = dimod.BinaryQuadraticModel({}, {}, 0.0, dimod.SPIN)

    for row in range(n):
        for col in range(n):
            node_digits = [prettify(row, col, digit) for digit in digits]
            one_digit_bqm = combinations(node_digits, 1)
            bqm.update(one_digit_bqm)

    for row in range(n):
        for digit in digits:
            row_nodes = [prettify(row, col, digit) for col in range(n)]
            row_bqm = combinations(row_nodes, 1)
            bqm.update(row_bqm)
    for col in range(n):
        for digit in digits:
            col_nodes = [prettify(row, col, digit) for row in range(n)]
            col_bqm = combinations(col_nodes, 1)
            bqm.update(col_bqm)


if __name__ == "__main__":
    main()

That's it. We have successfully implemented two smart solutions, one of which uses classical calculations and uses a very powerful artificial intelligence heuristic algorithm, which can even solve diagonal Sudoku grids. The second method uses an asynchronous hybrid heuristic sampler, which also happens to use the simulated annealing of the adiabatic quantum calculation model to convert the constraint satisfaction problem into a binary quadratic model to sample it, thereby obtaining the best sampling solution.

Author: Swastik Nath

deephub translation team

Guess you like

Origin blog.csdn.net/m0_46510245/article/details/108766461