Как найти минимальное количество ходов для перемещения предмета в позицию в стеке? - PullRequest
12 голосов
/ 24 марта 2020

Stacks

Учитывая набор стеков 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"]. Как я могу определить минимальное количество ходов, необходимое для этого?

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

Редактировать

Я написал эту функцию для вычисления минимального необходимого количества ходов: 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 я создал для задачи gist .

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

При запуске он печатает что-то из этого формата на консоль.

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']

Обновление статуса

I Я полон решимости решить эту проблему каким-то образом .

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

Правила, такие как:

Никогда не изменяться движение. Go из 1-> 0, затем 0-> 1 (не имеет смысла)

Никогда не перемещайте фигуру дважды подряд. Никогда не двигайтесь от 0 -> 1, затем 1 -> 3

Учитывая некоторый переход от стеков [X] к стекам [Y], затем некоторое количество ходов, затем переход от стеков [Y] к стекам [Z] ], если стеки [Z] находятся в том же состоянии, в котором они находились при переходе от стеков [X] к стекам [Y], перемещение можно было бы исключить, если перейти из стеков [X] непосредственно в стеки [Z]

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

Обновление

Благодаря ответу @RootTwo у меня был небольшой прорыв, который я здесь опишу.

На прорыв

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

Всякий раз, когда какая-либо цель попадает в индекс <= stack_height - высота цели, всегда будет кратчайший путь к победе с помощью метода clear_path (). </p>

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)

Это будет выполняться для всех случаев.

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

Это разбивает задачу на ряд подзадач:

  1. Когда в стеке назначения есть доступная фигура! = Цель, определяющая, есть ли для этой фигуры правильное место, или же эта фигура должна оставаться там во время замены другой фигуры.

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

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

Стек назначения должен всегда сначала оцените свои случаи.

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

Ответы [ 3 ]

1 голос
/ 03 апреля 2020

Я предложил два варианта, но ни один из них не смог решить проблему 2 своевременно. Первый вариант - использование A * с мерой расстояния строки в качестве h (n), второй вариант - IDA *. Я проверил много мер сходства строк, я использовал кузнеца-водителя на моем подходе. Я изменил вашу нотацию, чтобы решить проблему быстрее. Я добавил числа в конце каждого ди git, чтобы проверить, сдвинулся ли кусок дважды.

Вот примеры, которые я проверял:

start = [
 ['R1', 'R2', 'R3', 'R4'], 
 ['Y1', 'Y2', 'Y3', 'Y4'], 
 ['G1', 'G2', 'G3', 'G4'], 
 ['B1'], 
 ['B2', 'B3', 'B4']
]

case_easy = [
 ['R', 'R', 'R', 'R'], 
 ['Y', 'Y', 'Y', 'Y'], 
 ['G', 'G', 'G'], 
 ['B', 'B'], 
 ['B', 'B', 'G']
]


case_medium = [
 ['R', 'R', 'R', 'R'], 
 ['Y', 'Y', 'Y', 'B'], 
 ['G', 'G', 'G'], 
 ['B'],
 ['B', 'B', 'G', 'Y']
]

case_medium2 = [
 ['R', 'R', 'R' ], 
 ['Y', 'Y', 'Y', 'B'], 
 ['G', 'G' ], 
 ['B', 'R', 'G'],
 ['B', 'B', 'G', 'Y']
]

case_hard = [
 ['B'], 
 ['Y', 'Y', 'Y', 'Y'], 
 ['G', 'G', 'G', 'G'], 
 ['R','R','R', 'R'], 
 ['B','B', 'B']
]

Вот код A *:

from copy import deepcopy
from heapq import *
import time, sys
import textdistance
import os

def a_star(b, goal, h):
    print("A*")
    start_time = time.time()
    heap = [(-1, b)]
    bib = {}
    bib[b.stringify()] = b

    while len(heap) > 0:
        node = heappop(heap)[1]
        if node == goal:
            print("Number of explored states: {}".format(len(bib)))
            elapsed_time = time.time() - start_time
            print("Execution time {}".format(elapsed_time))
            return rebuild_path(node)

        valid_moves = node.get_valid_moves()
        children = node.get_children(valid_moves)
        for m in children:
          key = m.stringify()
          if key not in bib.keys():
            h_n = h(key, goal.stringify())
            heappush(heap, (m.g + h_n, m)) 
            bib[key] = m

    elapsed_time = time.time() - start_time
    print("Execution time {}".format(elapsed_time))
    print('No Solution')

Вот код IDA *:

#shows the moves done to solve the puzzle
def rebuild_path(state):
    path = []
    while state.parent != None:
        path.insert(0, state)
        state = state.parent
    path.insert(0, state)
    print("Number of steps to solve: {}".format(len(path) - 1))
    print('Solution')

def ida_star(root, goal, h):
    print("IDA*")
    start_time = time.time()
    bound = h(root.stringify(), goal.stringify())
    path = [root]
    solved = False
    while not solved:
        t = search(path, 0, bound, goal, h)
        if type(t) == Board:
            solved = True
            elapsed_time = time.time() - start_time
            print("Execution time {}".format(elapsed_time))
            rebuild_path(t)
            return t
        bound = t

