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

Как я вернулся к старой проблеме и, наконец, написал алгоритм решающего судоку

Автор оригинала: FreeCodeCapm Team.

Али Сплитл

Эта статья будет частью технической, частью личной истории и частью культурной критики. Если вы только здесь для кода и объяснения, перейдите на Первоначальный подход Заголовок!

Эта история начинается несколько лет назад в классе компьютерных наук колледжа. У меня был непрадиционный путь к написанию кода – я случайно зарегистрировал в классе компьютерных наук во время моего второкурсного года колледжа, потому что у меня был дополнительный кредитный час, и мне было любопытно, что оно было. Я думал, что мы узнаем, как использовать Microsoft Word и Excel – я не имел понятия, какой код был.

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

Как только я вошел в классную комнату, у них был введен код Python Code на IDLE, текстовый редактор, который поставляется с языком Python. Они напечатали код и только что у нас вводили его и запустить его – я был сразу зацепил. В течение этого класса я построил сценарий TIC TAC TAC с графическим интерфейсом для ввода кусочков и богомостным клоном для птиц. Это честно пришло довольно легко для меня, и у меня была куча веселья. Я быстро решил мило в информатике, и я просто хотел написать больше кода.

Следующий семестр я зарегистрировался в структурах данных и курс алгоритма, который был рядом с последовательностью информатики. Класс был преподан в C ++, который, не похожем для меня, должен был быть извлечен в течение лета до класса. Он быстро стал очевидным, что профессора пытались использовать класс для отфильтрования студентов – около 50% зачисленных на день в день, сделали его через семестр. Мы даже изменили классные комнаты из лекционного зала в разрывную комнату. Моя гордость была единственной, что держит меня в классе. Я чувствовал себя полностью потерянным в значительной степени каждый урок. Я провел много ночников, работающих над проектами и изучаю экзамены.

Одной из проблем, в частности, действительно забрала меня – мы должны были построить программу в C ++, которая решила бы любую проблему судоку. Опять же, я провел бесчисленные часы на задании, пытаясь получить код работы. К тому времени, когда проект был предоставлен, мое решение работало на некоторых тестовых случаях, но не все они. Я закончил получать C + в моем задании – один из моих худших сортов во всем колледже.

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

Конечно, забавные вещи случаются в жизни, и я, очевидно, снова начал кодировать, но мне потребовалось много времени, чтобы почувствовать, что был компетентным программистом.

Все, что говорит, несколько лет спустя в моем путешествии по программированию я решил повторить попытку реализации алгоритма решающего судоку, чтобы доказать себе, что я мог бы реализовать его сейчас. Код не идеален, но он будет решать в значительной степени любой головоломки судоку. Давайте пройдемся по алгоритму, а затем реализацию.

Головоломки судоку

Если вы не играли головоломки судоку раньше, они являются номерами головоломки, в которых каждая строка, столбец и квадрат 3×3 в головоломке должны иметь число 1-9, представленное ровно один раз. Есть много подходов к решению этих головоломок, многие из которых могут быть воспроизведены компьютером вместо человека. Обычно, когда мы решаем их с помощью компьютера, мы будем использовать вложенные массивы для представления доски Sudoku, как так:

puzzle = [[5, 3, 0, 0, 7, 0, 0, 0, 0],          [6, 0, 0, 1, 9, 5, 0, 0, 0],          [0, 9, 8, 0, 0, 0, 0, 6, 0],          [8, 0, 0, 0, 6, 0, 0, 0, 3],          [4, 0, 0, 8, 0, 3, 0, 0, 1],          [7, 0, 0, 0, 2, 0, 0, 0, 6],          [0, 6, 0, 0, 0, 0, 2, 8, 0],          [0, 0, 0, 4, 1, 9, 0, 0, 5],          [0, 0, 0, 0, 8, 0, 0, 7, 9]]

При решении нули будут заполнены фактическими числами:

solution = [[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]]

Первоначальный подход

Потому что я не хотел писать полный тестовый люкс с разными головоломками, я использовал вызовы на Кодовые слова проверить себя. Первая проблема, которую я пытался, был это – где все головоломки были «легкими» судокусом, которые можно было решить без более сложного алгоритма.

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

Вот мой (нечищенный) код!

