Bagaimana menemukan jumlah minimum gerakan untuk memindahkan item ke posisi dalam tumpukan?

12

Tumpukan

Diberikan satu set tumpukan NXP dengan N menjadi jumlah tumpukan, dan P menjadi kapasitas tumpukan, bagaimana saya bisa menghitung jumlah minimum swap yang diperlukan untuk berpindah dari beberapa simpul di lokasi A ke beberapa lokasi acak B? Saya merancang sebuah game, dan tujuan akhirnya adalah menyortir semua tumpukan agar warnanya sama.

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

Jika saya ingin memasukkan "B" di tempat stacks[1][1]seperti itu stacks[1] = ["-", "B", "Y", "Y"]. Bagaimana saya bisa menentukan jumlah minimum gerakan yang diperlukan untuk melakukannya?

Saya telah melihat beberapa pendekatan, saya telah mencoba algoritma genetika yang menghasilkan semua kemungkinan pergerakan dari keadaan, memberi skor, dan kemudian melanjutkan jalur skor terbaik, saya juga mencoba menjalankan algoritma Djikstra untuk merintis masalah. . Tampaknya sangat sederhana, namun saya tidak tahu cara untuk menjalankannya selain dari waktu eksponensial. Apakah ada algoritma yang saya lewatkan yang dapat diterapkan di sini?

Edit

Saya telah menulis fungsi ini untuk menghitung jumlah minimum gerakan yang diperlukan: tumpukan: Daftar Daftar Karakter yang mewakili potongan-potongan dalam tumpukan, tumpukan [0] [0] adalah bagian atas tumpukan [0] stack_ind: Indeks dari tumpukan potongan yang akan ditambahkan ke needs_piece: Potongan yang harus ditambahkan ke tumpukan needs_index: Indeks di mana potongan harus ditempatkan

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

Sunting: Test Case pada tumpukan:

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

Implementasi kode yang sebenarnya bukan bagian yang sulit, itu menentukan cara mengimplementasikan algoritma yang memecahkan masalah yang saya perjuangkan.

Sesuai permintaan @ YonIif, saya telah membuat inti untuk masalah ini.

Ketika dijalankan, ini menghasilkan susunan acak tumpukan, dan memilih potongan acak yang perlu dimasukkan ke dalam tumpukan acak di lokasi acak.

Menjalankannya mencetak sesuatu dari format ini ke konsol.

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

Pembaruan status

Saya sangat bertekad untuk menyelesaikan masalah ini entah bagaimana .

Perlu diingat bahwa ada cara untuk meminimalkan jumlah kasus, seperti yang @Hans Olsson disebutkan dalam komentar. Pendekatan terbaru saya untuk masalah ini, adalah mengembangkan seperangkat aturan yang mirip dengan yang disebutkan, dan mempekerjakan mereka dalam algoritma generasi.

Aturan seperti:

Jangan pernah membalikkan gerakan. Pergi dari 1-> 0 lalu 0-> 1 (Tidak masuk akal)

Jangan pernah memindahkan sepotong dua kali berturut-turut. Jangan Pindah dari 0 -> 1 lalu 1 -> 3

Diberikan beberapa perpindahan dari tumpukan [X] ke tumpukan [Y], kemudian beberapa perpindahan, kemudian perpindahan dari tumpukan [Y] ke tumpukan [Z], jika tumpukan [Z] berada dalam keadaan yang sama seperti ketika perpindahan dari tumpukan [X] ke tumpukan [Y] terjadi, suatu langkah bisa dihilangkan dengan pindah dari tumpukan [X] langsung ke tumpukan [Z]

Saat ini, saya mendekati masalah ini dengan upaya untuk membuat aturan yang cukup, sehingga meminimalkan jumlah gerakan "valid", cukup sehingga jawaban dapat dihitung menggunakan algoritma generasi. Jika ada yang bisa memikirkan aturan tambahan, saya akan tertarik mendengarnya di komentar.

Memperbarui

Berkat jawabannya oleh @RootTwo, saya sudah memiliki sedikit terobosan, yang akan saya uraikan di sini.

