Учитывая набор стеков NXP, где N - это количество стеков, а P - емкость стеков, как я могу рассчитать минимальное количество перестановок, необходимое для перемещения из некоторого узла в местоположении A в какое-то произвольное местоположение B? Я разрабатываю игру, и конечной целью является сортировка всех стеков так, чтобы они были одного цвета.
# Let "-" represent blank spaces, and assume the stacks are
stacks = [
['R', 'R', 'R', 'R'],
['Y', 'Y', 'Y', 'Y'],
['G', 'G', 'G', 'G'],
['-', '-', '-', 'B'],
['-', 'B', 'B', 'B']
]
Если я хочу, чтобы вставить "B" в stacks[1][1]
такой, что stacks[1] = ["-", "B", "Y", "Y"]
. Как я могу определить минимальное количество ходов, необходимое для этого?
Я смотрел на несколько подходов, я попробовал генетические алгоритмы, которые генерируют все возможные ходы из состояния, оценивают их, а затем продолжают идти по лучшим путям скоринга, я также пытался запустить алгоритм Джикстры для поиска пути по проблеме , Это кажется невероятно простым, но я не могу придумать, как заставить его работать за исключением экспоненциального времени. Есть ли алгоритм, который мне не хватает, который применим здесь?
редактировать
Я написал эту функцию, чтобы вычислить минимальное количество необходимых ходов: stacks: список символов, представляющих фигуры в стеке, stacks [0] [0] является вершиной стека [0] stack_ind: индекс стек, к которому будет добавлен кусок. needs_piece: кусок, который должен быть добавлен в стек needs_index: индекс, в котором этот фрагмент должен быть расположен.
def calculate_min_moves(stacks, stack_ind, needs_piece, needs_index):
# Minimum moves needed to empty the stack that will receive the piece so that it can hold the piece
num_removals = 0
for s in stacks[stack_ind][:needs_index+1]:
if item != "-":
num_removals += 1
min_to_unlock = 1000
unlock_from = -1
for i, stack in enumerate(stacks):
if i != stack_ind:
for k, piece in enumerate(stack):
if piece == needs_piece:
if k < min_to_unlock:
min_to_unlock = k
unlock_from = i
num_free_spaces = 0
free_space_map = {}
for i, stack in enumerate(stacks):
if i != stack_ind and i != unlock_from:
c = stack.count("-")
num_free_spaces += c
free_space_map[i] = c
if num_removals + min_to_unlock <= num_free_spaces:
print("No shuffling needed, there's enough free space to move all the extra nodes out of the way")
else:
# HERE
print("case 2, things need shuffled")
Изменить: тестовые случаи на стеки:
stacks = [
['R', 'R', 'R', 'R'],
['Y', 'Y', 'Y', 'Y'],
['G', 'G', 'G', 'G'],
['-', '-', '-', 'B'],
['-', 'B', 'B', 'B']
]
Case 1: stacks[4][1] should be 'G'
Move 'B' from stacks[4][1] to stacks[3][2]
Move 'G' from stacks[2][0] to stacks[4][1]
num_removals = 0 # 'G' is directly accessible as the top of stack 2
min_to_unlock = 1 # stack 4 has 1 piece that needs removed
free_spaces = 3 # stack 3 has free spaces and no pieces need moved to or from it
moves = [[4, 3], [2, 4]]
min_moves = 2
# This is easy to calculate
Case 2: stacks[0][3] should be 'B'
Move 'B' from stacks[3][3] to stack[4][0]
Move 'R' from stacks[0][0] to stacks[3][3]
Move 'R' from stacks[0][1] to stacks[3][2]
Move 'R' from stacks[0][2] to stacks[3][1]
Move 'R' from stacks[0][3] to stacks[3][0]
Move 'B' from stacks[4][0] to stacks[0][3]
num_removals = 0 # 'B' is directly accessible
min_to_unlock = 4 # stack 0 has 4 pieces that need removed
free_spaces = 3 # If stack 3 and 4 were switched this would be 1
moves = [[3, 4], [0, 3], [0, 3], [0, 3], [0, 3], [4, 0]]
min_moves = 6
#This is hard to calculate
Реальная реализация кода не та сложная часть, она определяет, как реализовать алгоритм, который решает проблему, с которой я борюсь.
По запросу @ YonIif я создал суть проблемы.
При запуске он генерирует случайный массив стеков и выбирает случайный фрагмент, который необходимо вставить в случайный стек в случайном месте.
Запуск его выводит что-то такого формата на консоль.
All Stacks: [['-', '-', 'O', 'Y'], ['-', 'P', 'P', 'O'], ['-', 'P', 'O', 'Y'], ['Y', 'Y', 'O', 'P']]
Stack 0 is currently ['-', '-', 'O', 'Y']
Stack 0 should be ['-', '-', '-', 'P']
Обновление статуса
Я очень решительно , чтобы решить эту проблему , так или иначе .
Имейте в виду, что есть способы минимизировать количество дел, таких как @Hans Olsson, упомянутые в комментариях. Мой самый последний подход к этой проблеме - разработать набор правил, аналогичных упомянутым, и использовать их в алгоритме поколений.
Правила, такие как:
Никогда не меняй ход. Перейти от 1-> 0, затем 0-> 1 (не имеет смысла)
Никогда не перемещайте фигуру дважды подряд. Никогда не двигайтесь от 0 -> 1, затем 1 -> 3
При некотором перемещении из стеков [X] в стеки [Y], затем через некоторое количество ходов, затем из стеков [Y] в стеки [Z], если стеки [Z] находятся в том же состоянии, в котором они находились при перемещении от стеков [X] к стекам [Y], ход можно было исключить, если перейти непосредственно из стеков [X] к стекам [Z]
В настоящее время я подхожу к этой проблеме, пытаясь создать достаточно правил, чтобы минимизировать количество «допустимых» ходов, достаточно, чтобы можно было рассчитать ответ с использованием алгоритма генерации. Если кто-нибудь может придумать дополнительные правила, мне было бы интересно услышать их в комментариях.
Обновить
Благодаря ответу @RootTwo у меня был небольшой прорыв, который я опишу здесь.
На прорыв
Определите высоту цели как глубину, которую необходимо поместить в целевую стопку.
Всякий раз, когда какая-либо часть ворот помещается в index <= stack_height - высота цели, всегда будет кратчайший путь к победе с помощью метода clear_path ().
Let S represent some solid Piece.
IE
Stacks = [ [R, R, G], [G, G, R], [-, -, -] ]
Goal = Stacks[0][2] = R
Goal Height = 2.
Stack Height - Goal Height = 0
При некотором стеке stack[0] = R
игра выиграна.
GOAL
[ [ (S | -), (S | -), (S | -) ], [R, S, S], [(S | - ), (S | -), (S | -)] ]
Поскольку известно, что они всегда имеют по крайней мере пустые места stack_height, наихудший возможный случай будет:
[ [ S, S, !Goal ], [R, S, S], [-, -, -]
Так как мы знаем, что цель не может быть в пункте назначения цели, или игра выиграна. В этом случае минимальное количество необходимых ходов будет:
(0, 2), (0, 2), (0, 2), (1, 0)
Stacks = [ [R, G, G], [-, R, R], [-, -, G] ]
Goal = Stack[0][1] = R
Stack Height - Goal Height = 1
При некотором стеке stack[1] = R
игра выиграна.
GOAL
[ [ (S | -), (S | -), S], [ (S | -), R, S], [(S | -), (S | -), (S | -)]
Мы знаем, что доступно как минимум 3 пробела, поэтому наихудшим из возможных случаев будет:
[ [ S, !Goal, S], [S, R, S], [ -, -, - ]
В этом случае минимальное количество ходов было бы ходами:
(1, 2), (0, 2), (0, 2), (1, 0)
Это будет справедливо для всех случаев.
Таким образом, проблема была сведена к задаче нахождения минимального количества ходов, необходимого для размещения части ворот на высоте цели или выше.
Это разбивает проблему на ряд подзадач:
Когда у стека назначения есть свой доступный кусок! = Цель, определяющий, есть ли допустимое место для этого куска, или этот кусок должен оставаться там, пока другой кусок поменялся местами.
Когда у целевого стека есть доступная часть == цель, определяется, может ли она быть удалена и помещена на требуемую высоту цели, или должна ли часть оставаться, пока другой обменивается.
Когда в двух вышеупомянутых случаях требуется замена другой части, определяют, какие части следует поменять местами для увеличения, чтобы дать возможность целевому элементу достичь высоты цели.
В стеке назначения всегда должны сначала оцениваться случаи.
IE
stacks = [ [-, R, G], [-, R, G], [-, R, G] ]
Goal = stacks[0][1] = G
Проверка стека целей сначала приводит к:
(0, 1), (0, 2), (1, 0), (2, 0) = 4 Moves
Игнорирование стека голов:
(1, 0), (1, 2), (0, 1), (0, 1), (2, 0) = 5 Moves
Ответы:
Я придумал два варианта, но ни один из них не смог решить проблему 2 своевременно. Первый вариант - использование A * с мерой расстояния строки в качестве h (n), второй вариант - IDA *. Я проверил много мер сходства строк, я использовал кузнеца-водителя на моем подходе. Я изменил ваши обозначения, чтобы решить проблему быстрее. Я добавил числа в конце каждой цифры, чтобы проверить, сдвинулся ли кусок дважды.
Вот случаи, на которых я проверял:
Вот код A *:
Вот код IDA *:
Применение:
источник
В комментариях вы сказали, что есть N стеков с емкостью P, и всегда есть P пустых мест. Если это так, кажется, что этот алгоритм будет работать в
else
предложении в вашем коде (т.е. когдаnum_removals + min_to_unlock > num_free_spaces
):источник
Хотя я не нашел времени, чтобы доказать это математически, я все равно решил опубликовать это; Надеюсь, поможет. Подход состоит в том, чтобы определить параметр p, который уменьшается при хороших ходах и достигает нуля точно после окончания игры. В программе рассматриваются только хорошие ходы или нейтральные ходы (которые оставляют p без изменений), и забывают о плохих ходах (которые увеличивают p).
Так что же такое р? Для каждого столбца определите p как число блоков, которые еще нужно удалить, прежде чем все цвета в этом столбце станут желаемым цветом. Итак, предположим, что мы хотим, чтобы красные блоки оказались в крайнем левом столбце (я вернусь к этому позже), и предположим, что есть один красный блок внизу, затем желтый желтый сверху, еще один блок сверху это, а затем пустое место. Тогда p = 2 для этого столбца (два блока, которые нужно удалить, прежде чем все станут красными). Рассчитайте p для всех столбцов. Для столбца, который должен оказаться пустым, p равно количеству блоков в нем (все они должны идти). P для текущего состояния - это сумма всех p для всех столбцов.
Когда p = 0, все столбцы имеют одинаковый цвет, а один столбец пуст, поэтому игра окончена.
Выбирая шаги, которые уменьшают p (или, по крайней мере, не увеличивают p), мы движемся в правильном направлении, это, на мой взгляд, принципиальное отличие от алгоритмов кратчайшего пути: Дейкстра не знал, двигался ли он в правильном направлении с каждым вершина, которую он расследовал.
Так как же определить, где должен заканчиваться каждый цвет? В основном, путем определения р для каждой возможности. Например, начните с красного / желтого / зеленого / пустого, вычислите p, затем перейдите к красному / желтому / пустому / зеленому, вычислите p и т. Д. Возьмите начальную позицию с самым низким p. Это занимает п! расчеты. Для n = 8 это 40320, что выполнимо. Плохая новость заключается в том, что вам придется проверять все стартовые позиции с равным самым низким p. Хорошей новостью является то, что вы можете забыть все остальное.
Здесь есть две математические неопределенности. Один: возможно ли, что есть более короткий путь, который использует плохой ход? Кажется маловероятным, я не нашел контрпример, но я также не нашел доказательства. Второе: возможно ли, что при старте с неоптимальной стартовой позиции (т. Е. Не с самого низкого p) путь будет короче, чем при всех оптимальных стартовых позициях. Опять же: нет контрпримеров, но нет и доказательств.
Некоторые предложения по реализации. Отслеживание p во время выполнения для каждого столбца не сложно, но, конечно, это нужно сделать. Другим параметром, который следует сохранить для каждого столбца, является количество открытых пятен. Если 0, то эти столбцы могут на мгновение не принимать какие-либо блоки, поэтому могут быть исключены из цикла. Когда p = 0 для столбца, он не подходит для pop. Для каждого возможного хлопка, проверьте, есть ли хороший ход, то есть тот, который уменьшает общее p. Если их несколько, изучите все. Если их нет, рассмотрим все нейтральные ходы.
Все это должно значительно сократить время вычислений.
источник