pythonalgorithmsudoku

Repeating values on recursive sudoku solver in python


I'm trying to make a sudoku solver using recursion by storing possible values in a dictionary with index as key and the set of possible values as value. Parameter b is just the board expressed as a single list, and .get_row, .get_col, .get_box methods return their respective values properly. If an entry in the dictionary has a set with a single element then it gets put into the board b, otherwise if there's no possible set with a single element a value gets picked from the set of possible numbers and the recursion starts. This is the full repeatable code:

class Sudoku:
    def solve_it(self, board: List[List[str]]) -> None:
        """
        :param board: sudoku board gets modified inplace at .create_board_from_list
        :return: None
        """
        unit_values = []
        for i in range(len(board)):  # flattening the board into unit_values
            for j in range(len(board[i])):
                unit_values.append(board[i][j])
        self.go_through_recursive(board, unit_values)

    def get_row(self, n: int, b):  # retrieve all elements of nth row in the board
        assert 0 <= n <= 8
        return set(b[9 * n: 9 * (n + 1)])

    def get_col(self, n: int, b):  # retrieve all elements of nth col in the board
        assert 0 <= n <= 8
        return set(b[n::9])

    def get_box(self, n: int, b):  # retrieve all elements of nth box in the board
        assert 0 <= n <= 8
        return set(b[i] for i in range(81) if (i // 27) == (n // 3) and i % 9 // 3 == n % 3)

    def go_through_recursive(self, board, b, d=False):
        """
        :param board: sudoku board as matrix where each row is a line
        :param b: sudoku board as a single long list
        """
        numbers = {'1', '2', '3', '4', '5', '6', '7', '8', '9'}
        while True:

            if d:
                return True

            missing_dict = {}  # populate missing_dict
            for idx in range(len(b)):
                if b[idx] == '.':
                    row, col, box = idx // 9, idx % 9, (idx // 27) * 3 + (idx % 9) // 3
                    # union of all the present values in current row, col and box
                    values = self.get_row(row, b).union(self.get_col(col, b)).union(self.get_box(box, b))
                    values.remove('.')
                    missing_ones = numbers.difference(values)  # possible values to input in the slot
                    if len(missing_ones) == 0:  # impossible to continue
                        return False
                    missing_dict[idx] = missing_ones

            old_count = b.count('.')
            # now we iterate through the dictionary of possible values per index,
            # if one index has a set with a single number it means that that's the only possible number so we store it
            for idx, missings in missing_dict.items():  # store assured values
                if len(missings) == 1:
                    b[idx] = missings.pop()

            if b.count('.') == 0:  # check if complete
                self.create_board_from_list(board, b)
                return True

            if b.count('.') == old_count:  # if no progress has been made
                for idx, s in missing_dict.items():  # iterate through the dictionary
                    for number in s:  # create a new board and store indecisive value then recur
                        if d:
                            return True
                        bb = b[:]
                        bb[idx] = number
                        d = self.go_through_recursive(board, bb)

    def create_board_from_list(self, board, b):
        temp_board = []
        chunk = 9
        for idx in range(0, len(b), chunk):
            temp_board.append(b[idx: idx + chunk])
        for idx in range(len(board)):
            board[idx] = temp_board[idx]
        print('done')

The issue I have is that once it completes, some numbers are repeated(either in the rows, cols or boxes).Also I 've noticed that if I check the possible values inside the ' store assured values ' loop it sometimes returns the whole range [1, 9] possibly being the reason of some rewriting:

row, col, box = idx // 9, idx % 9, (idx // 27) * 3 + (idx % 9) // 3
values = self.get_row(row, b).union(self.get_col(col, b)).union(self.get_box(box, b))
values.remove('.')  # some of these values are [1, 9]

Given this board input:

board = 
[[".",".","9","7","4","8",".",".","."],
 ["7",".",".",".",".",".",".",".","."],
 [".","2",".","1",".","9",".",".","."],
 [".",".","7",".",".",".","2","4","."],
 [".","6","4",".","1",".","5","9","."],
 [".","9","8",".",".",".","3",".","."],
 [".",".",".","8",".","3",".","2","."],
 [".",".",".",".",".",".",".",".","6"],
 [".",".",".","2","7","5","9",".","."]]

the correct output is

board =
[["5","1","9","7","4","8","6","3","2"],
 ["7","8","3","6","5","2","4","1","9"],
 ["4","2","6","1","3","9","8","7","5"],
 ["3","5","7","9","8","6","2","4","1"],
 ["2","6","4","3","1","7","5","9","8"],
 ["1","9","8","5","2","4","3","6","7"],
 ["9","7","5","8","6","3","1","2","4"],
 ["8","3","2","4","9","1","7","5","6"],
 ["6","4","1","2","7","5","9","8","3"]]

while the code has the incorrect board

board = 
[["3","1","9","7","4","8","6","5","2"],
 ["7","8","5","6","3","2","1","1","9"],
 ["4","2","6","1","5","9","8","7","3"],
 ["5","3","7","9","8","6","2","4","1"],
 ["2","6","4","3","1","7","5","9","8"],
 ["1","9","8","5","2","4","3","6","7"],
 ["9","7","1","8","6","3","4","2","5"],
 ["8","5","2","4","9","1","7","3","6"],
 ["6","4","3","2","7","5","9","8","4"]]

EDIT:

I changed the go_through_recursive to store the unique possible value directly into the board, in this case the only possible set length in the dictionary is 2 or greater. But this seems to get stuck in an infinite loop

    def go_through_recursive(self, board, b, d=False):
        """
        :param board: sudoku board as matrix where each row is a line
        :param b: sudoku board as a single long list
        """
        numbers = {'1', '2', '3', '4', '5', '6', '7', '8', '9'}
        while True:

            old_count = b.count('.')
            missing_dict = {}  # populate missing_dict
            for idx in range(len(b)):
                if b[idx] == '.':
                    row, col, box = idx // 9, idx % 9, (idx // 27) * 3 + (idx % 9) // 3
                    # union of all the present values in current row, col and box
                    values = self.get_row(row, b).union(self.get_col(col, b)).union(self.get_box(box, b))
                    values.remove('.')
                    missing_ones = numbers.difference(values)  # possible values to input in the slot
                    if len(missing_ones) == 0:  # impossible to continue
                        return False
                    elif len(missing_ones) == 1:
                        b[idx] = missing_ones.pop()
                    else:
                        missing_dict[idx] = missing_ones

            if b.count('.') == 0:  # check if complete
                self.create_board_from_list(board, b)
                return True

            if b.count('.') == old_count:  # if no progress has been made
                for idx, s in missing_dict.items():  # iterate through the dictionary
                    for number in s:  # create a new board and store indecisive value then recur
                        bb = b[:]
                        bb[idx] = number
                        if self.go_through_recursive(board, bb):
                            return True

Solution

  • There are two issues in the first version of your code:

    Not a problem, but as it is related to the second point's variable d: you have two places where you have if d:. You can easily avoid this duplication by placing this block right after you get the boolean back from recursion. Then you'll not even need the d parameter/variable.

    Several other comments could be made, but correcting the above is enough to make it work. Here is the first version of your code with these corrections (The code comments indicate where a change was made):

        def go_through_recursive(self, board, b):  # No d argument needed
            numbers = {'1', '2', '3', '4', '5', '6', '7', '8', '9'}
            while True:
                # Removed the `if d` block from here. See further down.
    
                missing_dict = {}
                for idx in range(len(b)):
                    if b[idx] == '.':
                        row, col, box = idx // 9, idx % 9, (idx // 27) * 3 + (idx % 9) // 3
                        values = self.get_row(row, b).union(self.get_col(col, b)).union(self.get_box(box, b))
                        values.remove('.')
                        missing_ones = numbers.difference(values)
                        if len(missing_ones) == 0:
                            return False
                        missing_dict[idx] = missing_ones
    
                old_count = b.count('.')
     
                for idx, missings in missing_dict.items():
                    if len(missings) == 1:
                        b[idx] = missings.pop()
                        # Must break here because now missing_dict 
                        #    is no longer up to date!
                        break  
    
                if b.count('.') == 0:
                    self.create_board_from_list(board, b)
                    return True
    
                if b.count('.') == old_count:
                    # Only get one entry from this dictionary (no loop)
                    idx, s = next(iter(missing_dict.items()))  
                    for number in s: 
                        bb = b[:]
                        bb[idx] = number
                        # Not needed to assign to d, 
                        #    and the exit if-block can follow here
                        if self.go_through_recursive(board, bb):  
                           return True
                    # This index has no possible value, so backtrack!
                    return False