【问题标题】:How to find the minimum number of moves to move an item into a position in a stack?如何找到将项目移动到堆栈中某个位置的最小移动次数?
【发布时间】:2020-07-04 13:47:34
【问题描述】:

给定一组 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']
         ]

如果我想在stacks[1][1] 插入一个“B”,这样stacks[1] = ["-", "B", "Y", "Y"]。如何确定这样做所需的最少移动次数?

我一直在研究多种方法,我尝试过从一个状态生成所有可能的移动的遗传算法,对它们进行评分,然后继续沿着最佳评分路径前进,我还尝试运行 Djikstra 的寻路算法关于问题。它看起来简单得令人沮丧,但我想不出一种方法让它在指数时间内运行。是否有我遗漏的适用于此处的算法?

编辑

我编写了这个函数来计算所需的最小移动次数: stacks: List of Characters 表示堆栈中的片段,stacks[0][0] 是堆栈的顶部 [0] stack_ind:将要添加到的堆栈的索引 need_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']

状态更新

我非常有决心以某种方式解决这个问题。

请记住,有一些方法可以最大限度地减少案例数量,例如 cmets 中提到的 @Hans Olsson。我最近解决这个问题的方法是开发一组类似于上述规则的规则,并将它们应用到世代算法中。

规则如:

永远不要逆转动作。从 1->0 然后 0->1(没有意义)

永远不要连续移动一块。永远不要从 0 -> 1 然后 1 -> 3

给定从 stacks[X] 到 stacks[Y] 的一些移动,然后是一些移动次数,然后是从 stacks[Y] 到 stacks[Z] 的移动,如果 stacks[Z] 处于与之前相同的状态当发生从 stacks[X] 到 stacks[Y] 的移动时,可以通过从 stacks[X] 直接移动到 stacks[Z] 来消除移动

目前,我正在尝试创建足够的规则来解决这个问题,以最大限度地减少“有效”移动的数量,从而可以使用分代算法计算答案。如果有人能想到其他规则,我很想在 cmets 中听到它们。

更新

感谢@RootTwo 的回答,我有了一些突破,我将在这里概述。

迈向突破

将球门高度定义为球门块必须放置在 目标堆栈。

每当某个目标块放置在索引

Let S represent some solid Piece.

I.E.

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. 当以上两种情况需要换另一块时, 确定要交换的部分以增加以使 球门块达到目标高度。

目标堆栈应始终首先评估其案例。

I.E.

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

【问题讨论】:

  • 你试过A*吗?它与 Dijkstra 的算法非常相似,但有时速度要快得多。
  • 你能分享一个github repo链接吗?如果可以的话,我想自己试验一下。 @Tristen
  • 乍一看,这个问题似乎是 NP-hard。它可能不在 NP 范围内(不是 NP 完全的),因为即使我给你一个最优解,你也无法轻易验证它。这对于排列的优化问题是臭名昭著的。我建议在CS 交叉发布问题。研究这个问题的近似算法。这是一个相当困难的问题,但应该存在一个不错的近似值。这是相似的:Arbitrary Towers of Hanoi
  • @DarioHett 这就是我担心的!我祈祷它最终不会成为一个 NP-Hard 问题,但我也有一种直觉,它可能是一个问题。我一直在使用遗传算法以及一些专门的评分函数来对移动进行评分,运气更好。我来看看河内的任意塔!感谢您的建议。
  • 如果您尝试随机生成拼图 - 请记住删除明显多余的动作(在向前移动后将某些东西向后移动或在一个就足够的情况下分两步移动;并且还结合可能不相关的移动混合)。

标签: python algorithm sorting stack dynamic-programming


【解决方案1】:

虽然我还没有时间用数学方法证明这一点,但我还是决定发布这个;希望能帮助到你。方法是定义一个参数 p,它随着好的动作而减小,并在游戏结束时准确地达到零。在程序中只考虑好的移动或中性移动(保持 p 不变)并忘记坏移动(增加 p)。

那么什么是p?对于每一列,将 p 定义为在该列中的所有颜色成为所需颜色之前仍必须删除的块数。所以假设我们希望红色块在最左边的列中结束(我稍后会回到那个),并假设底部有一个红色块,然后在其顶部有一个黄色,在顶部再有一个块那个,然后是一个空的空间。然后此列的 p=2 (在全部变为红色之前要删除两个块)。计算所有列的 p。对于最终应该为空的列,p 等于其中的块数(所有块都应该去)。当前状态的 P 是所有列的所有 p 的总和。

