Bir öğeyi yığındaki bir konuma taşımak için minimum sayıda hareket nasıl bulunur?


12

Yığınlar

N'nin yığın sayısı ve P'nin yığın kapasitesi olduğu bir dizi NXP yığını göz önüne alındığında, A konumundaki bir düğümden bazı rastgele B konumuna geçmek için gereken minimum swap sayısını nasıl hesaplayabilirim? Bir oyun tasarlıyorum ve son amaç tüm yığınları aynı renkte olacak şekilde sıralamak.

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

Ben stacks[1][1]böyle bir "B" eklemek istiyorum stacks[1] = ["-", "B", "Y", "Y"]. Bunun için gereken minimum hareket sayısını nasıl belirleyebilirim?

Birden fazla yaklaşıma baktım, bir durumdan tüm olası hareketleri üreten, onları puanlayan ve daha sonra en iyi puanlama yollarını devam ettiren genetik algoritmaları denedim, ayrıca problem üzerinde yol bulma için Djikstra'nın algoritmasını çalıştırmaya çalıştım . Sinir bozucu bir şekilde basit görünüyor, ancak üstel zamandan başka bir şeyde çalıştırılmasını sağlayacak bir yol bulamıyorum. Burada uygulanabilecek bir algoritma var mı?

Düzenle

Gerekli minimum hareket sayısını hesaplamak için bu işlevi yazdım: yığınlar: Yığındaki parçaları temsil eden Karakterler Listesi, yığınlar [0] [0] yığının üst kısmı [0] stack_ind: parçasının needs_piece öğesine ekleneceği yığın: Stack'e eklenecek parça needs_index: Parçanın yerleştirilmesi gereken dizin

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")

Düzenleme: Yığınlardaki Test Durumları:

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

Gerçek kod uygulaması zor olan kısım değil, mücadele ettiğim problemi çözen bir algoritmanın nasıl uygulanacağını belirliyor.

YonIif isteği @ gereğince bir oluşturduk özünü sorun.

Çalıştığında, rastgele bir yığın dizisi oluşturur ve rastgele bir konuma rastgele bir yığına yerleştirilmesi gereken rastgele bir parça seçer.

Çalıştırılması konsolda bu formatta bir şey yazdırır.

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

Durum güncelleme

Bu sorunu bir şekilde çözmeye çok kararlıyım .

Yorumlarda bahsedilen @Hans Olsson gibi davaların sayısını en aza indirmenin bir yolu olduğunu unutmayın. Bu soruna en son yaklaşımım, bahsedilenlere benzer bir dizi kural geliştirmek ve bunları nesilsel bir algoritmada kullanmaktı.

Gibi kurallar:

Bir hareketi asla tersine çevirme. 1-> 0'dan sonra 0-> 1'den gidin (Anlamsız)

Bir parçayı arka arkaya iki kez hareket ettirmeyin. Asla 0 -> 1 sonra 1 -> 3

Yığınlar [X] 'dan yığınlara [Y] bir miktar hareket verildiğinde, bir miktar hamle, daha sonra yığınlar [Y]' dan yığınlar [Z] 'a bir hareket verildiğinde, eğer yığınlar [Z] hareket sırasındaki durumdaysa yığınlardan [X] yığınlara [Y] gerçekleşti, yığınlardan [X] doğrudan yığınlara [Z] geçilerek bir hareket ortadan kaldırılabilirdi

Şu anda, bu soruna bir kuşak algoritması kullanılarak hesaplanabilecek kadar "geçerli" hareket sayısını en aza indirecek yeterli kurallar yaratma çabasıyla yaklaşıyorum. Birisi ek kurallar düşünebilirse, yorumlarda duymak isterim.

Güncelleme

@RootTwo tarafından verilen cevap sayesinde burada özetleyeceğim bir dönüm noktası yaşadım.

Atılım üzerine