class SudokuSolver:    def __init__(self, puzzle):        self.puzzle = puzzle        self.box_size = 3
    def find_possibilities(self, row_number, column_number):        possibilities = set(range(1, 10))        row = self.get_row(row_number)        column = self.get_column(column_number)        box = self.get_box(row_number, column_number)        for item in row + column + box:            if not isinstance(item, list)and item in possibilities:                possibilities.remove(item)        return possibilities
    def get_row(self, row_number):        return self.puzzle[row_number]
    def get_column(self, column_number):        return [row[column_number] for row in self.puzzle]
    def get_box(self, row_number, column_number):        start_y = column_number // 3 * 3        start_x = row_number // 3 * 3        if start_x < 0:            start_x = 0        if start_y < 0:            start_y = 0        box = []        for i in range(start_x, self.box_size + start_x):            box.extend(self.puzzle[i][start_y:start_y+self.box_size])        return box
    def find_spot(self):        unsolved = True        while unsolved:            unsolved = False            for row_number, row in enumerate(self.puzzle):                for column_number, item in enumerate(row):                    if item == 0:                        unsolved = True                        possibilities = self.find_possibilities(                            row_number, column_number)                        if len(possibilities) == 1:                            self.puzzle[row_number][column_number] = list(possibilities)[                                0]        return self.puzzle
def sudoku(puzzle):    sudoku = SudokuSolver(puzzle)    return sudoku.find_spot()

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

Алгоритм

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

Первое, что я сделал, было продолжать свой «легкий» подход Sudoku Solver нахождения возможных значений для каждого квадрата на основе того, какие значения уже были в строке, столбце и коробке площади. Я сохранил все эти значения в списке, чтобы я мог бы быстро обратиться к ним во время возвращения или нахождения, какое значение использовать в этом квадрате.

Далее мне нужно было реализовать прямые движущиеся и возвратные вещи в каждом пробеле. Я положил маркеры на каждое не данное пространство (поэтому те, которые были нулями, когда игра началась) так, чтобы эти пробелы были бы включены в обратный путь, и данные пятна не были бы. Затем я итаграфила через эти не решенные пятна. Я бы поставил первый элемент возможного списка значений в этом месте, а затем перейти к следующему неконфунту. Затем я бы поставил первую возможное значение этого места на его месте. Если он противоречит значению предыдущего слота, я бы перешел ко второму элементу в списке возможных значений, а затем перейти к следующему слоту.

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

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

После того, как конец головоломки был достигнут с правильными значениями на каждом квадрате, головоломка была решена!

Мой подход

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

class Cell:    """One individual cell on the Sudoku board"""
    def __init__(self, column_number, row_number, number, game):        # Whether or not to include the cell in the backtracking        self.solved = True if number > 0 else False        self.number = number  # the current value of the cell        # Which numbers the cell could potentially be        self.possibilities = set(range(1, 10)) if not self.solved else []        self.row = row_number  # the index of the row the cell is in        self.column = column_number  # the index of the column the cell is in        self.current_index = 0  # the index of the current possibility        self.game = game  # the sudoku game the cell belongs to        if not self.solved:  # runs the possibility checker            self.find_possibilities()
    def check_area(self, area):        """Checks to see if the cell's current value is a valid sudoku move"""        values = [item for item in area if item != 0]        return len(values) == len(set(values))
    def set_number(self):        """changes the number attribute and also changes the cell's value in the larger puzzle"""        if not self.solved:            self.number = self.possibilities[self.current_index]            self.game.puzzle[self.row][self.column] = self.possibilities[self.current_index]
    def handle_one_possibility(self):        """If the cell only has one possibility, set the cell to that value and mark it as solved"""        if len(self.possibilities) == 1:            self.solved = True            self.set_number()
    def find_possibilities(self):        """filter the possible values for the cell"""        for item in self.game.get_row(self.row) + self.game.get_column(self.column) + self.game.get_box(self.row, self.column):            if not isinstance(item, list) and item in self.possibilities:                self.possibilities.remove(item)        self.possibilities = list(self.possibilities)        self.handle_one_possibility()
    def is_valid(self):        """checks to see if the current number is valid in its row, column, and box"""        for unit in [self.game.get_row(self.row), self.game.get_column(self.column), self.game.get_box(self.row, self.column)]:            if not self.check_area(unit):                return False        return True
    def increment_value(self):        """move number to the next possibility while the current number is invalid and there are possibilities left"""        while not self.is_valid() and self.current_index < len(self.possibilities) - 1:            self.current_index += 1            self.set_number()
