[PYTHON] Schreiben wir ein Programm zur Lösung des Rubik-Würfels (Teil 2: IDA * -Suche)

Einführung

Dieser Artikel ist der 17. Tagesartikel von Speedcubing Adventskalender 2019. [Serie] Schreiben wir ein Programm zur Lösung des Rubik-Würfels (Teil 1) Wir werden fortfahren.

Im ersten Teil habe ich eine Klasse erstellt, die den Status / die Operation des Rubik-Würfels ausdrückt, und eine Methode zum Bedienen des Status geschrieben. Nachdem Sie den Würfel programmgesteuert drehen können, fahren wir mit der Suche fort.

Im zweiten Teil werden wir Round-Robin suchen, damit wir die Lösung des Rubik-Würfels noch in keinem Zustand finden können, aber auch kleine Rätsel wie Pyraminx und 2x2x2-Würfel können auf diese Weise gelöst werden.

Umgebung

In Bezug auf die Umgebung entspricht Python 3.6 oder höher dem ersten Teil.

Fortsetzung des ersten Teils eine Colaboratory-Notebook-Version des unten angezeigten Codes Ich habe mich auch vorbereitet. Wenn Sie also auf "[In Play Ground öffnen]" klicken und während der Ausführung fortfahren, müssen Sie keine Umgebung erstellen, und es wird einfacher.

Löse den Zauberwürfel mit einem Kreisverkehr

