Рубрики
Без рубрики

Как решить судоку быстрее, чем с backtracking?

TL; DR: Решите некоторые клетки, прежде чем пройти от возвращения! Мне всегда нравилось решение математической головоломки … Помечено Python, Showdev, начинающим.

TL; DR: Решите некоторые клетки, прежде чем пройти от возвращения!

Мне всегда нравилось решить математические головоломки и всегда бороться с поиском развлекательных и сложных проблем для практики кодирования. Тем не менее, когда идея написать сценарий, который решил бы судоку более изощренным образом, чем просто использовать метод Backtracking, пришел к моей голове, я начал думать о решении сразу.

BackTracking, который в значительной степени размещает числа в пустых клетках и проверка, если доска Sudoku по-прежнему действительна, кажется, неэффективным способом решения проблемы. Решение будет найдено в конце концов (если есть решение), но почему бы не попробовать нанести дополнительную логику? Та же логика, которая применяется людьми, использующими карандаш на последней странице газеты с напечатанной головоломкой.

Я реализовал некоторые правила, чтобы облегчить отступление. Каждая клетка, которая решается заранее, резко сокращает набор решений и делает скрипт намного короче. Два правила, которые я решил подать заявку, – это тот, кто когда-либо решил, что когда-либо решил Sudoku, использует и может описать.

  1. Найдите клетку, что можно поместить только одно число: Не существует много усилий, необходимых для поиска ячеек, где можно поставить только одно число. Вы выбираете ячейку и подумаете о возможностях, устраняя номера, которые уже записаны в данной коробке, столбце и строке. Вы нашли, что есть только один вариант? Вы нашли решение.

  2. Найти коробку/столбец/ряд, где номер может быть помещен только в одну клетку: Время применить обратную логику здесь. Вместо того, чтобы искать клетку, которая имеет только одну возможность, которая делает его действительным, вы ищете коробку, столбец или строку и проверьте, если один из номеров отсутствует от него, может быть размещен только в одном месте.

После определения этих правил я решил найти способ запустить мой сценарий. Вот упрощенный блок-схему алгоритма, которая описывает, как работает первая часть моего сценария:

Просто как есть. Простые правила мы все применяли, чтобы решить судоку. Самым интересным еще предстоит прийти: обратно.

Когда я впервые подумал о реализации Backtracking в моем коде, я не знал, что рекурсия не лучший способ написания алгоритмов в Python. Я полностью реализовал способ резурсительно решить проблему, так как обратно, казалось, был идеальным примером использования его и должен был переписать эту часть кода, в значительной степени с нуля. Тем не менее, урок для жизни. Вот как я реализовал возврат в конце концов:

С таком объяснил, вот мой код:

import time #used to check script's run time

#class representing a single cell of sudoku board
class Cell:
    def __init__(self, row_number, column_number, value):
        self.row = row_number #0 thru 8
        self.column = column_number #0 thru 8
        self.box = self.set_box_number() #0 thru 8
        self.value = value #0 if empty cell
        self.possibilities = self.set_initial_possibilities() #empty list or 1 thru 9

    #set box number based on row and column number
    def set_box_number(self):
        box_number = int(self.column/3)
        box_number += int(self.row/3)*3
        return box_number

    #set possibilities to empty list if cell not empty or 1 thru 9
    def set_initial_possibilities(self):
        if self.value != 0:
            return []
        else:
            return [*range(1, 10)] 


#class solving sudoku in 'human' way
class Solver:
    def __init__(self, board):
        self.cells = []
        for row_no, row in enumerate(board):
            for col_no, val in enumerate(row):
                self.cells.append(Cell(row_no, col_no, val))

    #get all cells from box
    def get_cells_from_box(self, box_number):
        return [cell for cell in self.cells if box_number == cell.box]

    #get all cells from column
    def get_cells_from_column(self, column_number):
        return [cell for cell in self.cells if column_number == cell.column]

    #get all cells from row
    def get_cells_from_row(self, row_number):
        return [cell for cell in self.cells if row_number == cell.row] 

    #get all cells within same box, column and row as given cell
    def get_conflicting_cells(self, given_cell):
        return {cell for cell in self.cells if given_cell.box == cell.box or given_cell.column == cell.column or given_cell.row == cell.row} 

    #remove not valid possibilities from all cells
    def set_all_cells_possibilities(self):
        for cell in self.cells:
            self.remove_cell_value_from_others_possibilities(cell)

    #look for a cell with single possibility and solve it
    def find_and_solve_cell_with_one_possibility(self):
        for cell in self.cells:
            if len(cell.possibilities) == 1:
                self.solve_cell(cell, cell.possibilities[0])
                return True
        return False

    #look for unique possibilities in all boxes, column and rows
    def find_and_solve_unique_possibilities(self):
        for number in range (0, 8):
            if self.if_unique_possibility_within_cells(self.get_cells_from_box(number)):
                return True
            if self.if_unique_possibility_within_cells(self.get_cells_from_column(number)):
                return True
            if self.if_unique_possibility_within_cells(self.get_cells_from_row(number)):
                return True
        return False

    #look for unique possibility in group of cells
    def if_unique_possibility_within_cells(self, same_group_cells):
        counts = [0] * 10
        for cell in same_group_cells:
            for possibility in cell.possibilities:
                counts[possibility] += 1
        if 1 in counts:
            unique_value = counts.index(1)
            for cell in same_group_cells:
                if unique_value in cell.possibilities:
                    self.solve_cell(cell, unique_value)
                    return True
            return False

    #solve cell and remove its value from other cells possibilities
    def solve_cell(self, given_cell, value):
        given_cell.value = value
        given_cell.possibilities = []
        self.remove_cell_value_from_others_possibilities(given_cell)

    #remove cell value from possibilites of cells within same box, column and row
    def remove_cell_value_from_others_possibilities(self, given_cell):
        for conflicting_cell in self.get_conflicting_cells(given_cell):
            try:
                conflicting_cell.possibilities.remove(given_cell.value)
            except:
                continue

    #main algorithm of the class
    def run(self):
        while(True):
            if not self.find_and_solve_cell_with_one_possibility():
                if not self.find_and_solve_unique_possibilities():
                    break


