【发布时间】: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)
这适用于所有情况。
因此,问题已简化为找到最小数量的问题 将球门块放置在球门高度或高于球门高度所需的移动。
这将问题拆分为一系列子问题:
当目标堆栈有它的可访问块 != 目标块时, 确定该作品是否存在有效位置,或者该作品是否应该 待在原地,同时交换另一块。
当目标堆栈有其可访问片段 == 目标片段时, 确定是否可以将其移除并放置在所需的目标高度,或者是否 一块应该保留,而另一块被交换。
当以上两种情况需要换另一块时, 确定要交换的部分以增加以使 球门块达到目标高度。
目标堆栈应始终首先评估其案例。
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