Ke terobosan

Tentukan tinggi gawang sebagai kedalaman potongan gawang harus ditempatkan di tumpukan tujuan.

Setiap kali bagian gawang ditempatkan pada indeks <= stack_height - tinggi gawang, akan selalu ada jalan terpendek menuju kemenangan melalui metode clear_path ().

Let S represent some solid Piece.

YAITU

Stacks = [ [R, R, G], [G, G, R], [-, -, -] ]
Goal = Stacks[0][2] = R
Goal Height = 2.
Stack Height - Goal Height = 0

Diberikan beberapa tumpukan sehingga stack[0] = R, permainan dimenangkan.

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

Karena diketahui bahwa mereka selalu setidaknya ruang kosong stack_height tersedia, kasus terburuk yang mungkin terjadi adalah:

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

Karena kita tahu bagian gawang tidak bisa berada di tujuan gawang atau permainan dimenangkan. Dalam hal ini jumlah gerakan minimum yang diperlukan adalah gerakan:

(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

Diberikan beberapa tumpukan sehingga stack[1] = R, permainan dimenangkan.

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

Kami tahu setidaknya ada 3 ruang kosong yang tersedia, jadi kasus terburuk yang mungkin terjadi adalah:

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

Dalam hal ini jumlah gerakan minimum adalah gerakan:

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

Ini akan berlaku untuk semua kasus.

Dengan demikian, masalahnya telah direduksi menjadi masalah menemukan jumlah minimum gerakan yang diperlukan untuk menempatkan potongan gawang pada atau di atas pada ketinggian gawang.

Ini membagi masalah menjadi serangkaian sub-masalah:

  1. Ketika tumpukan tujuan memiliki bagian yang dapat diakses! = Bagian tujuan, menentukan apakah ada lokasi yang valid untuk potongan itu, atau apakah potongan itu harus tetap di sana saat potongan lain ditukar.

  2. Ketika tumpukan tujuan memiliki bagian yang dapat diakses == bagian tujuan, menentukan apakah itu dapat dihapus dan ditempatkan pada ketinggian tujuan yang diperlukan, atau jika potongan harus tetap sementara yang lain ditukar.

  3. Ketika kedua kasus di atas membutuhkan bagian yang lain untuk ditukar, tentukan bagian mana yang akan ditukar untuk meningkatkan agar bagian tujuan dapat mencapai ketinggian tujuan.

Tumpukan tujuan harus selalu dievaluasi kasusnya terlebih dahulu.

YAITU

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

Goal = stacks[0][1] = G

Memeriksa Goal Stack pertama-tama mengarah ke:

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

Mengabaikan Sasaran Sasaran:

(1, 0), (1, 2), (0, 1), (0, 1), (2, 0) = 5 Moves
Tristen
sumber
2
Sudahkah Anda mencoba A * ? Ini cukup mirip dengan algoritma Dijkstra tetapi kadang-kadang jauh lebih cepat.
Yonlif
1
Bisakah Anda berbagi tautan repo github? Saya ingin mencoba sendiri jika tidak apa-apa. @ Kristen
Yonlif
1
Setelah melihat pertama kali, masalah ini tampaknya NP-hard. Mungkin tidak dalam NP (bukan NP-complete), karena bahkan jika saya memberi Anda solusi optimal, Anda bahkan tidak dapat memverifikasinya dengan mudah. Ini terkenal karena masalah optimasi permutasi. Saya sarankan untuk mengirim silang masalah di CS . Lihatlah algoritma perkiraan untuk masalah ini. Ini adalah masalah yang cukup sulit tetapi perkiraan yang layak harus ada. Ini serupa: Menara Sewenang-wenang Hanoi
DarioHett
1
@DarioHett Itulah yang saya khawatirkan! Saya memiliki jari saya bersilang bahwa itu tidak akan berakhir menjadi masalah NP-Hard, tetapi saya juga punya firasat itu mungkin salah satu. Saya telah memiliki keberuntungan yang lebih baik dengan algoritma genetika, dan juga beberapa fungsi penilaian khusus yang memberi skor pada pergerakan. Saya akan melihat Menara Arbitrase di Hanoi! Terima kasih untuk sarannya.
Tristen
1
Jika Anda mencoba membuat teka-teki secara acak - ingatlah untuk menghapus gerakan yang jelas-jelas berlebihan (memindahkan sesuatu kembali setelah gerakan maju atau melakukan gerakan dalam dua langkah ketika seseorang sudah cukup; dan juga dalam kombinasi dengan gerakan yang mungkin tidak terkait yang tercampur).
Hans Olsson

Jawaban:

1

Saya datang dengan dua opsi, tetapi tidak ada yang bisa menyelesaikan kasus 2 secara tepat waktu. Opsi pertama menggunakan A * dengan ukuran jarak string sebagai h (n), opsi kedua adalah IDA *. Saya menguji banyak ukuran kesamaan string, saya menggunakan smith-waterman pada pendekatan saya. Saya telah mengubah notasi Anda untuk menangani masalah lebih cepat. Saya telah menambahkan angka pada akhir setiap digit untuk memeriksa apakah sebuah keping dipindahkan dua kali.

Berikut adalah beberapa kasus yang telah saya uji:

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

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

Inilah IDA * Code:

#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

Pemakaian:

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)
Victor Silva
sumber
0