def search(path, g, bound, goal, h):

    node = path[-1]
    time.sleep(0.005)
    f = g + h(node.stringify(), goal.stringify())

    if f > bound: return f
    if node == goal:
        return node

    min_cost = float('inf')
    heap = []
    valid_moves = node.get_valid_moves()
    children = node.get_children(valid_moves)
    for m in children:
      if m not in path:
        heappush(heap, (m.g + h(m.stringify(), goal.stringify()), m)) 

    while len(heap) > 0:
        path.append(heappop(heap)[1])
        t = search(path, g + 1, bound, goal, h)
        if type(t) == Board: return t
        elif t < min_cost: min_cost = t
        path.pop()
    return min_cost

class Board:
  def __init__(self, board, parent=None, g=0, last_moved_piece=''):
    self.board = board
    self.capacity = len(board[0])
    self.g = g
    self.parent = parent
    self.piece = last_moved_piece

  def __lt__(self, b):
    return self.g < b.g

  def __call__(self):
    return self.stringify()

  def __eq__(self, b):
    if self is None or b is None: return False
    return self.stringify() == b.stringify()

  def __repr__(self):
    return '\n'.join([' '.join([j[0] for j in i]) for i in self.board])+'\n\n'

  def stringify(self):
    b=''
    for i in self.board:
      a = ''.join([j[0] for j in i])
      b += a + '-' * (self.capacity-len(a))

    return b

  def get_valid_moves(self):
    pos = []
    for i in range(len(self.board)):
      if len(self.board[i]) < self.capacity:
        pos.append(i)
    return pos

  def get_children(self, moves):
    children = []
    for i in range(len(self.board)):
      for j in moves:
        if i != j and self.board[i][-1] != self.piece:
          a = deepcopy(self.board)
          piece = a[i].pop()
          a[j].append(piece)
          children.append(Board(a, self, self.g+1, piece))
    return children

Использование:

initial = Board(start)
final1 = Board(case_easy)
final2 = Board(case_medium)
final2a = Board(case_medium2)
final3 = Board(case_hard)

x = textdistance.gotoh.distance

a_star(initial, final1, x)
a_star(initial, final2, x)
a_star(initial, final2a, x)

ida_star(initial, final1, x)
ida_star(initial, final2, x)
ida_star(initial, final2a, x)
0 голосов
/ 03 апреля 2020

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

Так что же такое p? Для каждого столбца определите p как число блоков, которые еще нужно удалить, прежде чем все цвета в этом столбце станут желаемым цветом. Итак, предположим, что мы хотим, чтобы красные блоки оказались в крайнем левом столбце (я вернусь к этому позже), и предположим, что есть один красный блок внизу, затем желтый сверху, еще один блок сверху это, а затем пустое место. Тогда p = 2 для этого столбца (два блока, чтобы удалить, прежде чем все будут красными). Рассчитайте p для всех столбцов. Для столбца, который должен оказаться пустым, p равно количеству блоков в нем (все они должны go). P для текущего состояния - это сумма всех p для всех столбцов.

Когда p = 0, все столбцы имеют одинаковый цвет и один столбец пуст, поэтому игра завершена.

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

Итак, как мы можем определить, где должен находиться каждый цвет? В основном, путем определения р для каждой возможности. Так, например, начните с красного / желтого / зеленого / пустого, вычислите p, затем go до красного / желтого / пустого / зеленого, вычислите p, et c. Возьмите стартовую позицию с самым низким p. Это занимает п! расчеты. Для n = 8 это 40320, что выполнимо. Плохая новость заключается в том, что вам придется проверять все стартовые позиции с равным самым низким p. Хорошей новостью является то, что вы можете забыть об остальном.

Здесь есть две математические неопределенности. Один: возможно ли, что есть более короткий путь, который использует плохой ход? Кажется маловероятным, я не нашел контрпример, но я также не нашел доказательства. Второе: возможно ли, что при старте с неоптимальной стартовой позиции (т. Е. Не с самого низкого p) путь будет короче, чем при всех оптимальных стартовых позициях. Опять же: нет контрпримеров, но нет и доказательств.

Некоторые предложения по реализации. Отслеживание p во время выполнения для каждого столбца не сложно, но, конечно, это нужно сделать. Другим параметром, который следует сохранить для каждого столбца, является количество открытых пятен. Если 0, то эти столбцы могут на мгновение не принимать никаких блоков, поэтому могут быть исключены из l oop. Когда p = 0 для столбца, он не подходит для pop. Для каждого возможного хлопка проверьте, есть ли хороший ход, то есть тот, который уменьшает общее p. Если их несколько, изучите все. Если их нет, рассмотрите все нейтральные ходы.

Все это должно значительно сократить время вычислений.

0 голосов
/ 03 апреля 2020

В комментариях вы сказали, что есть N стеков с емкостью P, и всегда есть P пустых мест. В этом случае кажется, что этот алгоритм будет работать в предложении else в вашем коде (т. Е. Когда num_removals + min_to_unlock > num_free_spaces):

  1. Найдите нужную часть, ближайшую к вершине stack.
  2. Переместите все фигуры сверху желаемой фигуры таким образом, чтобы был один стек (не стек назначения), в котором сверху было пустое место. При необходимости переместите фигуры из стека назначения или другого стека. Если единственным открытым пространством является вершина стека назначения, переместите туда кусок, чтобы открыть вершину другого стека. Это всегда возможно, потому что есть P открытых пространств и не более P-1 фигур для перемещения над нужной частью.
  3. Переместите нужный фрагмент в пустое место на вершине стека.
  4. Перемещение фигур из стека назначения до тех пор, пока место назначения не будет открыто.
  5. Перемещение нужной части в место назначения.
...