#class solving sudoku with backtracking algorithm
class Backtrack():
    def __init__(self, cells):
        self.cells = cells
        self.unsolved_cells = [cell for cell in self.cells if cell.value == 0]

    #get all cells within same box, column and row as given cell
    def get_conflicting_cells_values(self, given_cell):
        return {cell.value for cell in self.cells if given_cell.box == cell.box or given_cell.column == cell.column or given_cell.row == cell.row} 

    #check validity of sudoku if cell is set to given value
    def if_solution_valid(self, given_cell, cell_value_to_check):
        return not cell_value_to_check in self.get_conflicting_cells_values(given_cell)

    #check current cell value and pick next of its possibilities
    def get_next_possibility(self, given_cell):
        if given_cell.value == 0:
            return given_cell.possibilities[0]
        else:
            return given_cell.possibilities[given_cell.possibilities.index(given_cell.value)+1]

    #main algorithm of the class
    def run(self):
        current_cell_number = 0
        while (current_cell_number >= 0 and current_cell_number < len(self.unsolved_cells)):
            current_cell = self.unsolved_cells[current_cell_number]
            try:
                #get next possibility for cell
                next_possibility = self.get_next_possibility(current_cell)
            except IndexError:
                #no more possibilities for cell -> take step back and work on previous cell
                current_cell.value = 0
                current_cell_number -= 1
            else:
                #possibility valid -> change cell value and proceed to next cell
                if self.if_solution_valid(current_cell, next_possibility):
                    current_cell.value = next_possibility
                    current_cell_number += 1
                #possibility not valid -> change cell value and repeat process
                else:
                    current_cell.value = next_possibility


#class responsible for printing sudoku board in readable form
class Printer():
    def __init__(self, cells):
        self.cells = cells

    #print sudoku board
    def print(self):
        last_cell_row_no = 0
        for cell in self.cells:
            if last_cell_row_no != cell.row:
                print()
            print(cell.value, end = " ")
            last_cell_row_no = cell.row
        print("\n- - - - - - - - -")


#class responsible for checking if sudoku solution is valid
class Validator():
    def __init__(self, cells):
        self.cells = cells

    #check if sudoku solution is valid
    def if_valid(self):
        for cell in self.cells:
            if cell.value == 0:
                return False
        return True

    #print sudoku validity
    def print(self):
        if self.if_valid():
            print("Sudoku solved!")
        else:
            print("Sudoku is not valid!")


### main part below ###
def main():

    #sudoku to solve
    sudoku_to_solve = [
    [0, 0, 0, 0, 0, 0, 2, 0, 0],
    [0, 8, 0, 0, 0, 7, 0, 9, 0],
    [6, 0, 2, 0, 0, 0, 5, 0, 0],
    [0, 7, 0, 0, 6, 0, 0, 0, 0],
    [0, 0, 0, 9, 0, 1, 0, 0, 0],
    [0, 0, 0, 0, 2, 0, 0, 4, 0],
    [0, 0, 5, 0, 0, 0, 6, 0, 3],
    [0, 9, 0, 4, 0, 0, 0, 7, 0],
    [0, 0, 6, 0, 0, 0, 0, 0, 0]
    ]

    start_time = time.time() #start timer to measure run time

    solver = Solver(sudoku_to_solve)

    printer=Printer(solver.cells)
    printer.print()

    solver.set_all_cells_possibilities() #checks possibilities for all cells
    solver.run() #comment this line to leave everything to backtracking

    backtrack=Backtrack(solver.cells)
    backtrack.run()

    printer=Printer(backtrack.cells)
    printer.print()

    validator=Validator(backtrack.cells)
    validator.print()

    print("Run time: %s seconds" % (time.time() - start_time))

if __name__ == "__main__":
    main()

Кодекс следует за идеей, показанной в алгоритме блок-схемах: способ решить судоку быстрее, чем только с backtracking. Это может быть доказано: выполните скрипт дважды, сначала с Solver.run () оставлена, как оно, а второе без этой строки (или с # до нее), чтобы пропустить часть, которая упрощает судоку перед возвратом. В зависимости от сложности, время выполнения может значительно уменьшаться. В примере, написанном в скрипте, мое время пробега ок. 6,9 против 64 секунды.

Это может быть даже быстрее, если использовались более сложные правила решения судоку. Я оставлю это так, как сейчас, как я доволен результатом. Эти простые правила помогали уже найти решение намного быстрее.

Найти мой код на github:

Лворчиньский/судоку-решатель

Python Script для решения судоку, ища уникальные возможности и применение метода Backtracking

Оригинал: “https://dev.to/lwolczynski/how-to-solve-sudoku-faster-than-with-backtracking-1nbk”