使用「消去法」解數獨 - tsungjung411/python-study GitHub Wiki

Leetcode

  • https://leetcode.com/problems/sudoku-solver/ 37. Sudoku Solver
  • Content:
    • Write a program to solve a Sudoku puzzle by filling the empty cells.
    • A sudoku solution must satisfy all of the following rules:
      • Each of the digits 1-9 must occur exactly once in each row.
      • Each of the digits 1-9 must occur exactly once in each column.
      • Each of the the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.
    • Empty cells are indicated by the character '.'


Test cases

Case1

  • board1 = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
  • answer = [["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]

Case2

  • 此 case 無法使用消去法,需使用暴力法下去搜尋
  • board2 = [[".",".","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",".","."]]
  • answer = [["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"]]

程式碼

主要分成三大塊:

  • SudokuSolver 類別。 不管是消去法、暴力法、還是其他方法,SudokuSolver 的實作就是他們的共同核心

    • init_states_and_get_empty_cell_list(self, board): 初始化已經佔用的數值,並回傳空的位置
    • is_accepted(self, row, col, digit): 該數字在指定位置是否能被接受?
    • set_cell(self, row, col, digit): 在指定的位置上,佔用該數值
    • unset_cell(self, row, col, digit): 在指定的位置上數值,取消佔用該數值
    • __repr__(self): 印出棋盤
  • SparseMatrix 類別。 實作 2 維稀疏矩陣,每個列、每個行都是用「雙向鏈結串列(doubly linked list)」來實作。

    • 可以單獨拜訪某一行、某一列、某一個3x3區塊
    • 新增元素時,需要拜訪該列、該行,來插入某元素
    • 可以在 O(1) 刪除該元素,因為有 dict + doubly links
  • Eliminator 類別,則是繼承自 SudokuSolver 類別。

    • eliminate_impossible_digits(self): 實作消去法,並回傳消去的個數
    • fill_in_once_digit_if_possible(self): (實驗性質)
      • 如果無法再度消去,就尋找某一行、某一列、某個區塊,可能性只出現過一次的數字,就是直接填入它。
class SudokuSolver:
    '''
    Implements common methods.
    '''
    
    def __init__(self, board):
        self.int_board = [[0] * 9 for _ in range(9)]
        
        # -----------------------------------------------
        # for the tables below, 
        # we ignores the digit 0 of the digit list [0~9]
        # -----------------------------------------------
        # horizontal states: [row_index][1~9]
        self.row_states = [[False] * 10 for _ in range(9)]

        # vertical states: [col_index][1~9]
        self.col_states = [[False] * 10 for _ in range(9)]

        # block states: [block_row_index][block_col_index][1~9]
        self.block_states = [
            [
                [False] * 10 for _ in range(3) # for cols
            ] for _ in range(3) # for rows
        ]
    # end-of-def
    
    def init_states_and_get_empty_cell_list(self, board):
        empty_cell_list = list()
        
        # (1) collects the empty cells
        # (2) marks the states of the non-empty cells as occupied
        for row in range(9):
            for col in range(9):
                if board[row][col] == '.':
                    digit_set = set()
                    cell = (row, col, digit_set)
                    empty_cell_list.append(cell)
                else:
                    digit = int(board[row][col])
                    self.set_cell(row, col, digit)
                # end-of-if
            # end-of-for
        # end-of-for
        
        for row, col, digit_set in empty_cell_list:
            for digit in range(1, 10):
                if self.is_accepted(row, col, digit):
                    digit_set.add(digit)
                # end-of-if
            # end-of-for
            
            # shows possible digits for empty celss
            #print('[{}][{}]={}'.format(row, col, digit_set))
        # end-of-for
        
        return empty_cell_list
    # end-of-def
    
    def is_accepted(self, row, col, digit):
        '''
        Indicates where the digit on the board (row, col) is accepted.
        '''
        if self.row_states[row][digit]:
            return False
        elif self.col_states[col][digit]:
            return False
        else:
            block_row = row // 3
            block_col = col // 3
            if self.block_states[block_row][block_col][digit]:
                return False
            # end-of-if
        # end-of-if
        return True
    # end-of-def
    
    def set_cell(self, row, col, digit):
        self.int_board[row][col] = digit
        self.row_states[row][digit] = True
        self.col_states[col][digit] = True
        self.block_states[row//3][col//3][digit] = True
    # end-of-def
    
    def unset_cell(self, row, col, digit):
        self.int_board[row][col] = 0
        self.row_states[row][digit] = False
        self.col_states[col][digit] = False
        self.block_states[row//3][col//3][digit] = False
    # end-of-def
    
    def __repr__(self):
        s = '-' * 13 + '\n'
        for r in range(9):
            s += '|'
            for c in range(9):
                digit = self.int_board[r][c]
                s += str(digit) if digit != 0 else ' '
                if c == 2 or c == 5 or c == 8:
                    s += '|'
                # end-of-if
            # end-of-for
            s += '\n'
            
            if r == 2 or r == 5 or r == 8:
                s += '-' * 13 + '\n'
            # end-of-if
        # end-of-for
        return s
    # end-of-def
# end-of-class



class SparseMatrix:
    '''
    The sparse matrix is constructed by the horizontal and vertical 
    doubly linked lists. 

    Where the linked list is used to visit or iterate a row or a 
    column. For example, if a value on the (r, c) place of the 
    Sudoku board is decided, we need to eliminate the same values 
    from the possible digit sets on the r-th row or the c-th column.
    
    In order to locate the cell quickly, cell_dict is supported to 
    get the target cell. And the "doubly" linked list is used to 
    support the quick deletion in O(1).
    '''
    
    class Cell:
        '''
        The cell is an element of the sparse matrix.
        '''
        def __init__(self, row, col, value = None):
            '''
            The value is located at (row, col), and the base index is 0.
            '''
            self.row = row
            self.col = col
            self.value = value
            
            self.left = None
            self.right = None
            self.up = None
            self.down = None
        # end-of-def
        
        def __repr__(self):
            return '[{}][{}]={}'.format(self.row, self.col, self.value)
        # end-of-def
    # end-of-def
    
    def __init__(self, rows, cols):
        self.rows = rows
        self.cols = cols
        
        self.cell_dict = dict()
        self.row_heads = [None] * rows
        self.col_heads = [None] * cols
    # end-of-def
    
    def get_cell(self, row, col) -> Cell:
        return self.cell_dict.get((row, col))
    # end-of-def
    
    def set_cell(self, row, col, value) -> Cell:
        cell = SparseMatrix.Cell(row, col, value)
        
        # registers to the cell_dict
        key = (row, col)
        self.cell_dict[key] = cell
        
        # registers to the horizontal linked list
        head = self.row_heads[row]
        if not head:
            # be the head
            self.row_heads[row] = cell
        else:
            if col < head.col:
                # before the head
                cell.right = head
                head.left = cell
                self.row_heads[row] = head
            else:
                # behind the head
                ptr = head.right
                pre = head
                while ptr != None:
                    if ptr.col < cell.col:
                        pre = ptr
                        ptr = ptr.right
                    else:
                        break
                    # end-of-if
                # end-of-while
                
                if not ptr:
                    # pre < cell (tail)
                    pre.right = cell
                    cell.left = pre
                else:
                    # pre < cell < ptr
                    pre.right = cell
                    cell.left = pre
                    cell.right = ptr
                    ptr.left = cell
                # end-of-if
            # end-of-if
        # end-of-if
        
        # registers to the vertical linked list
        head = self.col_heads[col]
        if not head:
            # be the head
            self.col_heads[col] = cell
        else:
            if row < head.row:
                # before the head
                cell.down = head
                head.up = cell
                self.col_heads[col] = head
            else:
                ptr = head.down
                pre = head
                while ptr != None:
                    if ptr.row < cell.row:
                        pre = ptr
                        ptr = ptr.down
                    else:
                        break
                    # end-of-if
                # end-of-while
                
                if not ptr:
                    # pre < cell (tail)
                    pre.down = cell
                    cell.up = pre
                else:
                    # pre < cell < ptr
                    pre.down = cell
                    cell.up = pre
                    cell.down = ptr
                    ptr.up = cell
                # end-of-if
            # end-of-if
        # end-of-if
        return cell
    # end-of-def
    
    def delete_cell(self, row, col):
        key = (row, col)
        cell = self.cell_dict.get(key)
        
        # deletes the cell from the horizontal linked list
        if cell.left == None:
            # the cell is the head
            self.row_heads[row] = cell.right
        else:
            # the cell is behine the head
            cell.left.right = cell.right
        # end-of-if
        
        if cell.right != None:
            cell.right.left = cell.left
        # end-of-if
        
        # deletes the cell from the vertical linked list
        if cell.up == None:
            # the cell is the head
            self.col_heads[col] = cell.down
        else:
            # the cell is behine the head
            cell.up.down = cell.down
        # end-of-if
        
        if cell.down != None:
            cell.down.up = cell.up
        # end-of-if
        
        # deletes the cell from the celll_dict
        del self.cell_dict[key]
        del cell
    # end-of-if
    
    def move_next_from(self, row = 0, col = 0) -> Cell:
        key = (row, col)
        cell = self.cell_dict.get(key)
        
        # for the first cell and the last one
        if cell == None:
            if row == 0 and col == 0:
                for r in range(0, self.rows):
                    if self.row_heads[r] != None:
                        return self.row_heads[r]
                    # end-of-if
                # end-of-for
            # end-of-if
            return None # cannot find the next cell
        # end-of-if
        
        # finds the next cell from the current row
        if cell.right != None:
            return cell.right
        # end-of-if
        
        # finds the next cell from the next row
        for r in range(row + 1, self.rows):
            if self.row_heads[r] != None:
                return self.row_heads[r]
            # end-of-if
        # end-of-for
        
        return None # cannot find the next cell
    # end-of-if
    
    def get_row_cell_list(self, row):
        cell_list = list()
        ptr = self.row_heads[row]
        
        while ptr != None:
            cell_list.append(ptr)
            ptr = ptr.right
        # end-of-while
        return cell_list
    # end-of-def
    
    def get_col_cell_list(self, col):
        cell_list = list()
        ptr = self.col_heads[col]
        
        while ptr != None:
            cell_list.append(ptr)
            ptr = ptr.down
        # end-of-while
        return cell_list
    # end-of-def
    
    def get_block_cell_list(self, row, col):
        cell_list = list()
        block_row = (row // 3) * 3
        block_col = (col // 3) * 3
        next_block_row = block_row + 3
        next_block_col = block_col + 3
        
        for r in range(block_row, next_block_row):
            for c in range(block_col, next_block_col):
                
                # gets the 1st cell from the block
                cell = self.cell_dict.get((r, c))
                if not cell:
                    continue
                else:
                    cell_list.append(cell)
                # end-of-if
                
                # moves to the next cell (on the same row)
                if cell.right != None: 
                    cell = cell.right
                else:
                    break
                # end-of-if
                
                # gets the 2nd cell
                if cell.col < next_block_col: 
                    cell_list.append(cell)
                else:
                    break
                # end-of-if
                
                # moves to the next cell (on the same row)
                if cell.right != None: 
                    cell = cell.right
                else:
                    break
                # end-of-if
                
                # gets the 3rd cell
                if cell.col < next_block_col: 
                    cell_list.append(cell)
                else:
                    break
                # end-of-if
            # end-of-for 
        # end-of-for
        return cell_list
    # end-of-if
    
    def get_cell_list_by_row(self):
        cell_list = list()
        
        for row in range(self.rows):
            ptr = self.row_heads[row]
            while ptr != None:
                cell_list.append(ptr)
                ptr = ptr.right
            # end-of-while
        # end-of-for
        return cell_list
    # end-of-def
    
    def get_cell_list_by_col(self):
        cell_list = list()
        
        for col in range(self.cols):
            ptr = self.col_heads[col]
            while ptr != None:
                cell_list.append(ptr)
                ptr = ptr.down
            # end-of-while
        # end-of-for
        return cell_list
    # end-of-def
        
    def __repr__(self):
        s = ''
        for cell in self.get_cell_list_by_row():
            s += str(cell) + '\n'
        # end-of-for
        return s
    # end-of-if
    
    def debug_rows(self):
        print('\n--- debug_rows ---')
        for row in range(self.rows):
            ptr = self.row_heads[row]
            print(' - row_heads[{}]:'.format(row))
            while ptr != None:
                print('   {} <- ({}) -> {}'.format(
                    ptr.left, ptr, ptr.right))
                ptr = ptr.right
            # end-of-while
            print()
        # end-of-for
    # end-of-def
    
    def debug_cols(self):
        print('\n--- debug_cols ---')
        for col in range(self.cols):
            ptr = self.col_heads[col]
            print(' - col_heads[{}]:'.format(col))
            while ptr != None:
                print('   {} <- ({}) -> {}'.format(
                    ptr.up, ptr, ptr.down))
                ptr = ptr.down
            # end-of-while
            print()
        # end-of-for
    # end-of-def
# end-of-class



class Eliminator(SudokuSolver):
    '''
    Implements the elimination method for Sudoku.
    '''
    def __init__(self, board):
        super(Eliminator, self).__init__(board)
        
        empty_cell_list = self.init_states_and_get_empty_cell_list(board)
        
        # used to list possible ditigts for empty cells
        self.empty_cell_table = SparseMatrix(9, 9)
        
        for row, col, digit_set in empty_cell_list:
            self.empty_cell_table.set_cell(row, col, digit_set)
        # end-of-for
    # end-of-def
    
    def run(self):
        count = 1
        while count > 0:
            count = self.eliminate_impossible_digits()
            #print('After deleting impossible digits:', count)
            #print(e)
        # end-of-while
    # end-of-def
    
    def eliminate_impossible_digits(self) -> int:
        '''
        If a cell has only one possible, then just fill-in it.
        
        Output:
            - the number of cells which have only one possible
        '''
        row = col = 0
        cell = self.empty_cell_table.move_next_from(row, col)
        deleted_cell_list = list()
        
        while cell != None:
            digit_set = cell.value
            row = cell.row
            col = cell.col
            
            # prints the current cell
            # print('({}, {}) = {}'.format(row, col, digit_set))
            
            # moves to the next cell before deleting the cell
            cell = self.empty_cell_table.move_next_from(row, col)
            
            if len(digit_set) == 1:
                # prints the deletion info of the current cell
                # print('delete [{}][{}] = {}'.format(row, col, digit_set))
                
                digit = digit_set.pop()
                deleted_cell_list.append((row, col, digit))
                
                if self.set_cell(row, col, digit) == False:
                    return -1
                # end-of-if
                self.empty_cell_table.delete_cell(row, col)
            # end-of-if
        # end-of-while
        
        # removes other impossible digits
        for row, col, digit in deleted_cell_list:
            
            row_cell_list = self.empty_cell_table.get_row_cell_list(row)
            for cell in row_cell_list:
                digit_set = cell.value
                if digit in digit_set:
                    digit_set.remove(digit)
                # end-of-if
            # end-of-for
            
            col_cell_list = self.empty_cell_table.get_col_cell_list(col)
            for cell in col_cell_list:
                digit_set = cell.value
                if digit in digit_set:
                    digit_set.remove(digit)
                # end-of-if
            # end-of-for
            
            block_cell_list = self.empty_cell_table.get_block_cell_list(row, col)
            for cell in block_cell_list:
                digit_set = cell.value
                if digit in digit_set:
                    digit_set.remove(digit)
                # end-of-if
            # end-of-for
        # end-of-for
        
        return len(deleted_cell_list)
    # end-of-def
    
    def fill_in_once_digit_if_possible(self) -> int:
        # for example
        #  [1][1]={8, 1, 3, 4, 5}
        #  [1][2]={1, 3, 5}
        #  [1][4]={3, 5}
        #  [1][6]={8, 1, 4}
        #  [1][7]={8, 1, 3, 5}
        #  [1][8]={3, 4, 5, 9}
        # We can make sure that board[1][8] must be 9
        
        def fill_digits(cell_list):
            digit2cell_dict = dict()
            
            # visits each digit in the cell list
            for cell in cell_list:
                for digit in cell.value:
                    exist_cell = digit2cell_dict.get(digit)
                    if not exist_cell:
                        digit2cell_dict[digit] = cell
                    else:
                        # more than once -> impossible
                        digit2cell_dict[digit] = -1
                    # end-of-if
                # end-of-if
            # end-of-for
            
            for digit, cell in digit2cell_dict.items():
                if cell != -1:
                    print('>> [{}][{}]={}'.format(cell.row, cell.col, digit))
                    self.set_cell(cell.row, cell.col, digit)
                    cell.value.remove(digit)
                # end-of-if
            # end-of-for
        # end-of-def
        
        mat = self.empty_cell_table
        for idx in range(9):
            # for each row and column
            fill_digits(mat.get_row_cell_list(idx))
            fill_digits(mat.get_col_cell_list(idx))
            
            # the left-top cell of the idx-th block
            cell_offset_idx = idx * 3
            cell_row = (cell_offset_idx // 9) * 3
            cell_col = cell_offset_idx % 9
            fill_digits(mat.get_block_cell_list(cell_row, cell_col))
        # end-of-for
    # end-of-def
# end-of-class



def main():
    board1 = [
        ["5","3",".",".","7",".",".",".","."],
        ["6",".",".","1","9","5",".",".","."],
        [".","9","8",".",".",".",".","6","."],
        ["8",".",".",".","6",".",".",".","3"],
        ["4",".",".","8",".","3",".",".","1"],
        ["7",".",".",".","2",".",".",".","6"],
        [".","6",".",".",".",".","2","8","."],
        [".",".",".","4","1","9",".",".","5"],
        [".",".",".",".","8",".",".","7","9"]]

    board2 = [
        [".",".","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",".","."]]
    
    board = board1
    e = Eliminator(board)
    print(e)
    e.run()
    print(e)
# end-of-def

main()

board1 執行結果:

-------------
|53 | 7 |   |
|6  |195|   |
| 98|   | 6 |
-------------
|8  | 6 |  3|
|4  |8 3|  1|
|7  | 2 |  6|
-------------
| 6 |   |28 |
|   |419|  5|
|   | 8 | 79|
-------------

-------------
|534|678|912|
|672|195|348|
|198|342|567|
-------------
|859|761|423|
|426|853|791|
|713|924|856|
-------------
|961|537|284|
|287|419|635|
|345|286|179|
-------------


board2 執行結果:
-------------
|  9|748|   |
|7  |   |   |
| 2 |1 9|   |
-------------
|  7|   |24 |
| 64| 1 |59 |
| 98|   |3  |
-------------
|   |8 3| 2 |
|   |   |  6|
|   |275|9  |
-------------

-------------
|  9|748|   |
|7  |6 2|   |
| 2 |1 9|   |
-------------
|  7|986|241|
|264|317|598|
|198|524|367|
-------------
|   |863| 2 |
|   |491|  6|
|   |275|9  |
-------------

[0][0]={3, 5, 6}
[0][1]={1, 3, 5}
[0][6]={1, 6}
[0][7]={1, 3, 5}
[0][8]={2, 3, 5}
[1][1]={8, 1, 3, 4, 5}
[1][2]={1, 3, 5}
[1][4]={3, 5}
[1][6]={8, 1, 4}
[1][7]={8, 1, 3, 5}
[1][8]={3, 4, 5, 9}
[2][0]={8, 3, 4, 5, 6}
[2][2]={3, 5, 6}
[2][4]={3, 5}
[2][6]={8, 4, 6, 7}
[2][7]={8, 3, 5, 7}
[2][8]={3, 4, 5}
[3][0]={3, 5}
[3][1]={3, 5}
[6][0]={4, 5, 9}
[6][1]={1, 4, 5, 7}
[6][2]={1, 5}
[6][6]={1, 4, 7}
[6][8]={4, 5}
[7][0]={3, 5, 8}
[7][1]={3, 5, 7, 8}
[7][2]={2, 3, 5}
[7][6]={8, 7}
[7][7]={8, 3, 5, 7}
[8][0]={8, 3, 4, 6}
[8][1]={8, 1, 3, 4}
[8][2]={1, 3, 6}
[8][7]={8, 1, 3}
[8][8]={3, 4}
找不到只剩一種可能的位置。
因此,必須使用遞迴暴力法下去解決

另外,從第一列來觀察,可以看出 9 只出現在 board[1][8]
[1][1]={8, 1, 3, 4, 5}
[1][2]={1, 3, 5}
[1][4]={3, 5}
[1][6]={8, 1, 4}
[1][7]={8, 1, 3, 5}
[1][8]={3, 4, 5, 9}
因此,可以推斷 board[1][8] 必然是 9

對每列、每行、每個區塊掃過一次,可以找出可填的數字:
[0][8]=2
[6][0]=9
[1][8]=9
[7][2]=2
不過這樣的檢查,只能消去零星,感覺幫助不到。
⚠️ **GitHub.com Fallback** ⚠️