Dalam komentar Anda mengatakan ada N tumpukan dengan kapasitas P, dan selalu ada P ruang kosong. Jika itu masalahnya, sepertinya algoritma ini akan berfungsi pada elseklausa dalam kode Anda (yaitu kapan num_removals + min_to_unlock > num_free_spaces):

  1. Temukan bagian yang diinginkan yang paling dekat dengan bagian atas tumpukan.
  2. Pindahkan semua potongan dari atas potongan yang diinginkan sedemikian rupa sehingga ada satu tumpukan (bukan tumpukan tujuan) yang memiliki ruang kosong di atasnya. Jika perlu, pindahkan potongan dari tumpukan tujuan atau tumpukan lain. Jika satu-satunya ruang terbuka adalah bagian atas tumpukan tujuan, pindahkan potongan di sana untuk membuka bagian atas tumpukan lain. Ini selalu mungkin, karena ada ruang terbuka P dan paling banyak potongan P-1 untuk bergerak dari atas potongan yang diinginkan.
  3. Pindahkan potongan yang diinginkan ke tempat kosong di atas tumpukan.
  4. Pindahkan potongan-potongan dari tumpukan tujuan hingga tujuan terbuka.
  5. Pindahkan karya yang diinginkan ke tujuan.
RootTwo
sumber
Saya telah menghabiskan beberapa jam terakhir menggali jawaban ini, dan saya pikir mungkin ada sesuatu di sana. Jika memungkinkan, dapatkah Anda memberikan sedikit informasi tentang bagaimana cara Anda memindahkan bagian yang berada di atas bagian yang diinginkan? Bagaimana Anda menentukan tumpukan mana untuk dipindahkan? Mungkin sedikit kode psuedocode /. Ini jelas merupakan yang paling dekat yang saya rasakan untuk menyelesaikan masalah sejauh ini.
Tristen
0

Meskipun saya belum menemukan waktu untuk membuktikan ini secara matematis, saya memutuskan untuk tetap memposting ini; semoga membantu. Pendekatannya adalah menentukan parameter p yang berkurang dengan gerakan yang baik dan mencapai nol tepat saat game selesai. Dalam program ini hanya mempertimbangkan gerakan yang baik atau gerakan netral (yang membuat p tidak berubah) dan melupakan gerakan buruk (yang meningkatkan p).

