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

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

Автор оригинала: Beau Carnes.

Судоку (и его предшественники) играли более ста лет. Когда это сначала вышло, люди должны были действительно решить головоломки, используя только их умы. Теперь у нас есть компьютеры! (Хорошо, так что большинство людей все еще просто используют свои умы …)

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

Peter Norvig разработала элегантную программу, использующую Python, чтобы выиграть судоку, используя распространение и поиск ограничений. Решение Norvig считается классическим и часто называют, когда люди развивают свой собственный код для поиска судоку. После рассмотрения судоку и некоторых стратегий я сломаю код Норвига пошаговый шаг, чтобы вы могли понять, как это работает.

Что такое судоку?

Sudoku – это головоломка размещения числа, и есть несколько разных типов. Эта статья о самом популярном типе.

Цель состоит в том, чтобы заполнить сетку 9×9 с цифрами (1-9), чтобы каждый столбец, строк и каждый из девяти подпунтов 3×3 (также называемых полями) все содержат каждую из цифр от 1 до 9. Пазлы начинаются с некоторых Числа уже на сетке, и вам решать для заполнения других номеров.

На рисунке ниже из игры Sudoku номер, который должен идти на синем выделенном квадрате, не может быть в любом из желтых квадратов, соответствующих столбцу, строке и коробке 3×3.

Как решить судоку

При решении головоломки судоку вы должны постоянно делать две вещи. Первое, что вы должны сделать, это устранить номера от рядов, столбцов и коробок (подпоз 3×3). Второе, что вы должны сделать, это искать одного кандидата.

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

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

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

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

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

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

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

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

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

Программа Peter Norvig, чтобы выиграть Судоку

Питер Норвиг объяснил свой подход к решению судоку и кода, который он использовал в своей статье Решение каждого головоломки судоку Отказ

Некоторые могут находить свое объяснение немного трудно следовать, особенно начинающим. Я сломаю вещи, поэтому легче понять, как работает код Norvig.

В этой статье код Norvig’s Python 2 был обновлен на Python 3. (преобразование Python 3 by Naoki Shibuya .) Я пройду через код несколько строк одновременно, но вы можете увидеть полный код на конец этой статьи. Для некоторых людей может быть полезно посмотреть полный код перед чтением.

Во-первых, мы охватим основную настройку и обозначение. Вот как Norvig описывает основную обозначение, которое он использует в своем коде:

Вот имена квадратов:

 A1 A2 A3| A4 A5 A6| A7 A8 A9
 B1 B2 B3| B4 B5 B6| B7 B8 B9
 C1 C2 C3| C4 C5 C6| C7 C8 C9
---------+---------+---------
 D1 D2 D3| D4 D5 D6| D7 D8 D9
 E1 E2 E3| E4 E5 E6| E7 E8 E9
 F1 F2 F3| F4 F5 F6| F7 F8 F9
---------+---------+---------
 G1 G2 G3| G4 G5 G6| G7 G8 G9
 H1 H2 H3| H4 H5 H6| H7 H8 H9
 I1 I2 I3| I4 I5 I6| I7 I8 I9

Norvig определяет цифры, строки и столбцы как строки:

digits   = '123456789'
rows     = 'ABCDEFGHI'
cols     = digits

Вы заметите, что Cols установлен на равную цифры Отказ Хотя они та же ценность, представляют разные вещи. цифры Переменная представляет цифры, которые идут на квадрате, чтобы решить головоломку. Cols Переменная представляет имена столбцов сетки.

Квадраты также определяются как строки, но строки создаются с функцией:

def cross(A, B):
    "Cross product of elements in A and elements in B."
    return [a+b for a in A for b in B]

squares  = cross(rows, cols)

Возвратная часть Крест Функция ( [A + B для A в A для B в b] ) – это просто модный способ написания этого кода:

squares = []
for a in rows:
    for b in cols:
        squares.append(a+b)

квадраты Переменная сейчас равен список всех квадратных имен.

['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B9', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'E7', 'E8', 'E9', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8', 'G9', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'H7', 'H8', 'H9', 'I1', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I9']

Каждый квадрат в сетке имеет 3 единицы и 20 сверл. Единицы квадрата – это строка, столбец и коробка, в которой оно появляется. Участок квадрата – все остальные квадраты в единицах. Например, вот агрегаты и сверстники для квадрата C2:

Все единицы для каждого квадрата создаются с использованием Крест Функция со следующим кодом:

