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


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"]. Як я можу визначити мінімальну кількість рухів, необхідних для цього?

Я дивився на кілька підходів, я спробував генетичні алгоритми, які генерують усі можливі рухи від стану, оцінюють їх, а потім продовжують внизу найкращих балів, я також спробував запустити алгоритм Джикстра для визначення маршруту щодо проблеми . Це здається розчаровуючи простим, але я не можу придумати спосіб змусити його запуститись ні в що інше, ніж експоненційний час. Чи є у мене відсутній алгоритм, який застосовується тут?

Редагувати

Я написав цю функцію для обчислення мінімальної кількості необхідних рухів: стеки: Список списку символів, що представляють фігури в стеку, стеки [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']

Оновлення статусу

Я дуже налаштований якось вирішити цю проблему .

Майте на увазі, що існують способи мінімізувати кількість випадків, таких як випадки, про які Ханс Ольссон згадував у коментарях. Мій останній підхід до цієї проблеми полягає у розробці набору правил, подібних до згаданих, та використанню їх у алгоритмі поколінь.

Такі правила, як:

Ніколи не зворотний крок. Перехід від 1-> 0, потім 0-> 1 (не має сенсу)

Ніколи не рухайте шматок двічі поспіль. Ніколи не рухайтеся від 0 -> 1, потім 1 -> 3

З огляду на деякий перехід від стеків [X] до стеків [Y], то деяка кількість переміщень, то переміщення зі стеків [Y] в стеки [Z], якщо стеки [Z] знаходяться в тому ж стані, що і при переміщенні зі стеків [X] до стеків [Y] стався, переміщення можна було усунути, перемістившись зі стеків [X] безпосередньо в стеки [Z]

В даний час я підходжу до цієї проблеми зі спробою створити достатню кількість правил, щоб вона мінімізувала кількість "дійсних" рухів, достатньо, щоб відповідь можна було обчислити за допомогою генераційного алгоритму. Якщо хтось може придумати додаткові правила, мені було б цікаво почути їх у коментарях.

Оновлення

Завдяки відповіді @RootTwo у мене був невеликий прорив, який я окреслити тут.

На прорив

Визначте висоту цілі, оскільки глибина цільової деталі повинна бути розміщена в цільовому стеку.

Щоразу, коли якась частина мети буде розміщена в індексі <= 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 repo? Я хотів би експериментувати сам, якщо це нормально. @Tristen
Yonlif

1
Після першого погляду ця проблема здається NP-важкою. Це, мабуть, не в межах NP (не NP-повне), тому що навіть якщо я даю вам оптимальне рішення, ви навіть не зможете легко його перевірити. Це горезвісно для проблем оптимізації перестановок. Я б запропонував переписати проблему в CS . Подивіться на алгоритми наближення цього питання. Це досить важка проблема, але гідне наближення повинно існувати. Це схоже: Довільні вежі Ханої
ДаріоХетт

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. Перемістіть потрібний шматок до місця призначення.

Я провів останні пару годин, переглядаючи цю відповідь, і думаю, що там може щось бути. Якщо можливо, ви могли б надати трохи більше інформації про те, як би ви рухалися про переміщення шматочків, що знаходяться над потрібним твором? Як визначити, до яких стеків їх перемістити? Можливо, трохи psuedocode / code. Це, безумовно, найближче, що я відчував до вирішення цього поки що.
Трістен

0

Хоча я не знайшов часу, щоб довести це математично, я все-таки вирішив опублікувати це; сподіваюся, що це допомагає. Підхід полягає у визначенні параметра p, який зменшується з хорошими рухами та досягає нуля саме тоді, коли гра закінчена. У програмі розглядаються лише добрі ходи або нейтральні рухи (які залишають p незмінними) і забувають про погані ходи (що збільшують p).

Отже, що таке p? Для кожного стовпця визначте p як кількість блоків, які ще потрібно видалити, перш ніж усі кольори у цьому стовпці будуть бажаним кольором. Отже, припустимо, що ми хочемо, щоб червоні блоки опинилися в крайньому лівому стовпці (я повернусь до цього пізніше), і припустимо, що внизу є один червоний блок, потім жовтий, ще один блок зверху те, а потім порожнє місце. Тоді p = 2 для цього стовпця (два блоки, які потрібно видалити, перш ніж усі стануть червоними). Обчисліть p для всіх стовпців. Для стовпця, який повинен закінчитися порожнім, p дорівнює кількості блоків, які є в ньому (усі вони повинні пройти). P для поточного стану - це сума всіх p для всіх стовпців.

Коли p = 0, усі стовпці мають однаковий колір, а один стовпець порожній, тому гра закінчена.

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

Тож як ми можемо визначити, де повинен закінчуватися кожен колір? В основному, визначаючи p для кожної можливості. Так, наприклад, почніть з червоного / жовтого / зеленого / порожнього, обчисліть p, а потім перейдіть до червоного / жовтого / порожнього / зеленого, обчисліть p тощо. Займіть початкову позицію з найнижчого p. Це займає п! розрахунки. Для n = 8 це 40320, що можна виконати. Погана новина полягає в тому, що вам доведеться вивчити всі вихідні позиції з рівними найнижчими p. Хороша новина в тому, що ви можете забути решту.

Тут є дві математичні невизначеності. Перше: чи можливо, що існує коротший шлях, який використовує поганий хід? Мабуть, мабуть, я не знайшов контрприкладу, але не знайшов і доказів. Двоє: чи можливо, що при запуску з неоптимальної вихідної позиції (тобто не найнижчого p) був би коротший шлях, ніж при всіх оптимальних вихідних положеннях. Знову ж таки: жодного контрприкладу, але жодного доказу теж немає.

Деякі пропозиції щодо впровадження. Відстеження p під час виконання для кожного стовпця не є складним, але, звичайно, це потрібно робити. Ще один параметр, який слід зберігати для кожного стовпця, - це кількість відкритих плям. Якщо 0, то ці стовпці можуть миттєво не приймати жодних блоків, тому можуть залишатися поза циклом. Коли p = 0 для стовпця, він не підходить для попсу. Для кожного можливого попса вивчіть, чи є хороший хід, тобто такий, який зменшує загальну p. Якщо їх кілька, огляньте всіх. Якщо такого немає, врахуйте всі нейтральні ходи.

Все це повинно значно скоротити час на обчислення.


1
Я думаю, ви неправильно зрозуміли питання! Хоча в цьому і полягає мотивація питання. Питання полягає в тому, щоб знайти мінімальну кількість рухів для переміщення однієї частини, до одного місця. Питання полягало не в тому, щоб знайти мінімальну кількість рухів для сортування стеків, хоча саме в цьому мотивація питання. Однак з таким підрахунком P ви б помилилися. Існує багато випадків, коли виникають "погані кроки", які спочатку збільшують P, а потім пізніше зменшують його. З урахуванням сказаного, можливо, перечитайте питання, оскільки ваша відповідь не має жодної актуальності.
Трістен

1
Вибачте Трістен, я справді питання уважно не читав. Мене захопив математичний аспект цього, і, запізнившись на партію, занадто швидко відповів. Я буду обережнішим наступного разу. Сподіваємось, ви знайдете відповідь.
Пол Рене
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.