Jadi apa itu p? Untuk setiap kolom, tentukan p sebagai jumlah blok yang masih harus dihapus sebelum semua warna dalam kolom itu adalah warna yang diinginkan. Jadi misalkan kita ingin blok merah berakhir di kolom paling kiri (saya akan kembali lagi nanti), dan misalkan ada satu blok merah di bagian bawah, lalu kuning di atasnya, satu blok lagi di atas itu, dan kemudian ruang kosong. Kemudian p = 2 untuk kolom ini (dua blok untuk dihapus sebelum semuanya berwarna merah). Hitung p untuk semua kolom. Untuk kolom yang seharusnya berakhir kosong, p sama dengan jumlah blok yang ada di dalamnya (semuanya harus pergi). P untuk kondisi saat ini adalah jumlah dari semua p untuk semua kolom.

Ketika p = 0, semua kolom memiliki warna yang sama dan satu kolom kosong, sehingga permainan telah selesai.

Dengan memilih gerakan yang mengurangi p (atau setidaknya tidak menambah p) kita bergerak ke arah yang benar, ini menurut saya perbedaan penting dengan algoritma jalur terpendek: Dijkstra tidak tahu apakah dia bergerak ke arah yang benar dengan setiap vertex yang dia selidiki.

Jadi bagaimana kita menentukan di mana masing-masing warna harus berakhir? Pada dasarnya dengan menentukan p untuk setiap kemungkinan. Jadi misalnya mulai dengan merah / kuning / hijau / kosong, hitung p, lalu pergi ke merah / kuning / kosong / hijau, hitung p, dll. Ambil posisi awal dengan p terendah. Ini membutuhkan n! perhitungan. Untuk n = 8 ini adalah 40320, yang bisa dilakukan. Berita buruknya adalah Anda harus memeriksa semua posisi awal dengan p terendah yang sama. Berita baiknya adalah Anda bisa melupakan sisanya.

Ada dua ketidakpastian matematis di sini. Satu: apakah mungkin ada jalan yang lebih pendek yang menggunakan langkah buruk? Tampaknya tidak mungkin, saya belum menemukan contoh tandingan, tapi saya juga belum menemukan bukti. Dua: apakah mungkin ketika memulai dengan posisi awal yang tidak optimal (Yaitu bukan p terendah) akan ada jalan yang lebih pendek daripada dengan semua posisi awal yang optimal. Sekali lagi: tidak ada contoh balik tetapi juga tidak ada bukti.

Beberapa saran implementasi. Melacak p selama eksekusi untuk setiap kolom tidak sulit tetapi tentu saja harus dilakukan. Parameter lain yang harus disimpan untuk setiap kolom adalah jumlah tempat terbuka. Jika 0, maka kolom ini untuk sementara waktu tidak dapat menerima blok apa pun, sehingga dapat diabaikan. Ketika p = 0 untuk kolom, itu tidak memenuhi syarat untuk pop. Untuk setiap kemungkinan pop, periksa apakah ada gerakan yang baik, yaitu yang mengurangi keseluruhan p. Jika ada banyak, periksa semua. Jika tidak ada, pertimbangkan semua gerakan netral.

Semua ini akan sangat mengurangi waktu perhitungan Anda.

Paul Rene
sumber
1
Saya pikir Anda salah paham pertanyaannya! Meskipun ini adalah motivasi di balik pertanyaan itu. Pertanyaannya adalah untuk menemukan jumlah minimum gerakan untuk memindahkan satu bagian, ke satu lokasi. Pertanyaannya bukan untuk menemukan jumlah minimum gerakan untuk menyortir tumpukan, meskipun itu adalah motivasi di balik pertanyaan. Namun, dengan skor P itu, Anda akan salah. Ada banyak contoh di mana ada "gerakan buruk" yang pada akhirnya meningkatkan P pada awalnya, dan kemudian menguranginya dengan kecepatan yang lebih cepat. Dengan itu, mungkin baca kembali pertanyaan karena jawaban Anda tidak memiliki relevansi.
Tristen
1
Permintaan maaf saya Tristen, saya memang tidak membaca pertanyaan dengan hati-hati. Saya terpesona oleh aspek matematika dan, karena terlambat ke pesta, terlalu cepat untuk menjawab. Saya akan lebih berhati-hati lain kali. Semoga Anda menemukan jawabannya.
Paul Rene