Labyrinth-Problem

Aus Informatik
Wechseln zu: Navigation, Suche

Aufgabenstellung

Erstellt werden soll ein Programm, das aus einem beliebigen Labyrinth einen Ausgang findet, wenn es ihn gibt. Dazu benötigt man zunächst ein Labyrinth, das auch graphisch ausgegeben werden kann. Desweiteren können zusätzliche Aufgaben und Hindernisse in den Suchalgorithmus hinzugefügt werden.

Wegsuchen

Der Algorithmus des Wegsuchens funktioniert nach der Vorgehensweise des Backtracking, nach dem Versuch-und-Irrtum-Prinzip (trial and error). Dabei wird jede mögliche Kombination von Teillösungen in Form einer Rekursion ausprobiert bis eine problemlösende Gesamtlösung gefunden ist oder feststeht, dass es keine Lösung gibt, da wirklich alles getestet worden ist. Für das Labyrinthproblem ist ein Pseudocodealgorithmus von Wirth vorgegeben:

PROZEDUR Suche_Weg
  Initialisiere den Auswahlvorgang
  erfolgreich:= false
  WIEDERHOLE 
    Wähle ersten/nächsten Schritt aus (hier 4 Möglichkeiten)
    FALLS Schritt möglich
    DANN 
      FALLS Schritt führt zum Ausgang
      DANN 
        beende die Rekursion (erfolgreich)
        SONST
          markiere den neu betretenen Platz
          Suche_Weg(von neuem Platz aus)
          FALLS nicht erfolgreich
          DANN 
            Markierung zurücknehmen
 BIS erfolgreich oder keine weitere Möglichkeit mehr

So wird zunächst eine Richtung ausgewählt, in die der Wegsucher zuerst gehen soll. Die Suche wird als nicht erfolgreich definiert. Anhand der initialisierten Richtung wird der erste Schritt gewählt. Wenn ein möglicher Schritt zum Ausgang führt, ist die Suche erfolgreich und wird abgebrochen. Andernfalls geht man einen Schritt weiter und führt von dort die selbe Prozedur wieder aus (rekursiv). Die letzten drei Schritte werden solange wiederholt, bis man den Ausgang gefunden hat (erfolgreich), oder alle Richtungen abgetestet wurden (keine weitere Möglichkeit mehr).

Richtungswahl

Bei der Initialisierung des Auswahlvorgangs, also der Wahl der Richtung, in die er zuerst gehen soll, gibt es zwei Varianten. Man kann zum einen systematisch vorgehen, also an jeder Kreuzung immer zuerst in die gleiche Richtung gehen und dann im Uhrzeigersinn die anderen Richtungen abtesten. Das würde dem Wegsuchen in einem Labyrinth mit Kompass entsprechen, wobei man z.B. immer erst nach Süden oder nach Norden geht. Daher gibt es bei dieser Variante auch vier Möglichkeiten (zuerst hoch, nach rechts, runter oder nach links). Je nachdem wo das Ziel liegt und wie das Labyrinth beschaffen ist, kann die eine Richtung günstiger sein als die andere, sowohl hinsichtilich der Lösungsweglänge, als auch der Suchzeit.

In dem Beispiel ist "nach links" die Richtung, die einen kurzen Lösungsweg bringt und nur wenig vom Labyrinth dafür absuchen muss. "Hoch" liefert den selben Weg, hat vorher jedoch das gesamte Labyrinth abgesucht. "Nach rechts" sucht zwar nicht alles ab, findet aber einen längeren Lösungsweg.

Die zweite Variante der Richtungswahl ist, dass an jeder Kreuzung die erste Richtung zufällig gewählt und dann nacheinander dem Uhrzeigersinn nach abgetestet wird. Dabei ist nicht vorauszusagen, ob ein günstiger oder ungünstiger Weg entsteht.

In der Pseudocodezeile "Wähle ersten/nächsten Schritt aus" wird von der vorherigen Richtung eine weitergegangen.

Möglicher Schritt

Um zu testen, ob ein Schritt von der aktuellen Position aus in eine bestimmte Richtung möglich ist, ist es sinnvoll eine GibSchritt-Abfrage zu schreiben, die zurückgibt, was sich an der nächsten Position im Falle eines Schrittes befinden würde. In einer zweiten GibMöglich-Abfrage wird festgestellt, ob der Schritt möglich ist, indem der Wert mit den zum Begehen freigegebenen Werten abgeglichen wird.

Ob ein Schritt erfolgreich zum Ziel führt kann mit der oben beschriebenen GibSchritt-Abfrage ermittelt werden.

Rekursiver Aufruf

Da der rekursive Aufruf von der neuen Position aus erfolgen soll, ist es am sinnvollsten ihn direkt nach der Durchführung des tatsächlichen Schrittes auszuführen, da dabei die neuen Koordinaten berechnet werden. Wenn das Schrittsetzen in einer externen Prozedur stattfindet, kann der Aufruf also auch von dort erfolgen.

Rekursionsende

Die Rekursion wird beendet, wenn alle möglichen Richtungen erfolglos gewesen sind oder das Ziel gefunden ist. Im ersten Fall ist man an einer Position, wo bereits alle Versuche (alle Richtungen) erfolglos waren, daher werden alle vorherigen Aufrufe, bei denen das gleiche gilt, automatisch beendet. Man muss bei diesem Zurückgehen daher nur noch an die graphische Ausgabe denken (Markierung zurücknehmen). Wenn das Ziel gefunden ist, muss dies an die vorherigen Aufrufe, die das nicht wissen können, zurückgegeben werden, damit auch sie beendet werden. Das Wegsuchen ist beendet, wenn alle Aufrufe beendet sind.