unitlist = ([cross(rows, c) for c in cols] +
            [cross(r, cols) for r in rows] +
            [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')])

В Python словарь – это коллекция пар ключевых ценностей. Следующие строки Code создают словари, которые используют квадратные имена в качестве клавиш и три единицы или 20 сверстников в качестве значений.

units = dict((s, [u for u in unitlist if s in u]) 
             for s in squares)
peers = dict((s, set(sum(units[s],[]))-set([s]))
             for s in squares)

Теперь 3 единицы «C2» могут быть доступны с единицы ['C2'] и даст следующий результат:

[['A2', 'B2', 'C2', 'D2', 'E2', 'F2', 'G2', 'H2', 'I2'], ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9'], ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']]

Далее нам понадобится два представления полной сетки Sudoku. Текстовый формат имени Сетка будет начальное состояние головоломки.

Другое представление сети также потребуется для внутреннего описания текущего состояния головоломки. Он будет отслеживать все оставшиеся возможные значения для каждого квадрата и быть названным Значения Отказ

Похоже на единицы и сверстники , Значения Будет словарь с квадратами в качестве клавиш. Значение каждого ключа будет строку цифр, которые являются возможными цифрами для квадрата. Если цифра была приведена в головоломке или была выяснена, будет только одна цифра в ключ. Например, если есть сетка, где A1 – 6 и A2 пусты, Значения будет выглядеть как {'A1': '6', 'A2': '123456789', ...} Отказ

Разборные функции сетки и значений сетки

parse_grid Функция (код ниже) преобразует сетку в словарь возможных значений. Сетка данная головоломка Суку. grid_values Функция извлекает важные значения, которые являются цифрами, 0 и Отказ Отказ В Значения Словарь, квадраты – это ключи, а данные цифры в сетке являются значениями.

Для каждого квадрата с заданным значением Назначить Функция используется для назначения значения на квадрату и устранить значение из сверстников. Назначить Функция скоро покрыта. Если что-то пойдет не так, функция возвращает false.

Вот код для parse_grid и grid_values Функции.

def parse_grid(grid):
    """Convert grid to a dict of possible values, {square: digits}, or
    return False if a contradiction is detected."""
    ## To start, every square can be any digit; then assign values from the grid.
    values = dict((s, digits) for s in squares)
    for s,d in grid_values(grid).items():
        if d in digits and not assign(values, s, d):
            return False ## (Fail if we can't assign d to square s.)
    return values

def grid_values(grid):
    "Convert grid into a dict of {square: char} with '0' or '.' for empties."
    chars = [c for c in grid if c in digits or c in '0.']
    assert len(chars) == 81
    return dict(zip(squares, chars))

Распространение ограничений

Начальные значения для квадратов будут либо конкретные цифры (1-9), либо пустое значение. Мы можем применить ограничения на каждый квадрат и устранить значения, которые невозможно.

Norvig использует две стратегии, чтобы помочь определить правильные значения для квадратов (которые соответствуют стратегиям выше):

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

Вот пример второй стратегии: если можно определить, что ни один из A1 – A8 не содержит 9 в качестве возможного значения, то мы можем быть уверены, что A9 имеет значение 9, так как 9 должно происходить где-то в устройстве.

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

Назначить функцию

Назначить (значения, S, D) Функция называется внутри parse_grid функция. Возвращает обновленные значения. Это принимает три аргумента: Значения , S и D Отказ

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

Это называет функцией Устранить (значения, S, D) Чтобы устранить каждое значение из S, кроме d.

Если существует противоречие, например, два квадрата, назначаемые одинаковым номером, функция ELIMINE вернет ложь.

def assign(values, s, d):
    """Eliminate all the other values (except d) from values[s] and propagate.
    Return values, except return False if a contradiction is detected."""
    other_values = values[s].replace(d, '')
    if all(eliminate(values, s, d2) for d2 in other_values):
        return values
    else:
        return False

Устранить функцию

Мы видели, что Назначить Функция вызывает Устранить функция. Функция ELLINITY называется так: Устранить (значения, S, D2) для D2 в orse_values)

Устранить Функция устранит значения, которые мы знаем, не может быть решением, используя два, упомянутых выше стратегий. Первая стратегия заключается в том, что когда есть только одно потенциальное значение для S это значение удаляется из сверстников S Отказ Вторая стратегия заключается в том, что когда есть только одно место, что значение D Можно пойти, это значение удаляется из всех сверстников.

Вот полная функция:

def eliminate(values, s, d):
    """Eliminate d from values[s]; propagate when values or places <= 2.
    Return values, except return False if a contradiction is detected."""
    if d not in values[s]:
        return values ## Already eliminated
    values[s] = values[s].replace(d,'')
    ## (1) If a square s is reduced to one value d2, then eliminate d2 from the peers.
    if len(values[s]) == 0:
        return False ## Contradiction: removed last value
    elif len(values[s]) == 1:
        d2 = values[s]
        if not all(eliminate(values, s2, d2) for s2 in peers[s]):
            return False
    ## (2) If a unit u is reduced to only one place for a value d, then put it there.
    for u in units[s]:
        dplaces = [s for s in u if d in values[s]]
        if len(dplaces) == 0:
            return False ## Contradiction: no place for this value
        elif len(dplaces) == 1:
        # d can only be in one place in unit; assign it there
            if not assign(values, dplaces[0], d):
                return False
    return values