当p=0时,所有列颜色相同,一列为空,游戏结束。

通过选择减少 p(或至少不增加 p)的移动,我们正朝着正确的方向前进,在我看来,这是与最短路径算法的关键区别:Dijkstra 不知道他是否在向右移动他正在调查的每个顶点的方向。

那么我们如何确定每种颜色的最终位置?基本上是通过为每种可能性确定 p。所以例如从红/黄/绿/空开始,计算p,然后到红/黄/空/绿,计算p等。取p最低的起始位置。这需要 n!计算。对于 n=8,这是 40320,这是可行的。坏消息是你必须检查所有具有相同最低 p 的起始位置。好消息是你可以忘记其余的。

这里有两个数学上的不确定性。一:是否有可能有一条较短的路径使用了一个坏的举动?似乎不太可能,我没有找到反例,但我也没有找到证据。二:是否有可能从非最佳起始位置(即不是最低 p)开始时,路径会比所有最佳起始位置更短。再说一遍:没有反例,但也没有证据。

一些实施建议。在每列的执行过程中跟踪 p 并不困难,但当然应该这样做。应该为每列保留的另一个参数是开放点的数量。如果为 0,则此列暂时不接受任何块,因此可以排除在循环之外。当列的 p=0 时,它不符合弹出条件。对于每一个可能的流行,检查是否有一个好的举动,即一个会降低整体 p 的值。如果有多个,检查所有。如果没有,请考虑所有中立的动作。

所有这些都将大大减少您的计算时间。

【讨论】:

  • 我想你误解了这个问题!尽管这是问题背后的动机。问题是找到将单件移动到单个位置的最小移动次数。问题不是要找到对堆栈进行排序的最小移动次数,尽管这是问题背后的动机。但是,对于 P 的评分,您将是不正确的。在许多情况下,“坏动作”最终会首先增加 P,然后以更快的速度减少它。话虽如此,也许重新阅读该问题,因为您的答案无关紧要。
  • 我很抱歉 Tristen,我确实没有仔细阅读这个问题。我对它的数学方面很着迷,而且因为参加聚会迟到了,所以回答得太快了。下次我会更加小心。希望你能找到答案。
【解决方案2】:

我想出了两个选择,但没有一个能够及时解决案例 2。第一个选项是使用带有字符串距离度量的 A* 作为 h(n),第二个选项是 IDA*。我测试了许多字符串相似性度量,我在我的方法中使用了 smith-waterman。我已更改您的符号以更快地处理问题。我在每个数字的末尾添加了数字,以检查一块是否移动了两次。

以下是我测试过的案例:

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)

【讨论】:

    【解决方案3】:

    在你说的cmets中有N个容量为P的堆栈,并且总是有P个空格。如果是这种情况,该算法似乎将在您的代码中的 else 子句中起作用(即当 num_removals + min_to_unlock &gt; num_free_spaces 时):

    1. 找到最靠近堆栈顶部的所需部分。
    2. 从所需棋子上方移动所有棋子,使一个堆栈(不是目标堆栈)顶部有一个空白空间。如果需要,从目标堆栈或另一个堆栈中移动碎片。如果唯一的开放空间是目标堆栈的顶部,则将一块移到那里以打开另一个堆栈的顶部。这总是可能的,因为有 P 个开放空间,并且最多有 P-1 个棋子可以从所需棋子上方移动。
    3. 将所需的部分移动到堆栈顶部的空白位置。
    4. 从目标堆栈中移动棋子,直到目标打开。
    5. 将所需的部分移动到目的地。

    【讨论】:

    • 过去几个小时我一直在研究这个答案,我认为那里可能有一些东西。如果可能的话,您能否提供更多信息,说明您将如何移动所需部分之上的部分?您如何确定将它们移动到哪些堆栈?也许有点伪代码/代码。这绝对是我迄今为止最接近解决这个问题的感觉。
    猜你喜欢
    • 2016-08-06
    • 2022-08-24
    • 1970-01-01
    • 1970-01-01
    • 2012-10-29
    • 1970-01-01
    • 2022-10-21
    • 1970-01-01
    • 2011-10-25
    相关资源
    最近更新 更多