아이템을 스택의 한 위치로 옮기기위한 최소 이동 횟수를 찾는 방법은 무엇입니까?


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']
         ]

나는에서 "B"를 삽입 할 경우 stacks[1][1]등 그 stacks[1] = ["-", "B", "Y", "Y"]. 그렇게하는 데 필요한 최소 이동 수를 어떻게 확인할 수 있습니까?

나는 여러 가지 접근법을 살펴 보았고, 상태에서 가능한 모든 움직임을 생성하고 점수를 매긴 다음 최고 점수 경로를 계속하는 유전자 알고리즘을 시도했으며 문제에 대한 경로 찾기를 위해 Djikstra의 알고리즘을 실행하려고했습니다. . 실망스럽게 보이지만 지수 시간 이외의 다른 방법으로 실행할 수있는 방법을 알 수 없습니다. 여기에 적용 가능한 누락 된 알고리즘이 있습니까?

편집하다

필요한 최소 이동 수를 계산하기 위해이 함수를 작성했습니다. 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]으로 약간의 이동이 주어진 경우, 스택 [Z]가 이동했을 때와 동일한 상태에있는 경우 약간의 이동, 스택 [Y]에서 스택 [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 * 를 사용해 보셨습니까 ? Dijkstra의 알고리즘과 상당히 유사하지만 때로는 훨씬 빠릅니다.
Yonlif

1
github repo 링크를 공유 할 수 있습니까? 괜찮다면 스스로 실험하고 싶습니다. @Tristen
Yonlif

1
처음 살펴본 후이 문제는 NP-hard로 보입니다. 최적의 솔루션을 제공하더라도 쉽게 확인할 수 없기 때문에 아마도 NP 내에 있지 않을 수도 있습니다 (NP- 완료 아님). 순열에 대한 최적화 문제로 악명이 높습니다. CS 에서 문제를 교차 게시하는 것이 좋습니다 . 이 문제에 대한 근사 알고리즘을 살펴보십시오. 이것은 꽤 어려운 문제이지만 적절한 근사치가 존재해야합니다. 이 유사하다 : 하노이의 임의 타워
DarioHett

1
@DarioHett 그것이 내가 걱정했던 것입니다! 나는 그것이 NP-Hard 문제가되지 않을 것이라고 손가락을 엇갈리게했지만, 또한 그것이 하나일지도 모른다는 느낌이 들었습니다. 나는 유전자 알고리즘과 움직임에 점수를 매기는 특수한 채점 기능으로 더 나은 행운을 얻었습니다. 하노이의 임의 타워를 살펴 보겠습니다! 제안 해 주셔서 감사합니다.
Tristen

1
퍼즐을 무작위로 생성하려는 경우 명백하게 여분의 이동을 제거해야합니다 (정방향 이동 후 무언가를 뒤로 이동하거나 두 단계로 이동하면 충분할 것입니다.
Hans Olsson

답변:


1

나는 두 가지 옵션을 생각해 냈지만 어느 것도 사례 2를 적시에 해결할 수 없습니다. 첫 번째 옵션은 h (n)로 문자열 거리 측정 값과 함께 A *를 사용하는 것이고, 두 번째 옵션은 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

주석에는 용량 P의 N 스택이 있으며 항상 P 빈 공간이 있다고 말했습니다. 이 경우이 알고리즘이 else코드 의 절에서 작동하는 것 같습니다 (예 : when num_removals + min_to_unlock > num_free_spaces).

  1. 스택 상단에 가장 가까운 원하는 부분을 찾으십시오.
  2. 맨 위에 빈 공간이있는 하나의 스택 (대상 스택이 아님)이 있도록 원하는 조각 위에서 모든 조각을 이동합니다. 필요한 경우 대상 스택 또는 다른 스택에서 조각을 이동하십시오. 열린 공간 만 대상 스택의 상단에 있으면 조각을 이동하여 다른 스택의 상단을 엽니 다. P 조각과 P-1 조각이 원하는 조각 위로 이동하기 때문에 항상 가능합니다.
  3. 원하는 조각을 스택 위의 빈 지점으로 이동하십시오.
  4. 대상이 열릴 때까지 대상 스택에서 조각을 이동하십시오.
  5. 원하는 조각을 대상으로 이동하십시오.

지난 몇 시간 동안이 답변을 파헤쳐 보았으며 거기에 뭔가있을 수 있다고 생각합니다. 가능하면 원하는 조각 위의 조각을 이동하는 방법에 대해 좀 더 자세한 정보를 제공 할 수 있습니까? 스택을 이동할 스택을 어떻게 결정합니까? 아마도 일부 psuedocode / code 일 것입니다. 이것은 내가 지금까지 이것을 해결 한 것에 가장 가깝습니다.
Tristen

0

수학적으로 이것을 증명할 시간을 찾지 못했지만 어쨌든 이것을 게시하기로 결정했습니다. 도움이되기를 바랍니다. 이 방법은 좋은 움직임으로 감소하고 게임이 끝나면 정확히 0에 도달하는 매개 변수 p를 정의하는 것입니다. 이 프로그램에서는 좋은 움직임이나 중립적 인 움직임 만 고려하고 (p는 변경되지 않은 상태) 나쁜 움직임은 잊습니다 (p 증가).

p는 무엇입니까? 모든 열에 대해 p를 해당 열의 모든 색상이 원하는 색상이되기 전에 제거해야하는 블록 수로 정의하십시오. 그래서 우리가 가장 왼쪽 열에 빨간색 블록이 있고 (아래에 다시 올 것이라고 가정), 아래쪽에 하나의 빨간색 블록이 있고 그 위에 노란색이 있고 그 위에 노란색이 하나 있다고 가정하십시오. 그런 다음 빈 공간입니다. 그런 다음이 열에 대해 p = 2입니다 (모두 제거하기 전에 두 블록이 모두 빨간색 임). 모든 열에 대해 p를 계산하십시오. 비어있는 열의 경우 p는 그 안에있는 블록 수와 같습니다 (모두 가야 함). 현재 상태의 P는 모든 열에 대한 모든 p의 합입니다.

p가 0이면 모든 열의 색이 같고 한 열이 비어 있으므로 게임이 완료된 것입니다.

우리가 올바른 방향으로 움직이고있는 p를 줄이거 나 최소한 p를 늘리지 않는 움직임을 선택함으로써, 이것이 가장 짧은 경로 알고리즘과 결정적인 차이점이라고 생각합니다. 그가 조사하고있는 정점.

그렇다면 각 색상의 위치를 ​​어떻게 결정합니까? 기본적으로 모든 가능성에 대해 p를 결정합니다. 예를 들어 빨강 / 노랑 / 녹색 / 빈으로 시작하고 p를 계산 한 다음 빨강 / 노랑 / 빈 / 녹색으로 이동하고 p를 계산하십시오. p가 가장 낮은 시작 위치를 취하십시오. 이것은 n이 걸립니다! 계산. n = 8의 경우 이것은 40320이며, 가능합니다. 나쁜 소식은 p가 가장 낮은 모든 시작 위치를 검사해야한다는 것입니다. 좋은 소식은 나머지를 잊을 수 있다는 것입니다.

여기에는 두 가지 수학적 불확실성이 있습니다. 하나 : 잘못된 이동을 사용하는 짧은 경로가있을 수 있습니까? 가능성이없는 것 같습니다, 나는 counterexample을 찾지 못했지만 증거를 찾지 못했습니다. 둘째 : 최적이 아닌 시작 위치 (즉 가장 낮은 p가 아님)로 시작할 때 모든 최적의 시작 위치보다 경로가 짧을 수 있습니다. 다시 한 번, 반례는 없지만 증거는 없습니다.

몇 가지 구현 제안. 각 열에 대해 실행하는 동안 p를 추적하는 것은 어렵지 않지만 물론 수행해야합니다. 각 열에 대해 유지해야하는 다른 매개 변수는 열린 지점 수입니다. 0이면이 열은 순간적으로 어떤 블록도 수용 할 수 없으므로 루프에서 제외 될 수 있습니다. 열이 p = 0이면 팝을 사용할 수 없습니다. 가능한 모든 팝에 대해 좋은 움직임, 즉 전체 p를 감소시키는 움직임이 있는지 검사하십시오. 여러 개가 있으면 모두 검사하십시오. 없는 경우 모든 중립 이동을 고려하십시오.

이 모든 것이 계산 시간을 크게 줄여야합니다.


1
나는 당신이 그 질문을 오해했다고 생각합니다! 이것이 문제의 동기가되지만. 문제는 단일 부품을 단일 위치로 이동하기위한 최소 이동 수를 찾는 것입니다. 문제는 스택을 정렬하기위한 최소 이동 수를 찾지는 않았지만, 그것은 문제의 동기입니다. 그러나 P 점수가 맞으면 틀릴 것입니다. 처음에는 P가 증가하고 나중에 더 빠른 속도로 감소하는 "나쁜 움직임"이있는 경우가 많이 있습니다. 그 대답으로 대답이 관련이 없으므로 질문을 다시 읽으십시오.
Tristen

1
사과 사과 Tristen, 나는 그 질문을주의 깊게 읽지 않았습니다. 나는 그것의 수학적 측면에 매료되어 파티에 늦어서 너무 빨리 대답하지 못했습니다. 다음에 더 조심하겠습니다. 잘하면 당신은 대답을 찾을 수 있습니다.
Paul Rene
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.