class SudokuSolver:    """contains logic for solving a sudoku puzzle -- even very difficult ones using a backtracking algorithm"""
    def __init__(self, puzzle):        self.puzzle = puzzle  # the 2d list of spots on the board        self.solve_puzzle = []  # 1d list of the Cell objects        # the size of the boxes within the puzzle -- 3 for a typical puzzle        self.box_size = int(len(self.puzzle) ** .5)        self.backtrack_coord = 0  # what index the backtracking is currently at
    def get_row(self, row_number):        """Get the full row from the puzzle based on the row index"""        return self.puzzle[row_number]
    def get_column(self, column_number):        """Get the full column"""        return [row[column_number] for row in self.puzzle]
    def find_box_start(self, coordinate):        """Get the start coordinate for the small sudoku box"""        return coordinate // self.box_size * self.box_size
    def get_box_coordinates(self, row_number, column_number):        """Get the numbers of the small sudoku box"""        return self.find_box_start(column_number), self.find_box_start(row_number)
    def get_box(self, row_number, column_number):        """Get the small sudoku box for an x and y coordinate"""        start_y, start_x = self.get_box_coordinates(row_number, column_number)        box = []        for i in range(start_x, self.box_size + start_x):            box.extend(self.puzzle[i][start_y:start_y+self.box_size])        return box
    def initialize_board(self):        """create the Cells for each item in the puzzle and get its possibilities"""        for row_number, row in enumerate(self.puzzle):            for column_number, item in enumerate(row):                self.solve_puzzle.append(                    Cell(column_number, row_number, item, self))
    def move_forward(self):        """Move forwards to the next cell"""        while self.backtrack_coord < len(self.solve_puzzle) - 1 and self.solve_puzzle[self.backtrack_coord].solved:            self.backtrack_coord += 1
    def backtrack(self):        """Move forwards to the next cell"""        self.backtrack_coord -= 1        while self.solve_puzzle[self.backtrack_coord].solved:            self.backtrack_coord -= 1
    def set_cell(self):        """Set the current cell to work on"""        cell = self.solve_puzzle[self.backtrack_coord]        cell.set_number()        return cell
    def reset_cell(self, cell):        """set a cell back to zero"""        cell.current_index = 0        cell.number = 0        self.puzzle[cell.row][cell.column] = 0
    def decrement_cell(self, cell):        """runs the backtracking algorithm"""        while cell.current_index == len(cell.possibilities) - 1:            self.reset_cell(cell)            self.backtrack()            cell = self.solve_puzzle[self.backtrack_coord]        cell.current_index += 1
    def change_cells(self, cell):        """move forwards or backwards based on the validity of a cell"""        if cell.is_valid():            self.backtrack_coord += 1        else:            self.decrement_cell(cell)
    def solve(self):        """run the other functions necessary for solving the sudoku puzzle"""        self.move_forward()        cell = self.set_cell()        cell.increment_value()        self.change_cells(cell)
    def run_solve(self):        """runs the solver until we are at the end of the puzzle"""        while self.backtrack_coord <= len(self.solve_puzzle) - 1:            self.solve()
def solve(puzzle):    solver = SudokuSolver(puzzle)    solver.initialize_board()    solver.run_solve()    return solver.puzzle

Жесткий соревер судоку

Мои вынос

Иногда это просто требует времени и практики. Sudoku Solver, который я провел бесчисленные часы колледжа, занял меня менее чем через несколько лет спустя.

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

Частично, это определенно способствует успеху кодирования BootCamps, которые начинаются с основы и преподают менее концептуальные навыки веб-разработки, а не тяжелые алгоритмы.

Теперь я могу написать алгоритм решающего судоку, но я не думаю, что это необходимый навык для разработчиков – я все еще стал успешным инженером программного обеспечения вскоре после того времени, когда я не мог реализовать Sudoku Solver.

Я думаю, что некоторые информационные науки могут быть очень полезными, даже для новых разработчиков. Например, концепции нотации Big-O могут быть действительно полезными для принятия решения между подходами. То, что говорится, что большинство структур данных и алгоритмов не используются в повседневной основе, поэтому почему они основы для интервью и классов компьютерных наук вместо более важных, используемых каждый день?

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

Если вам понравилась эта статья, пожалуйста подписываться На мой еженедельный бюллетень, где вы получите мои любимые ссылки с недели и моих последних статей.