Der Rubik-Würfel hat ungefähr 4,3 * 10 ^ 19 Zustände, und die kürzeste Anzahl von Zügen beträgt durchschnittlich ungefähr 18. Es ist bekannt, dass es im schlimmsten Fall in 20 Händen abgeschlossen ist (vgl. Https://www.cube20.org/).

Round-Robin ist eine Skala, die in einer realistischen Zeit nicht erreicht werden kann. Schreiben wir jedoch zunächst ein Programm, um den Rubik-Würfel mit Round-Robin zu lösen und ihn basierend darauf zu verbessern.

Aufgrund der großen Anzahl von Status im Rubik Cube verbraucht die Suche nach Breitenpriorität zu viel Speicher. Um den Rubik-Würfel im Round-Robin-Verfahren zu lösen, [Wiederholung der Suche nach vertiefender Tiefenpriorität](https://ja.wikipedia.org/wiki/%E5%8F%8D%E5%BE%A9%E6%B7%B1% E5% 8C% 96% E6% B7% B1% E3% 81% 95% E5% 84% AA% E5% 85% 88% E6% 8E% A2% E7% B4% A2) kann verwendet werden. Dies ist eine Methode zum Wiederholen einer Suche mit Tiefenpriorität (Suche nach Tiefenbeschränkungen), die die Suchtiefe (Lösungslänge) begrenzt, indem die Grenztiefe schrittweise erhöht wird.

Funktion zur Beurteilung des Abschlusszustands

Zuerst muss ich eine Funktion schreiben, um zu beurteilen, ob der Status, der im Suchprozess angezeigt wird, ein abgeschlossener Status ist. Wenn alle EO, CO, EP und CP vollständig sind, bedeutet dies, dass das Produkt vollständig ist. Daher habe ich eine Funktion geschrieben, um dies zu bestimmen.

def is_solved(state):
    return (state.eo == [0] * 12  #Alle EOs sind verfügbar
            and state.co == [0] * 8  #Alle CO ist verfügbar
            and state.ep == list(range(12))  #Alle EPs sind verfügbar
            and state.cp == list(range(8))  #Alle CPs sind verfügbar
            )

Eine Funktion, die zurückgibt, ob die Operation als nächste Bewegung verwendet werden kann

Schreiben Sie eine Funktion, die sich auf den vorherigen Zug bezieht und zurückgibt, ob der Vorgang als nächster Zug verwendet werden kann.

In einem einfachen Fall ist es nicht möglich, dieselbe Oberfläche kontinuierlich zu drehen. (Wie "R'R2") Außerdem ist "U D U" dasselbe wie "U2 D", also ist es schlecht. Um dies zu vermeiden, legen Sie beim kontinuierlichen Drehen von Angesicht zu Angesicht die Reihenfolge in der Wörterbuchreihenfolge der Gesichtsnamen fest (wie die Reihenfolge "D U" ist gut, die Reihenfolge "U D" ist jedoch nicht möglich).

inv_face = {
    "U": "D",
    "D": "U",
    "L": "R",
    "R": "L",
    "F": "B",
    "B": "F"
}

def is_move_available(prev_move, move):
    """
Bestimmen Sie, ob die Operation als nächster Zug verwendet werden kann, indem Sie den vorherigen Zug berücksichtigen
    -Drehen Sie nicht die gleiche Oberfläche kontinuierlich(e.g. R'R2 ist nicht möglich)
    -Korrigieren Sie die Reihenfolge beim Drehen von Angesicht zu Angesicht(e.g.D U ist gut, U D aber nicht)
    """
    if prev_move is None:
        return True  #Für den ersten Zug ist jede Operation möglich
    prev_face = prev_move[0]  #Die Oberfläche drehte sich zur Seite
    if prev_face == move[0]:
        return False #Gleiche Seite ist nicht möglich
    if inv_face[prev_face] == move[0]:
        return prev_face < move[0] #Von Angesicht zu Angesicht ist eine lexikalische Reihenfolge akzeptabel
    return True

Implementierung einer iterativen Suche nach vertiefender Tiefenpriorität

Ich habe es so geschrieben. In start_search beginnt es bei 0 Zügen, ruft die Implementierung der Tiefenbegrenzungssuche auf, während die Anzahl der Züge schrittweise erhöht wird, und gibt sie zurück, wenn eine Lösung gefunden wird. Das Argument "Tiefe" von "Tiefe_begrenzte_Suche" stellt die verbleibende Tiefe dar, und während diese verringert wird, wird die Suche rekursiv fortgesetzt, so dass bei Tiefe = 0 die Anzahl der aktuell durchsuchten Schritte erreicht wird.

Bereiten Sie einen Stapel vor, um die Lösung (Prozedur) zu verwalten, über die Sie nachdenken. Wenn Sie die Operation beim Eingeben des nächsten Knotens während des Suchvorgangs drücken und beim Beenden einfügen, befindet sich die aktuell gesuchte Lösung immer in diesem Stapel, und es ist nicht erforderlich, für jeden Status eine Lösung zu haben.

class Search:
    def __init__(self):
        self.current_solution = []  #Stapeln Sie das gesuchte Verfahren

    def depth_limited_search(self, state, depth):
        if depth == 0 and is_solved(state):
            # is_Wenn gelöst True ist, ist es vollständig und gibt True zurück, da die Lösung gefunden wurde.
            # depth==0 ist nicht erforderlich, aber Tiefe>Sie müssen die Entscheidungsfunktion nicht jedes Mal aufrufen, da Sie bereits wissen, dass 0 keine Lösung hat
            return True
        if depth == 0:  #Gibt False zurück, wenn die Lösung nach Erreichen der Tiefe 0 nicht gefunden wird
            return False

        # depth=Suche weiter bis 0
        prev_move = self.current_solution[-1] if self.current_solution else None  #Bedienung vor
        for move_name in move_names:
            if not is_move_available(prev_move, move_name):
                continue
            self.current_solution.append(move_name)
            if self.depth_limited_search(state.apply_move(moves[move_name]), depth - 1):
                return True
            self.current_solution.pop()

    def start_search(self, state, max_length=20):
        for depth in range(0, max_length):  #Wiederholen Sie die Suche nach Tiefenbeschränkungen, während Sie die Anzahl der Schritte schrittweise von 0 erhöhen
            print(f"# Start searching length {depth}")
            if self.depth_limited_search(state, depth):
                #Wenn eine Lösung gefunden wird, geben Sie sie an dieser Stelle zurück
                return " ".join(self.current_solution)
        return None

Versuchen Sie, eine iterative Suche nach vertiefender Tiefenpriorität durchzuführen

In dieser Implementierung kann in einer realistischen Zeit keine Lösung für gewöhnliches zufälliges Scramble gefunden werden, daher werde ich ein geeignetes 6-Hand-Scramble geben. Die Lösung wurde in 34 Sekunden gefunden. Die Lösung besteht natürlich darin, das Scramble in die entgegengesetzte Richtung zu drehen.

import time
scramble = "R U R' F2 D2 L"
scrambled_state = scamble2state(scramble)
search = Search()
start = time.time()
solution = search.start_search(scrambled_state)
print(f"Finished! ({time.time() - start:.2f} sec.)")
if solution:
  print(f"Solution: {solution}.")
else:
  print("Solution not found.")
Finished! (34.11 sec.)
Solution: L' D2 F2 R U' R'.

Beschleunigen durch Beschneiden (IDA * -Suche)

Das Beschneiden ist eine Technik, um die Suche zu beschleunigen. Mit Ihren Vorkenntnissen des Rubik-Würfels können Sie immer mehr aufrunden, wenn Sie es für sinnlos halten, weiter zu erforschen. Diese Art der sachkundigen iterativen Suche nach vertiefender Tiefenpriorität wird als IDA * -Suche bezeichnet (vgl. Https://en.wikipedia.org/wiki/Iterative_deepening_A *).

Was ist zum Beispiel, wenn Sie zum Zeitpunkt von "Tiefe = 1" keine Teile haben (1 Bewegung nach links)? Versuchen Sie, den Zauberwürfel manuell aus dem fertigen Zustand zu verschieben. Wenn Sie mit einem Zug nach links nicht mindestens vier Ecken und acht Kanten haben, können Sie dies bei Ihrem nächsten Zug nicht mehr tun. Daher ist es sinnlos, mit der Suche nach dem nächsten Zug fortzufahren, "Tiefe = 0".

In ähnlicher Weise können bei "Tiefe = 2" (2 Züge nach links) alle Ecken unterbrochen werden (z. B. "LR"). Wenn Sie jedoch keine 4 Kanten haben, bleiben 2 Züge übrig. Es gibt keine Möglichkeit, dies zu tun.

Wenn Sie mit "Tiefe = 3" (3 Züge nach links) an diesem Punkt nicht mindestens 2 Kanten haben, können Sie nicht mit 3 Zügen nach links fertig werden.

Lassen Sie uns das Beschneiden mit diesem Wissen implementieren.

def count_solved_corners(state):
    """
Zählen Sie die Anzahl der ausgerichteten Ecken
    """
    return sum([state.cp[i] == i and state.co[i] == 0 for i in range(8)])


def count_solved_edges(state):
    """
Zählen Sie die Anzahl der ausgerichteten Kanten
    """
    return sum([state.ep[i] == i and state.eo[i] == 0 for i in range(12)])


def prune(depth, state):
    """
Gibt True zurück, wenn es keinen Sinn macht, weiterzumachen
    """
    if depth == 1 and (count_solved_corners(state) < 4 or count_solved_edges(state) < 8):
        #Wenn weniger als 4 Ecken mit der verbleibenden 1 Hand ausgerichtet sind oder wenn weniger als 8 ausgerichtete Kanten vorhanden sind, werden sie nicht mehr ausgerichtet
        return True
    if depth == 2 and count_solved_edges(state) < 4:
        #Wenn weniger als 4 Kanten mit den verbleibenden 2 Händen ausgerichtet sind, werden sie nicht mehr ausgerichtet
        return True
    if depth == 3 and count_solved_edges(state) < 2:
        #Wenn weniger als 2 Kanten mit den verbleibenden 3 Händen ausgerichtet sind, werden sie nicht mehr ausgerichtet
        return True
    return False
class Search:
    def __init__(self):
        self.current_solution = []

    def depth_limited_search(self, state, depth):
        if depth == 0 and is_solved(state):
            return True
        if depth == 0:
            return False

        #Beschneidung(Nur das Hinzufügen dieser beiden Zeilen hat sich geändert)
        if prune(depth, state):
            return False

        prev_move = self.current_solution[-1] if self.current_solution else None  #Bedienung vor
        for move_name in move_names:
            if not is_move_available(prev_move, move_name):
                continue
            self.current_solution.append(move_name)
            if self.depth_limited_search(state.apply_move(moves[move_name]), depth - 1):
                return True
            self.current_solution.pop()

    def start_search(self, state, max_length=20):
        for depth in range(0, max_length):
            print(f"# Start searching length {depth}")
            if self.depth_limited_search(state, depth):
                return " ".join(self.current_solution)
        return None

Erkundungszeit beim Beschneiden

Die Suche, die 34 Sekunden dauerte, dauert jetzt weniger als 1 Sekunde. Wie Sie sehen können, ist das Beschneiden sehr mächtig.

scramble = "R U R' F2 D2 L"
scrambled_state = scamble2state(scramble)
search = Search()
start = time.time()
solution = search.start_search(scrambled_state)
print(f"Finished! ({time.time() - start:.2f} sec.)")
if solution:
  print(f"Solution: {solution}.")
else:
  print("Solution not found.")
Finished! (0.21 sec.)
Solution: L' D2 F2 R U' R'.

Allein damit können kleine Rätsel wie Pyraminx- und 2x2x2-Würfel in realistischer Zeit gelöst werden. Einige Enthusiasten möchten vielleicht Teile des Rubik-Würfels erkunden (z. B. Schritte überqueren oder 2x2x3-Blöcke ausrichten), aber wenn es kurz ist, ist dies der richtige Weg. Wenn Sie die erste Lösung finden und so ändern, dass sie nicht sofort zurückgegeben wird, können Sie nach mehreren Arten von Lösungen suchen.

Nächster Hinweis: Zustandsindizierung + Zweiphasenalgorithmus

Ich habe ein Programm geschrieben, um den Rubik-Würfel auf Umwegen zu lösen, und ich konnte ihn durch Beschneiden ein wenig beschleunigen, aber es ist weit davon entfernt, in kurzer Zeit aus irgendeinem Zustand eine Lösung zu finden.

Zwei-Phasen-Algorithmus wird häufig als Algorithmus verwendet, um die suboptimale Lösung des Rubik-Würfels in einer realistischen Zeit zu finden. Es ist eine Methode, die Lösung des Rubik-Würfels in zwei Phasen zu zerlegen und diese jeweils im Round-Robin-Verfahren zu lösen.

In Phase 1 können Sie den Zauberwürfel frei bewegen und in einen Zustand bringen, in dem er nur mit der Bewegung von <U, D, R2, L2, F2, B2> abgeschlossen werden kann. In Phase2 werden wir nur die Bewegungen von <U, D, R2, L2, F2, B2> verwenden, um es zum Abschluss zu bringen.

Phase 1 sind die schlechtesten 12 Züge, ungefähr 10 Züge, Phase 2 kann im schlimmsten Fall in 18 Zügen abgeschlossen werden, etwa 13 bis 14 Züge (vgl. Http://kociemba.org/math/symmetric.htm).

Je tiefer die Suchtiefe (Prozedurlänge) ist, desto exponentieller nimmt der Suchbereich zu. Daher sind etwa 18 Schritte erforderlich, um ein Round-Robin-Verfahren ohne Aufteilung der Phase durchzuführen, während nach 10 und 13 Zügen gesucht wird. Es wird schneller sein, wenn Sie es in Suchen unterteilen.

Es ist wichtig, dass der Zwei-Phasen-Algorithmus ordnungsgemäß in zwei Phasen zerlegt und in zwei Schritte unterteilt wird, die weniger Aufwand erfordern. Es ist jedoch auch wichtig, dass die Anzahl der Zustände in jeder Phase durch Teilen der Phase in zwei erhöht wird. Die Anzahl der Zustände kann reduziert werden, der Zustand kann indiziert werden und eine Zustandsübergangstabelle kann im Voraus erstellt werden, und die Tabelle zum Beschneiden kann auch im Voraus berechnet werden. Bei der Implementierung des zweiten Teils wird bei jeder Suche eine Instanz der State-Klasse erstellt, und dieser Teil ist tatsächlich langsam. Sie können dies verbessern, indem Sie den Status indizieren.

Eine weitere Funktion ist, dass Sie der optimalen Lösung immer näher kommen können, wenn Sie sich die Zeit für die Suche nehmen. Die Implementierung des Zweiphasenalgorithmus wird im zweiten Teil belassen.

Verweise

Recommended Posts

Schreiben wir ein Programm zur Lösung des Rubik-Würfels (Teil 2: IDA * -Suche)
Schreiben Sie ein Programm, um den 4x4x4 Rubik Cube zu lösen! 2. Algorithmus
Schreiben Sie ein Programm, um den 4x4x4 Rubik Cube zu lösen! 3. Implementierung
Schreiben Sie ein Programm, um den 4x4x4 Rubik Cube zu lösen! 1. Übersicht
So erstellen Sie ein Programm zum Lösen des Rubik Cube ab PC Koshien 2014 Floppy Cube
Schreiben Sie ein Python-Programm, um die Bearbeitungsentfernung [Python] [Levenshtein-Entfernung] zu ermitteln.
Lassen Sie uns einen Roboter bauen, der den Zauberwürfel löst! 2 Algorithmus
Lassen Sie uns einen Roboter bauen, der den Zauberwürfel löst! 3 Software
Lassen Sie uns einen Roboter bauen, der den Zauberwürfel löst! 1. Übersicht
Schreiben wir ein einfaches Simulationsprogramm für das "Monty Hall Problem"
Ich habe versucht, ein Programm zu erstellen, um die Fehlersuche von Saiseriya zu lösen (Hinweis)
Schreiben Sie ein Programm, das das Programm missbraucht und 100 E-Mails sendet
Verschiedene Kommentare im Programm zu schreiben
Schreiben wir ein Python-Programm und führen es aus
So schreiben Sie eine GUI mit dem Befehl maya
Ich möchte in Python schreiben! (2) Schreiben wir einen Test
Ich habe versucht, Soma Cube mit Python zu lösen
Verwendung der Solver-Bibliothek "kociemba" von Rubik Cube
Ich wollte den AWS-Schlüssel nicht in das Programm schreiben
Lassen Sie uns von der Linie suchen
So starten Sie die erste Projektion
[Anmerkung] Versuchen wir, den Stromverbrauch vorherzusagen! (Teil 1)
Ich wollte das ABC164 A ~ D-Problem mit Python lösen
Schreiben Sie ein Skript, um die Entfernung mit dem Elasticsearch 5-System schmerzfrei zu berechnen
Lösen Sie Teilsummenprobleme mit der vollständigen Suche in Python
Lassen Sie uns das Computer-Go-Programm CgfGoBan so ändern, dass es mehrere Programmiersprachen unterstützt
[Python] Ein Programm, das den Inhalt der Liste nach links dreht