Hedef yüksekliğini, hedef parçasının hedef yığınına yerleştirilmesi gereken derinlik olarak tanımlayın.

<= Stack_height - goal height dizinine bir hedef parçası yerleştirildiğinde, clear_path () yöntemi ile her zaman zafere giden en kısa yol olacaktır.

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

Öyle bir yığın verilirse stack[0] = R, oyun kazanılır.

                       GOAL
[ [ (S | -), (S | -), (S | -) ], [R, S, S], [(S | - ), (S | -), (S | -)] ]

Her zaman en az stack_height boş alan olduğu bilindiği için, mümkün olan en kötü durum şöyle olur:

 [ [ S, S, !Goal ], [R, S, S], [-, -, -]

Gol parçasının gol hedefinde olamayacağını bildiğimiz için oyun kazanılır. Bu durumda gereken minimum hamle sayısı hamle olacaktır:

(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

Öyle bir yığın verilirse stack[1] = R, oyun kazanılır.

              GOAL
[ [ (S | -), (S | -), S], [ (S | -), R, S], [(S | -), (S | -), (S | -)]

En az 3 boş alan olduğunu biliyoruz, bu yüzden mümkün olan en kötü durum şöyle olacaktır:

[ [ S, !Goal, S], [S, R, S], [ -, -, - ]

Bu durumda minimum hamle sayısı hamle olacaktır:

(1, 2), (0, 2), (0, 2), (1, 0)

Bu, tüm durumlar için geçerli olacaktır.

Böylece, sorun, hedef parçayı hedef yüksekliğine veya üstüne yerleştirmek için gereken minimum hareket sayısını bulma sorununa indirgenmiştir.

Bu, sorunu bir dizi alt soruna böler:

  1. Hedef yığının erişilebilir parçası! = Hedef parçası olduğunda, o parça için geçerli bir yer olup olmadığını veya başka bir parça değiştirilirken parçanın orada kalması gerekip gerekmediğini belirleme.

  2. Hedef yığının erişilebilir parçası == hedef parçası olduğunda, kaldırılıp gereken hedef yüksekliğine yerleştirilip yerleştirilemeyeceğini veya bir başkası takas edilirken parçanın kalması gerekip gerekmediğini belirleme.

  3. Yukarıdaki iki durum başka bir parçanın değiştirilmesini gerektirdiğinde, hedef parçanın hedef yüksekliğine ulaşmasını sağlamak için hangi parçaların değiştirileceğini belirlemek için.

Hedef yığını her zaman önce durumlarını değerlendirmelidir.

IE

stacks = [ [-, R, G], [-, R, G], [-, R, G] ]

Goal = stacks[0][1] = G

Önce Hedef Yığını kontrol edilir:

(0, 1), (0, 2), (1, 0), (2, 0) = 4 Moves

Hedef Yığını Yok Sayma:

(1, 0), (1, 2), (0, 1), (0, 1), (2, 0) = 5 Moves

2
A * ' yı denediniz mi? Dijkstra'nın algoritmasına oldukça benziyor, ancak bazen çok daha hızlı.
Yonlif

1
Bir github repo bağlantısını paylaşabilir misiniz? Tamamsa kendimi denemek istiyorum. @Tristen
Yonlif

1
İlk bakıştan sonra bu problem NP zor görünüyor. Muhtemelen NP içinde değildir (NP-tam değil), çünkü size en uygun çözümü sunsam bile, bunu kolayca doğrulayamazsınız. Bu, permütasyonlardaki optimizasyon problemleri için kötü şöhretlidir. Sorunu CS'de paylaşmanızı öneririm . Bu sorun için yaklaşık algoritmalara bakın. Bu oldukça zor bir problem ama iyi bir yaklaşım olmalı. Benzer: Hanoi'nin Keyfi Kuleleri
DarioHett

1
@DarioHett Bu konuda endişelendim! Parmaklarımın bir NP-Hard problemi olmayacağını düşündüm, ama aynı zamanda bir sorun olabileceğini düşündüm. Genetik bir algoritma ve hareketleri puanlayan bazı özel puanlama fonksiyonları ile daha iyi şanslar yaşıyorum. Hanoi'nin Keyfi Kulelerine bir göz atacağım! Önerin için teşekkürler.
Tristen

1
Eğer bulmacayı rastgele oluşturmaya çalışırsanız - açıkça gereksiz hareketleri kaldırmayı unutmayın (ileri bir hareketten sonra bir şeyi geri hareket ettirmek veya birinin yeterli olacağı iki adımda bir hareket yapmak; ve ayrıca muhtemelen ilgisiz karışık hareketlerle birlikte).
Hans Olsson

Yanıtlar:


1

İki seçenek buldum, ancak hiçbiri dava 2'yi zamanında çözemedi. İlk seçenek h (n) olarak dize mesafe ölçüsü ile A * kullanmak, ikinci seçenek IDA *. Birçok dize benzerlik ölçüsünü test ettim, yaklaşımımda smith-waterman kullandım. Sorunu daha hızlı ele almak için gösterimi değiştirdim. Bir parçanın iki kez hareket edip etmediğini kontrol etmek için her basamağın sonuna sayılar ekledim.

İşte test ettiğim durumlar:

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

İşte A * kodu:

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

İşte IDA * Kodu:

#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

Kullanımı:

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

Yorumlarda P kapasiteli N yığınları olduğunu ve her zaman P boş alanları olduğunu söylediniz. Bu durumda, bu algoritma elsekodunuzdaki maddede (örn. Ne zaman num_removals + min_to_unlock > num_free_spaces) çalışacak gibi görünüyor :

  1. İstifin üst kısmına en yakın olan parçayı bulun.
  2. Tüm parçaları, üzerinde boş bir alanı olan bir yığın (hedef yığın değil) olacak şekilde istenen parçanın üstünden hareket ettirin. Gerekirse, parçaları hedef yığından veya başka bir yığından alın. Tek açık alan hedef yığınının üstündeyse, başka bir yığının üstünü açmak için oraya bir parça taşıyın. Bu her zaman mümkündür, çünkü istenen açıklıktan yukarı doğru hareket etmek için P açık alanlar ve en fazla P-1 parçaları vardır.
  3. İstediğiniz parçayı yığının üstündeki boş noktaya taşıyın.
  4. Hedef açılana kadar parçaları hedef yığından taşıyın.
  5. İstediğiniz parçayı hedefe taşıyın.

Geçtiğimiz birkaç saati bu cevabı bulmak için harcadım ve sanırım orada bir şeyler olabilir. Mümkünse, istediğiniz parçanın üzerindeki parçaları nasıl taşıyacağınız hakkında biraz daha bilgi verebilir misiniz? Hangi yığınları taşıyacağınızı nasıl belirlersiniz? Belki biraz psuedocode / kod. Bu kesinlikle bunu çözmek için hissettiğim en yakın şey.
Tristen

0

Bunu matematiksel olarak kanıtlamak için zaman bulamamam da yine de göndermeye karar verdim; Umarım yardımcı olur. Yaklaşım, iyi hamlelerle azalan ve oyun bittiğinde tam olarak sıfıra ulaşan bir p parametresi tanımlamaktır. Programda sadece iyi hareketleri veya nötr hareketleri (p'yi değiştirmeden bırakan) dikkate alır ve kötü hareketleri unutur (p'yi artırır).

Peki p nedir? Her sütun için p'yi, o sütundaki tüm renkler istenen renk olmadan önce kaldırılması gereken blok sayısı olarak tanımlayın. Öyleyse kırmızı blokların en soldaki sütunda kalmasını istiyoruz (daha sonra geri döneceğim) ve altta bir kırmızı blok, sonra bunun üstünde bir sarı, üst kısmında bir blok daha olduğunu varsayalım ve ardından boş bir alan. Sonra bu sütun için p = 2 (tümü kırmızıdan önce kaldırılacak iki blok). Tüm sütunlar için p hesaplayın. Boş olması gereken sütun için p, içindeki blok sayısına eşittir (hepsi gitmelidir). Geçerli durum için P, tüm sütunlar için tüm p'lerin toplamıdır.

P = 0 olduğunda, tüm sütunlar aynı renge sahiptir ve bir sütun boştur, bu nedenle oyun bitmiştir.

P'yi azaltan (veya en azından p'yi arttırmayan) hareketleri seçerek doğru yönde ilerliyoruz, bence bu en kısa yol algoritmaları ile önemli fark: Dijkstra'nın her biri ile doğru yönde hareket edip etmediği hakkında hiçbir fikri yoktu tepe noktasını araştırıyordu.

Peki her rengin nereye ulaşması gerektiğini nasıl belirleriz? Temel olarak her olasılık için p belirleyerek. Yani örneğin kırmızı / sarı / yeşil / boş ile başlayın, p hesaplayın, sonra kırmızı / sarı / boş / yeşil, p hesaplayın vb. İle başlayın. Başlangıç ​​konumunu en düşük p ile alın. Bu n sürer! hesaplamaları. N = 8 için bu yapılabilir 40320'dir. Kötü haber, tüm başlangıç ​​pozisyonlarını eşit en düşük p ile incelemeniz gerektiğidir. İyi haber, gerisini unutabiliyor olmanız.

Burada iki matematiksel belirsizlik var. Bir: Kötü bir hamle kullanan daha kısa bir yol olması mümkün mü? Olası görünmüyor, bir karşı örnek bulamadım, ancak bir kanıt da bulamadım. İkincisi: optimal olmayan bir başlangıç ​​pozisyonuyla (yani en düşük p değil) başlarken, tüm optimal başlangıç ​​pozisyonlarından daha kısa bir yol olması mümkündür. Yine: Karşı örnek yok ama kanıt da yok.

Bazı uygulama önerileri. Her sütun için yürütme sırasında p'yi takip etmek zor değildir, ama elbette yapılmalıdır. Her sütun için tutulması gereken bir diğer parametre açık noktaların sayısıdır. 0 ise, bu sütunlar herhangi bir bloğu geçici olarak kabul edemez, bu nedenle döngü dışında bırakılabilir. Bir sütun için p = 0 olduğunda, bir pop için uygun değildir. Her olası pop için, iyi bir hareket olup olmadığını, yani genel p'yi azaltan bir hareket olup olmadığını inceleyin. Birden fazla varsa, hepsini inceleyin. Eğer yoksa, tüm nötr hareketleri düşünün.

Tüm bunlar hesaplama sürenizi büyük ölçüde azaltmalıdır.


1
Sanırım soruyu yanlış anladınız! Bu sorunun arkasında yatan motivasyon olmasına rağmen. Soru, tek bir parçayı tek bir konuma taşımak için minimum hareket sayısını bulmaktır. Soru, istifin ardındaki motivasyon olmasına rağmen, yığınları sıralamak için minimum hareket sayısını bulmak değildi. Ancak, bu P puanlaması ile yanlış olur. İlk başta P'yi artıran ve daha sonra daha hızlı bir oranda azaltan "kötü hareketlerin" olduğu birçok durum vardır. Bununla birlikte, cevabınızın hiçbir ilgisi olmadığı için soruyu belki de tekrar okuyun.
Tristen

1
Özür dilerim Tristen, gerçekten soruyu dikkatlice okumadım. Matematiksel yönü beni çok etkiledi ve partiye geç kaldığım için çok hızlı cevap verdim. Bir dahaki sefere daha dikkatli olacağım. Umarım bir cevap bulursunuz.
Paul Rene
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.