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

Решение пазла с использованием рекурсивного возврата в Python

Мой партнер и я очень любим разбирать головоломки как наш момент связи. В этот Хэллоуин мы решили … помеченный питоном, рекурсией, головоломками.

Мой партнер и я очень любим разбирать головоломки как наш момент связи. Этот Хэллоуин, мы решили решить Загадываемости в Интернете загадывает загадку от Enchambered Анкет Из всех головоломок, которые мы пробовали той ночью, больше всего выделялись для меня, была загадкой № 4. Если вы не пробовали это, я настоятельно рекомендую вам попробовать – Головоломка 4 загадочная коробка Анкет

У платы есть числа от 1 до 9, которые загораются, и четыре красные кнопки, которые делят доску на четыре квадранта.

Сдвиг числа

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

Цель

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

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

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

Вундеркиды для гиков определяют отступать в качестве:

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

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

Между тем, рекурсия определяется как:

Процесс, в котором функция прямо или косвенно вызывает себя, называется рекурсией, а соответствующая функция называется рекурсивной функцией.

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

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

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

Представляя головоломку

Примечание. Следующие фрагменты кода, которые вы увидите здесь, реализованы с помощью Python.

Доска

Мы можем начать с представления платы с использованием двухмерных массивов:

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

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

Квадранты

Теперь давайте разделим нашу доску на 4 квадранта:

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

С точки зрения индексов массива, группировки:

# Quadrant 1 -> [0,0], [0,1], [1,1], [1,0]
# Quadrant 2 -> [0,1], [0,2], [1,2], [1,1]
# Quadrant 3 -> [1,0], [1,1], [2,1], [2,0]
# Quadrant 4 -> [1,1], [1,2], [2,2], [2,1]

Ротации

Позвольте нам представить операцию нажатия кнопки. Начнем с определения того, как сдвинуться в квадранте 1:

from copy import deepcopy

# Operations
def shiftQuadrant1(originalBoard):
    board = deepcopy(originalBoard)
    board[0][0], board[0][1], board[1][1], board[1][0] = board[1][0], board[0][0], board[0][1], board[1][1]
    return board

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

Для справки, вот Разница между мелкой копией против глубокой копии :

Неглубокая копия конструирует новый составной объект, а затем (в той степени, возможную) вводит ссылки на его объекты, найденные в оригинале. Глубокая копия создает новый составной объект, а затем, рекурсивно, вставляет в него копии объектов, найденных в оригинале.

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

# To switch values between a and in b in Python, we do:
# a, b = b, a
# In our case, we can do the clockwise shift of numbers by:
# board[0][0], board[0][1], board[1][1], board[1][0] = board[1][0], board[0][0], board[0][1], board[1][1]

Применяя ту же идею к трем другим кнопкам, у нас будут следующие функции:

def shiftQuadrant1(originalBoard):
    board = deepcopy(originalBoard)
    board[0][0], board[0][1], board[1][1], board[1][0] = board[1][0], board[0][0], board[0][1], board[1][1]
    return board

def shiftQuadrant2(originalBoard):
    board = deepcopy(originalBoard)
    board[0][1], board[0][2], board[1][2], board[1][1] = board[1][1], board[0][1], board[0][2], board[1][2]
    return board

def shiftQuadrant3(originalBoard):
    board = deepcopy(originalBoard)
    board[1][0], board[1][1], board[2][1], board[2][0] = board[2][0], board[1][0], board[1][1], board[2][1]
    return board

def shiftQuadrant4(originalBoard):
    board = deepcopy(originalBoard)
    board[1][1], board[1][2], board[2][2], board[2][1] = board[2][1], board[1][1], board[1][2], board[2][2]
    return board

Сравнения доски

Потрясающий! Теперь давайте настроим другие функции, которые помогут нам с сравнением:

def isSolved(board):
    return areBoardsEqual(board, solved_board)

def areBoardsEqual(board1, board2):
    return board1 == board2

Решатель: Рекурсивный возврат

Теперь давайте поговорим о фактической реализации рекурсивного возврата.

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

Если у вас был опыт реализации алгоритма поиска раньше, то вы, вероятно, заметили, что мы фактически реализуем алгоритм поиска в глубине.