Функция отображения

Дисплей Функция отобразит результат после вызова parse_grid Отказ

def display(values):
    "Display these values as a 2-D grid."
    width = 1+max(len(values[s]) for s in squares)
    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)
    print()

Вот пример того, как будет выглядеть сетка после вызова функции отображения после разбора сетки, которая является твердой головоломкой.

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

Поиск

Есть много способов решить проблему Sukoku, но некоторые намного эффективнее других. Norvig предлагает определенный тип алгоритма поиска.

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

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

Цифры рассматриваются в числовом порядке.

Вот Поиск Функция, наряду с решить Функция, которая анализирует начальную сетку и звонки Поиск Отказ

def solve(grid): return search(parse_grid(grid))

def search(values):
    "Using depth-first search and propagation, try all possible values."
    if values is False:
        return False ## Failed earlier
    if all(len(values[s]) == 1 for s in squares): 
        return values ## Solved!
    ## Chose the unfilled square s with the fewest possibilities
    n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1)
    return some(search(assign(values.copy(), s, d)) 
        for d in values[s])

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

Вот некоторые Функция, используемая для проверки, если попытка удается решить головоломку.

def some(seq):
    "Return some element of seq that is true."
    for e in seq:
        if e: return e
    return False

Этот код теперь решит каждую головоломку судоку. Вы можете просмотреть полный код ниже.

Полный Sudoku Solver Code

def cross(A, B):
    "Cross product of elements in A and elements in B."
    return [a+b for a in A for b in B]

digits   = '123456789'
rows     = 'ABCDEFGHI'
cols     = digits
squares  = cross(rows, cols)
unitlist = ([cross(rows, c) for c in cols] +
            [cross(r, cols) for r in rows] +
            [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')])
units = dict((s, [u for u in unitlist if s in u]) 
             for s in squares)
peers = dict((s, set(sum(units[s],[]))-set([s]))
             for s in squares)

def parse_grid(grid):
    """Convert grid to a dict of possible values, {square: digits}, or
    return False if a contradiction is detected."""
    ## To start, every square can be any digit; then assign values from the grid.
    values = dict((s, digits) for s in squares)
    for s,d in grid_values(grid).items():
        if d in digits and not assign(values, s, d):
            return False ## (Fail if we can't assign d to square s.)
    return values

def grid_values(grid):
    "Convert grid into a dict of {square: char} with '0' or '.' for empties."
    chars = [c for c in grid if c in digits or c in '0.']
    assert len(chars) == 81
    return dict(zip(squares, chars))

def assign(values, s, d):
    """Eliminate all the other values (except d) from values[s] and propagate.
    Return values, except return False if a contradiction is detected."""
    other_values = values[s].replace(d, '')
    if all(eliminate(values, s, d2) for d2 in other_values):
        return values
    else:
        return False

def eliminate(values, s, d):
    """Eliminate d from values[s]; propagate when values or places <= 2.
    Return values, except return False if a contradiction is detected."""
    if d not in values[s]:
        return values ## Already eliminated
    values[s] = values[s].replace(d,'')
    ## (1) If a square s is reduced to one value d2, then eliminate d2 from the peers.
    if len(values[s]) == 0:
        return False ## Contradiction: removed last value
    elif len(values[s]) == 1:
        d2 = values[s]
        if not all(eliminate(values, s2, d2) for s2 in peers[s]):
            return False
    ## (2) If a unit u is reduced to only one place for a value d, then put it there.
    for u in units[s]:
        dplaces = [s for s in u if d in values[s]]
        if len(dplaces) == 0:
            return False ## Contradiction: no place for this value
        elif len(dplaces) == 1:
            # d can only be in one place in unit; assign it there
            if not assign(values, dplaces[0], d):
                return False
    return values

def solve(grid): return search(parse_grid(grid))

def search(values):
    "Using depth-first search and propagation, try all possible values."
    if values is False:
        return False ## Failed earlier
    if all(len(values[s]) == 1 for s in squares): 
        return values ## Solved!
    ## Chose the unfilled square s with the fewest possibilities
    n,s = min((len(values[s]), s) for s in squares if len(values[s]) > 1)
    return some(search(assign(values.copy(), s, d)) 
        for d in values[s])

def some(seq):
    "Return some element of seq that is true."
    for e in seq:
        if e: return e
    return False