Как найти минимальное количество ходов для перемещения предмета в позицию в стеке?

12

Стеки

Учитывая набор стеков 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)

Это будет справедливо для всех случаев.

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

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

  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
Тристень
источник
2
Вы пробовали A * ? Это довольно похоже на алгоритм Дейкстры, но иногда это значительно быстрее.
Йонлиф
1
Можете ли вы поделиться ссылкой GitHub репо? Я хотел бы поэкспериментировать сам, если все в порядке. @Tristen
Йонлиф
1
После первого взгляда эта проблема кажется NP-сложной. Это, вероятно, не в NP (не NP-полная), потому что даже если я дам вам оптимальное решение, вы даже не можете легко проверить его. Это печально известно для задач оптимизации на перестановках. Я бы предложил сделать кросс-пост в CS . Посмотрите на алгоритмы аппроксимации для этой проблемы. Это довольно сложная проблема, но приличное приближение должно существовать. Это похоже: Произвольные башни Ханоя
DarioHett
1
@DarioHett Это было то, что я волновался! Я скрестил пальцы, чтобы это не стало проблемой NP-Hard, но у меня также было ощущение, что это может быть одна проблема. Мне больше повезло с генетическим алгоритмом, а также с некоторыми специализированными функциями подсчета очков, которые оценивают ходы. Я посмотрю на Произвольные Башни Ханоя! Спасибо за предложение.
Тристен
1
Если вы пытаетесь сгенерировать головоломку случайным образом - не забудьте удалить явно избыточные ходы (перемещение чего-либо назад после движения вперед или выполнение шага в два шага, когда этого будет достаточно; а также в сочетании с, возможно, смешанными движениями)
Ганс Олссон

Ответы:

1

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

Вот случаи, на которых я проверял:

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

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

  1. Найдите нужный кусок, который находится ближе всего к вершине стека.
  2. Переместите все фигуры сверху желаемой фигуры таким образом, чтобы был один стек (а не целевой стек) с пустым пространством сверху. При необходимости переместите фигуры из стека назначения или другого стека. Если единственным открытым пространством является вершина стека назначения, переместите туда кусок, чтобы открыть вершину другого стека. Это всегда возможно, потому что есть P открытых пространств и не более P-1 фигур для перемещения сверху над нужной фигурой.
  3. Переместите нужный кусок в пустое место на вершине стека.
  4. Перемещать фигуры из стека назначения, пока место назначения не будет открыто
  5. Переместить нужный кусок в пункт назначения.
RootTwo
источник
Я провел последние пару часов, копаясь в этом ответе, и я думаю, что там что-то есть. Если возможно, не могли бы вы предоставить немного больше информации о том, как вы будете перемещать фигуры, которые находятся выше желаемой фигуры? Как вы определяете, в какие стеки их переместить? Возможно, немного psuedocode / кода. Это определенно ближе всего я чувствовал решение этой проблемы до сих пор.
Tristen
0

Хотя я не нашел времени, чтобы доказать это математически, я все равно решил опубликовать это; Надеюсь, поможет. Подход состоит в том, чтобы определить параметр 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. Если их несколько, изучите все. Если их нет, рассмотрим все нейтральные ходы.

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

Пол Рене
источник
1
Я думаю, что вы неправильно поняли вопрос! Хотя это мотивация вопроса. Вопрос состоит в том, чтобы найти минимальное количество ходов, чтобы переместить один кусок в одно место. Вопрос заключался не в том, чтобы найти минимальное количество ходов для сортировки стеков, хотя это и есть мотивация вопроса. Тем не менее, с этим счетом P, вы были бы неверны. Есть много случаев, когда есть «плохие ходы», которые вначале увеличивают P, а затем снижают его быстрее. С учетом сказанного, возможно, перечитайте вопрос, так как ваш ответ не имеет отношения к делу.
Tristen
1
Мои извинения, Кристен, я действительно не внимательно прочитал вопрос. Я был очарован математическим аспектом и, опоздав на вечеринку, слишком быстр, чтобы ответить. Я буду более осторожным в следующий раз. Надеюсь, вы найдете ответ.
Пол Рене