Решатель: псевдокод

Наша рекурсивная функция будет иметь следующие шаги:

  1. Базовый случай: текущая доска в решении? Если да, то прекратите.
  2. Позвоните в Решатель Снова функционируйте, но на этот раз исследуйте новые состояния, достигнутые после перехода на Q1, Q2, Q3 и Q4 из текущего состояния.

Попытка № 1

Поместив это в код, у нас будет:

def solve(board, stack):
    if(isSolved(board)):
        print("Solution: ", stack)

    solve(shiftQuadrant1(board), stack + ['Q1'])
    solve(shiftQuadrant2(board), stack + ['Q2'])
    solve(shiftQuadrant3(board), stack + ['Q3'])
    solve(shiftQuadrant4(board), stack + ['Q4'])

# Calling the function with initial value setup:
solve(initial_board, [])

Выглядит аккуратно, верно? Не совсем. На самом деле есть проблема с нашим алгоритмом.

Один Наш алгоритм застрянет в одном из штатов, так как мы не отслеживаем, если мы раньше изучали государство.

Например, алгоритм поиска попытается сделать сдвиг с [Q1, Q1, Q1, Q1, …], который возвращает его в предыдущее состояние.

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

Попытка № 2

Наша новая и улучшенная функция будет выглядеть так:

def isBoardInPastBoardStates(board, pastBoardStates):
    for state in pastBoardStates:
        if(areBoardsEqual(board, state)):
            return True
    return False

def solve(board, stack, pastBoardStates):
    if isBoardInPastBoardStates(board, pastBoardStates):
        return

    if(isSolved(board)):
        print("Solution: ", stack)

    solve(shiftQuadrant1(board), stack + ['Q1'], pastBoardStates + [board])
    solve(shiftQuadrant2(board), stack + ['Q2'], pastBoardStates + [board])
    solve(shiftQuadrant3(board), stack + ['Q3'], pastBoardStates + [board])
    solve(shiftQuadrant4(board), stack + ['Q4'], pastBoardStates + [board])

Обратите внимание, как мы добавили isboardinpastboardstates Функция, чтобы убедиться, что мы перестали рассматривать состояния, если они уже были изучены.

Однако наша функция еще не завершена.

Если вы запустите это, алгоритм попробует следующую последовательность смещения: [Q1, Q1, Q1, Q2, Q1, Q1, Q1, Q2, Q1, Q1, Q1, Q2 …] и так далее. Он будет изучать глубже в этой ветви, потенциально пропустив другие жизнеспособные решения в других ветвях. Какой наш следующий шаг тогда?

Итеративный углубляющий поиск по глубине глубины

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

Хотя я помню этот алгоритм поиска из моих дней Compsci, я не осознавал, что использовал его, когда писал решение, пока не посмотрел его в Интернете.

IDDFS сочетает в себе пространственную эффективность поиска и быстрый поиск поиска в ширине (для узлов ближе к корне).

Как работает IDDFS? IDDFS вызывает DFS для разных глубин, начиная с начального значения. В каждом вызове DFS ограничивается тем, что выходит за рамки глубины. Так что в основном мы делаем DFS в моде BFS.

Этот алгоритм помогает нам найти лучшие решения для кандидатов. Мы можем начать с установки максимальной глубины равным 1, 2, 3 и т. Д., Пока мы не найдем наименьшую глубину, которая имеет решение.

Ради простоты, я твердо искал глубину-ограничения по имени Maxmoves и заметил, что это углубленное 11, где мы впервые нашли решение.

Окончательная попытка: IDDFS

def solve(board, stack, pastBoardStates, maxMoves):
    if(len(stack) >= maxMoves):
        return

    if(isSolved(board)):
        print("Solution: ", stack)

    if isBoardInPastBoardStates(board, pastBoardStates):
        return

    solve(shiftQuadrant1(board), stack + ['Q1'], pastBoardStates + [board], maxMoves)
    solve(shiftQuadrant2(board), stack + ['Q2'], pastBoardStates + [board], maxMoves)
    solve(shiftQuadrant3(board), stack + ['Q3'], pastBoardStates + [board], maxMoves)
    solve(shiftQuadrant4(board), stack + ['Q4'], pastBoardStates + [board], maxMoves)