Erweiterungen

Generieren

Das Generieren eines Labyrinths ist im Prinzip nur eine Vereinfachung des Wegsuchens, da die Abbruchbedingung des Ziel gefunden wegfällt. Das Labyrinth wird zunächst komplett mit Wänden gefüllt. Dann wird auch hier ein rekursiver Algorithmus benutzt, der alle möglichen Schritte prüft und abgeht, bis er keinen mehr findet. Um brauchbare Labyrinthe zu erhalten, ist es sinnvoll mindestens zwei Schritte auf einmal in die gleiche Richtung zu gehen, da dies besser aussieht.

Wegmarkieren

Bei diesem Labyrinth hat die Suche (Suchtempo: sehr schnell; Suchrichtung: zufällig) ohne Wegmarkieren 26 min gedauert

Bei einem Labyrinth, in dem es keine "Schleifen"(die Möglichkeit im Kreis zu gehen) gibt, z.B. in einem, wie oben beschrieben, generierten Labyrinth, ist diese Erweiterung nicht von besonderem Nutzen. Wenn es jedoch so eine Schleife oder sogar mehrere gibt, dann kann einem diese Funktion viel Zeit und dem Rechner Arbeit ersparen.

Das Problem besteht, wie hier zu sehen ist, darin, dass zwar erkannt wird, wenn ein Weg nicht zum Erfolg geführt hat, aber nicht, dass man an dieser Position schon einmal gewesen ist und dass sich nur der Weg zu dieser Position verändert hat. Wenn man den bereits erfolglos begangenen Weg jedoch Markiert, wird sichergestellt, dass er jede Position nur einmal betritt.

Monster

Ein Monster, das den gefundenen Wegsucher frisst, bewegt sich im Gegensatz zu diesem iterativ durch das Labyrinth. Das Ziel des Monsters bewegt sich und somit könnte bei einer Rekurson ein eben noch als erfolglos abgearbeiteter Weg im nächsten Moment erfolgreich sein. Es muss daher dahin zurückgehen dürfen, wo es gerade herkam. Die Wahl, in welche Richtung das Monster seinen nächsten Schritt setzt, darf jedoch nicht ganz dem Zufall überlassen werden, da es ansonsten genauso wahrscheinlich wäre, dass das Monster zurückgeht, wie, dass es weitergeht. Es käme kaum vom Fleck. Man sollte es also nicht unmöglich, aber unwahrscheinlich machen, dass es sofort wieder zurückgeht. Dies kann z.B. auf diese Weise erreicht werden (konkretes Beispiel):

WENN du in die Richtung (hoch) gegangen bist
DANN
  Gib die Möglichkeit zufällig die Richtung zu wählen, außer entgegengesetzte Richtung (runter)
  WENN dabei kein erlaubter Schritt entsteht 
  DANN 
    Teste beide Richtungsänderungen nochmal ab (rechts und links)
    WENN dabei kein erlaubter Schritt entsteht
    DANN
      Versuche weiter in die Richtung (hoch) zu gehen
      WENN dabei kein erlaubter Schritt entsteht
      DANN
        Gehe zurück (runter)

Auch für ein Monster braucht man eine Abfrage, die angibt, ob ein Schritt möglich ist, da es zusätzlich die Wegspur und markierten Felder betreten darf. Daher muss es eine Zwischenspeicherung geben, die sich merkt, was an der Selle, wo sich das Monster befindet, gewesen ist, damit nichts verloren geht. Das Monster wird sowohl gesetzt, wenn der Wegsucher vorwärts geht, als auch beim zurücknehmen des Wegs. Desweiteren muss der Wegsuchenalgorithmus um eine Abbruchbedingung Vom Monster gefressen erweitert werden. Die Abfrage dafür muss immer vor und nach dem Setzten des Monsters erfolgen.

Schlüsselsuchen

Die Erweiterung, dass man erst einen oder mehrere Schlüssel finden muss, damit der Ausgang freigegeben wird, besteht im Grunde aus zwei getrennten Suchen. Zuerst werden alle Schlüssel gesucht und die Abbruchbedingung des Ziel gefunden wird verhindert. Wenn alle Schlüssel gefunden sind, dann wird die Schlüsselsuche beendet und die Suche nach dem Ziel wird vom Start aus neu begonnen.

Kürzesten Weg suchen

Im wahren Leben wird man meist froh sein aus einem Labyrinth herausgefunden zu haben und sich nicht dafür interessieren, wie optimal sein Weg denn nun ist. Manch ein Freak wird sich aber vielleicht daran machen den kürzesten Weg zu suchen: Man sucht wie immer nach dem Ausgang, wenn man ihn gefunden hat, merkt (speichert) man sich den Weg und versucht auf anderem Wege wieder dorthin zu gelangen (Abbruchbedingung des Ziel gefunden muss verhindert werden). Wenn beim erneuten Zielsuchen der Weg länger wird, als der bereits gefundene, dann braucht man dort nicht mehr weitergehen (verhindert Schrittmoeglichkeit) und kehrt soweit zurück, bis das Weitersuchen wieder einen Sinn macht, der Weg wieder kürzer ist, als der bereits gefundene Kürzeste.

Umsetzung in Delphi

Das Programm: Datei:Labyrinth.zip