アイテムをスタック内の位置に移動するための最小移動数を見つける方法は?


12

スタック

Nがスタックの数、Pがスタックの容量であるNXPスタックのセットが与えられた場合、ロケーション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のアルゴリズムを実行しようとしました。イライラするほどシンプルに見えますが、指数時間以外で実行する方法を理解できません。ここで適用できる欠落しているアルゴリズムはありますか?

編集する

必要な移動の最小数を計算するためにこの関数を書きました:スタック:スタック内のピースを表すキャラクターのリストのリスト、スタック[0] [0]はスタックのトップです[0]スタック_インデックス:インデックスピースを追加するスタック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の順に移動します(意味がありません)

2回続けて駒を移動しないでください。0-> 1、1-> 3の順に移動しない

stacks [Z]が移動時と同じ状態にある場合、stacks [X]からstacks [Y]へのいくつかの移動、いくつかの移動、そしてstacks [Y]からstacks [Z]への移動が与えられます。 stacks [X]からstacks [Y]への移動が発生した場合、stacks [X]から直接stacks [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. 上記の2つのケースで別のピースを交換する必要がある場合、ゴールピースがゴールの高さに到達できるようにするために、どのピースを交換するかを決定します。

宛先スタックは常にそのケースを最初に評価する必要があります。

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 *を試しましか?ダイクストラのアルゴリズムとかなり似ていますが、かなり高速な場合もあります。
Yonlif

1
githubリポジトリのリンクを共有していただけますか?よかったら自分で実験したいです。@Tristen
Yonlif

1
一見したところ、この問題はNPに難しいようです。それはおそらくNP内ではありません(NP完全ではありません)。なぜなら、私が最適なソリューションを提供したとしても、簡単に検証することさえできないからです。これは順列の最適化問題で悪名高いです。CSで問題をクロスポストすることをお勧めします。この問題の近似アルゴリズムを調べてください。これはかなり難しい問題ですが、適切な近似が存在するはずです。これは似ています:ハノイの任意の塔
DarioHett

1
@DarioHettそれが私が心配していたことです!私はそれがNP-Hardの問題になることはないだろうと指を交差させましたが、私はそれが問題であるかもしれないという直感も持っていました。私は遺伝的アルゴリズムと、動きをスコアリングするいくつかの特殊なスコアリング関数で、より良い運を得ています。ハノイの任意の塔を見てみましょう!提案をありがとう。
Tristen

1
パズルをランダムに生成しようとする場合-明らかに冗長な動きを削除することを忘れないでください(前進の後で何かを戻すか、1つのステップで十分な場合は2つのステップで移動します。また、関連しない可能性のある動きと組み合わせて)。
Hans Olsson

回答:


1

私は2つのオプションを考え出しましたが、いずれもケース2をタイムリーに解決することができません。最初のオプションは、A(*)をh(n)として文字列距離メジャーと共に使用することで、2番目のオプションはIDA *です。私は多くの文字列類似性測定をテストしました。私のアプローチではsmith-watermanを使用しました。問題をより早く処理できるように表記を変更しました。各桁の最後に数字を追加して、駒が2回移動されたかどうかを確認しました。

これが私がテストしたケースです:

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

コメントで、容量PのスタックはN個あり、常にP個の空のスペースがあります。その場合、このアルゴリズムはelseコード句で機能するようです(つまり、の場合num_removals + min_to_unlock > num_free_spaces)。

  1. スタックの一番上に最も近い目的のピースを見つけます。
  2. 目的のピースの上からすべてのピースを移動します。これは、上部に空きスペースがある1つのスタック(宛先スタックではない)になるようにするためです。必要に応じて、宛先スタックまたは別のスタックからピースを移動します。唯一のオープンスペースが宛先スタックの一番上にある場合は、そこに駒を移動して、別のスタックの一番上を開きます。P個のオープンスペースがあり、最大でP-1個のピース​​が目的のピースの上から移動するため、これは常に可能です。
  3. スタックの一番上の空の場所に目的のピースを移動します。
  4. 宛先が開かれるまで、宛先スタックからピースを移動します。
  5. 目的のピースを目的地に移動します。

この答えを掘り下げるために過去数時間を費やしてきましたが、そこに何かがあると思います。可能であれば、目的のピースの上にあるピースを移動する方法についてもう少し情報を提供できますか?どのスタックに移動するかをどのように決定しますか?おそらく少しの疑似コード/コードです。これは間違いなくこれまでのところこれを解決するのに私が感じた最も近いものです。
Tristen

0

これを数学的に証明する時間はわかりませんが、とにかくこれを投稿することにしました。それが役に立てば幸い。アプローチは、良い動きで減少し、ゲームが終了したときに正確にゼロに達するパラメーターpを定義することです。プログラムでは、良い動きまたはニュートラルな動き(pを変更しないままにする)のみを考慮し、悪い動き(pを増やす)を忘れます。

それで、pとは何ですか?すべての列について、その列のすべての色が目的の色になる前に削除する必要があるブロックの数としてpを定義します。それで、赤いブロックを左端の列に終わらせたいとしましょう(後で戻ってきます)。下部に赤いブロックが1つあり、その上に黄色があり、上部にもう1つのブロックがあるとします。それ、そして空のスペース。次に、この列のp = 2(すべてが赤になる前に削除する2つのブロック)。すべての列のpを計算します。空になる列の場合、pはその列に含まれるブロックの数と等しくなります(すべての列が移動するはずです)。現在の状態のPは、すべての列のすべてのpの合計です。

p = 0の場合、すべての列が同じ色になり、1つの列が空になるため、ゲームは終了します。

私は正しい方向に移動しているpを減少させる(または少なくとも増加しない)動きを選択することにより、これは最短経路アルゴリズムとの決定的な違いです:ダイクストラはすべての方向で正しい方向に移動しているかどうかわかりませんでした彼が調査していた頂点。

では、どのようにして各色を最終的に決定するのでしょうか。基本的に、あらゆる可能性についてpを決定します。たとえば、赤、黄、緑、空から始めて、pを計算してから、赤、黄、空、緑に進み、pを計算します。最小のpから開始位置を取得します。これはnかかります!計算。n = 8の場合、これは40320であり、実行可能です。悪いニュースは、最低のpが等しいすべての開始位置を調べる必要があることです。良い知らせは、残りを忘れることができるということです。

ここには2つの数学的な不確実性があります。1つ:悪い動きを使用する短いパスがある可能性はありますか?ありそうもないようですが、反例は見つかりませんでしたが、証拠も見つかりませんでした。2つ目:非最適な開始位置(つまり、最小のpではない)で開始する場合、すべての最適な開始位置よりも経路が短くなる可能性があります。繰り返しますが、反例はありませんが、証拠もありません。

いくつかの実装の提案。各列の実行中にpを追跡することは難しくありませんが、もちろん実行する必要があります。各列で保持する必要がある別のパラメーターは、オープンスポットの数です。0の場合、この列は一時的にブロックを受け入れないため、ループから除外できます。列がp = 0の場合、ポップの対象にはなりません。考えられるすべてのポップについて、良い動きがあるかどうか、つまり全体的なpを減らす動きがあるかどうかを調べます。複数ある場合は、すべて調べてください。何もない場合は、すべての中立的な動きを検討してください。

これにより、計算時間が大幅に短縮されます。


1
私はあなたが質問を誤解したと思います!これは質問の背後にある動機ですが。問題は、単一のピースを単一の場所に移動するための最小移動数を見つけることです。問題は、スタックをソートするための最小数の動きを見つけることではありませんでしたが、それが問題の動機です。ただし、Pのスコアリングでは、正しくありません。最初はPを増加させ、その後、より速い速度で減少させる「悪い動き」が存在する多くの例があります。とはいえ、回答に関連性がないため、質問をもう一度読んでください。
トリステン

1
トリステンさん、申し訳ありませんが、質問を注意深く読んでいませんでした。私はそれの数学的側面に魅了され、パーティーに遅刻したので、返答が早すぎました。次回はもっと注意していきます。うまくいけば、あなたは答えを見つけるでしょう。
ポールルネ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.