maxMoves = 11
solve(initial_board, [], [], maxMoves)

Maxmoves представляет предел нашей глубины.

Сделать все это вместе

from copy import deepcopy

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

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

def isSolved(board):
    return areBoardsEqual(board, solved_board)

def areBoardsEqual(board1, board2):
    return board1 == board2

# Operations
def shiftQuadrant1(originalBoard):
    board = deepcopy(originalBoard)
    board[0][0], board[0][1], board[1][1], board[1][0] = board[1][0], board[0][0], board[0][1], board[1][1]
    return board

def shiftQuadrant2(originalBoard):
    board = deepcopy(originalBoard)
    board[0][1], board[0][2], board[1][2], board[1][1] = board[1][1], board[0][1], board[0][2], board[1][2]
    return board

def shiftQuadrant3(originalBoard):
    board = deepcopy(originalBoard)
    board[1][0], board[1][1], board[2][1], board[2][0] = board[2][0], board[1][0], board[1][1], board[2][1]
    return board

def shiftQuadrant4(originalBoard):
    board = deepcopy(originalBoard)
    board[1][1], board[1][2], board[2][2], board[2][1] = board[2][1], board[1][1], board[1][2], board[2][2]
    return board

def isBoardInPastBoardStates(board, pastBoardStates):
    for state in pastBoardStates:
        if(areBoardsEqual(board, state)):
            return True
    return False

attempt = 0
def solve(board, stack, pastBoardStates, maxMoves):
    global attempt
    attempt = attempt + 1

    if(len(stack) >= maxMoves):
        return

    if(isSolved(board)):
        print("Attempt: ", attempt, "Solution: ", stack)

    if isBoardInPastBoardStates(board, pastBoardStates):
        return

    solve(shiftQuadrant1(board), stack + ['Q1'], pastBoardStates + [board], maxMoves)
    solve(shiftQuadrant2(board), stack + ['Q2'], pastBoardStates + [board], maxMoves)
    solve(shiftQuadrant3(board), stack + ['Q3'], pastBoardStates + [board], maxMoves)
    solve(shiftQuadrant4(board), stack + ['Q4'], pastBoardStates + [board], maxMoves)

maxMoves = 11
solve(initial_board, [], [], maxMoves)

Запустив приведенный выше код, мы прибываем со следующими решениями:

Attempt # 2472679 Solution:  ['Q2', 'Q4', 'Q3', 'Q4', 'Q1', 'Q3', 'Q1', 'Q3', 'Q2', 'Q3']
Attempt # 3452929 Solution:  ['Q3', 'Q3', 'Q4', 'Q1', 'Q2', 'Q2', 'Q2', 'Q3', 'Q3', 'Q1']
Attempt # 3467708 Solution:  ['Q3', 'Q3', 'Q4', 'Q2', 'Q1', 'Q1', 'Q3', 'Q3', 'Q1', 'Q2']
Attempt # 3621688 Solution:  ['Q3', 'Q4', 'Q2', 'Q1', 'Q3', 'Q1', 'Q3', 'Q2', 'Q1', 'Q3']
Attempt # 4340258 Solution:  ['Q4', 'Q2', 'Q3', 'Q1', 'Q1', 'Q2', 'Q1', 'Q3', 'Q1', 'Q3']

Решение головоломки

Вот GIF, который решает головоломку после решений, найденных нашим кодом:

Оптимизация: Параллелизм

Наш код работал почти 40 секунд, чтобы найти 5 решений. Одним из способов улучшения своей скорости является использование параллелизма, поскольку он позволит нам исследовать несколько состояний параллельно, и объединить все решения, обнаруженные в конце.

Если вы хотите получить доступ к коду RAW, вы можете найти его в GitHub Анкет

Спасибо за чтение!

Если вы знаете, как решить эту проблему логически, не стесняйтесь оставить комментарий. Счастливого решения!

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

Плавник. 🐟

Оригинал: “https://dev.to/tigertopher/solving-the-puzzle-box-using-recursive-backtracking-3pfg”