Logische Programmierung mit Prolog
Transcrição
Logische Programmierung mit Prolog
Prolog 0. Grundlagen Die Sprache Prolog wird nicht zufällig in Kapitel 2.03 zusammen mit Haskell behandelt. Wir erinnern uns: • PHP, Java, Delphi und C# gehören zu den prozeduralen Programmiersprachen. Hier muss man einen Algorithmus angeben, der beschreibt, wie das Problem zu lösen ist. • Prolog und Haskell sind logische bzw. funktionale Programmiersprachen. In ihnen beschreibt man, was das Problem ist. Prolog bedeutet Programmation en Logique. Zu Beginn der 70-er Jahre wurde sie von Alain Colmerauer und Philppe Roussel entwickelt. Beide arbeiteten mit einem Team an der Universität von Aix an der Frage, wie man sich mit Computern (schriftlich) in französisch unterhalten kann. Zu dieser Zeit gab es eigentlich schon eine logische Programmiersprache namens Planner. Die offizielle Lesart, weshalb diese Sprache von Colmerauer und seinem Team nicht benutzt und weiterentwickelt wurde ist: Planner baut auf der funktionalen Programmiersprache Lisp auf. Und Lisp wollte die Gruppe auf keinen Fall benutzen. Warum auch immer! Die inoffizielle Lesart: Planner konnte kein französisch. Und in einem Land, in dem ein Computer „ordinateur“, ein Walkman „baladeur“ genannt wird, scheint die Beziehung zu Fremdsprachen eher problematisch zu sein. Bleibt nur die Frage, wie es der Begriff „le weekend“ ins Dictionaire geschafft hat...;-) Die Forscher dachten sich das so: Man erzählt dem Computer – pardon, dem ordinateur etwas und dieser bewegt die Worte im Prozessor-Herz(t)e, um den Menschen bestimmte Schlussfolgerungen zu liefern. Ein Beispiel: Man gibt dem Rechner (das ist der ordinateur auf deutsch!) folgende Zeilen ein: • • „Meine Frau hat am 24.12.2004 einen Jungen zur Welt gebracht.“ „Meine Frau hat am 18.04.2005 ein Mädchen zur Welt gebracht.“ Der Rechner gibt dann aus: „Du hast also zwischen dem 25.12.2004 und dem 18.04.2005 neu geheiratet!“ Gedacht war die Sprache Prolog für Expertensysteme, mathematische Logik, automatisches Beweisen und all die anderen Anwendungen, die eine Wissensbasis benötigen, innerhalb derer man Fragen stellen kann, die ein Mensch nur mit viel 1 Zeitaufwand beantworten kann, - wenn er sie, der großen Komplexität wegen, überhaupt beantworten kann. Prolog ist im Gegensatz zu allen anderen hier behandelten Sprachen eine interaktive Sprache. Das heißt, die Programmentwicklung muss man sich eher als Dialog vorstellen. Man gibt dem System das nötige Wissen und stellt dann eine Frage. Möglicherweise merkt man dann, dass dem System weiteres Wissen fehlt. Dieses gibt man dann ebenfalls ein und stellt erneut eine Frage.... In Kapitel 6 (Datenbanken) werden Sie auch lernen, dass Prolog eine relationale Datenbank ist. Mit ihr werden Objekte und Relationen zwischen den Objekten beschrieben. Sie erinnern sich: Bei Haskell war die Formulierung eines Algorithmus sehr nah an der natürlichen Sprache. Denken Sie etwa an das „Turm-Problem“. Dies ist in Prolog in noch stärkerem Umfang gültig. Ein Beispiel: „Das Meer ist türkis.“ In Prolog schreiben Sie: tuerkis(meer). grün(palmenblaetter). weiss(sand). graublau(himmel). In der Sprache Prolog ist beispielsweise meer eine Konstante, die die Eigenschaft (das Prädikat) tuerkis hat. Eine einfache Anfrage wäre nun: ?- gruen(meer) SWI-Prolog-Editor Im Schulnetz ist der SWI-Prolog-Editor in der Version 4.14 installiert. Sie können sich das Programm für zuhause aus dem Tauschverzeichnis kopieren oder hier runterladen: http://lernen.bildung.hessen.de/informatik/swiprolog/setup.zip Die drei Systemerweiterungen turtel, spur und zeichenterm gibt es hier http://lakk.bildung.hessen.de/netzwerk/faecher/informatik/swiprolog/swiprolog.htm Enpacken Sie die Dateien und legen Sie sie im Ordner library von SWI-Prolog ab. Danach im Editor die Anweisung ?- make. eingeben. SWI-Prolog ist mit seinem Konsolenmodus etwas für den Profi. Der SWI-PrologEditor von Gerhard Röhner hingegen eignet sich auch gut für Anfänger. Damit der Editor so wie unten dargestellt aussieht, müssen Sie nach der Installation einige Einstellungen vornehmen. 2 Mit diesem Schalter (Konfiguration) kommen Sie zum Konfigurationsfenster. Hier sollten Sie vor allem das Quellverzeichnis wählen, in das Sie die pl-Dateien ablegen werden. Sonst kann Prolog keine Compilierung des Quelltextes vornehmen: 3 Wählen Sie zum Beispiel: H:\Prolog\Quelltexte Im gleichen Fenster wählt man den Tab „Dokumentation“ und stellt dort den Dateipfad zu den genannten Hilfen ein. Zunächst sind diese Felder rot unterlegt, weil bei der Standardinstallation keine Hilfedateien abgelegt werden. Im Tauschverzeichnis können Sie sich die nötigen Dateien holen. Falls Sie eine schnelle Internetverbindung haben, können Sie hier auch die entsprechenden Internetadressen eintragen: • • • http://gollem.science.uva.nl/SWI-Prolog/Manual/Contents.html http://gollem.science.uva.nl/SWI-Prolog/Manual/DocIndex.html http://hcs.science.uva.nl/projects/xpce/UserGuide/ Um die Geschwindigkeit zu erhöhen, werden die bereits abgerufenen Dateien lokal gespeichert (gecacht). Auch in diesem Fall sollten Sie Ihr Homeverzeichnis H: wählen, damit die Daten nach dem Herunterfahren erhalten bleiben. Der SWI-Prolog-Editor, der auf der letzten Seite dargestellt ist, zeigt links ein Hilfefenster. Rechts daneben erkennt man das eigentliche Prolog-Programm. In diesem Fall ist das Programm familie1.pl, mit dem wir uns gleich beschäftigen werden, dargestellt. Im unteren Feld findet die „Kommunikation“ mit Prolog statt. Hier kann man beispielsweise seine Fragen, die die pl-Datei betreffen, stellen. Sobald Sie sich einige Zeit mit diesem Editor beschäftigt haben, werden Sie selbst entdecken, welche weiter Einstellungen günstig sind. Zum Schluss nur noch ein Hinweis hierzu: Im Test-Menu kann man den sogenannten „Trace-Modus“ einschalten. Ist er eingeschaltet, so wird das Programm schrittweise ausgeführt. Das ist insbesondere dann praktisch, wenn man einen Fehler aufspüren will. Aufgabe 1 Öffnen oder installieren Sie SWI-Prolog-Editor. Nehmen Sie die oben ausgeführten Einstellungen vor und erforschen Sie die weiteren Einstellungsmöglichkeiten. 4 1. Fakten Damit Sie auch mit allen Fenstern arbeiten können, laden Sie das Mini-Programm ist_mutter_von.pl (Tauschverzeichnis). Es enthält nur zwei Zeilen: ist_mutter_von(christine,michael). ist_muter_von(christine,maximilian). Nun wählen Sie Start/consultieren (das Programm wird danach compiliert) und stellen im unteren Fenster hinter ?- eine Anfrage. Vermutlich werden Sie von den Fähigkeiten des Programms ist_mutter_von.pl nicht besonders beeindruckt sein. Kein Wunder! Die Datenbank ist ja auch mehr als bescheiden. Also muss eine etwas größere für weitere Forschungen erzeugt werden. Wir nennen sie familie1.pl und sie sieht so aus: %Funktor: %Variable: %Bedeutung: % %Funktor: %Variable: %Bedeutung: % %Funktor: %Bedeutung: % ivom(X,Y) "Ist Vater oder Mutter von" X,Y sind Menschen ivom(X,Y) ist wahr, wenn X Vater oder Mutter von Y ist. w(X) X sind Menschen w(X) ist wahr, wenn X eine Frau oder ein Mädchen ist m(X) m(X) ist wahr, wenn X ein Mann oder ein Junge ist ivom(christine,michael). ivom(christine,max). ivom(julian,max). ivom(julian,maria). ivom(max,paul). ivom(max,paula). ivom(paul,manuela). ivom(paula,jana). ivom(gernot,jana). w(christine). w(maria). w(paula). w(jana). w(manuela). m(michael). m(max). m(paul). m(gernot). m(julian). %% Ende Im obigen Datensatz werden Fakten beschrieben. Benötigt werden hierzu die Atome (Konstanten), wie christine. Beziehungen zwischen den Atomen werden durch Funktoren beschrieben. ivm/2 bedeutet beispielsweise, dass der Funktor ivm zwei Argumente aufnimmt, w/1 nur eines. 5 Atome und Funktoren müssen mit einem kleinen Buchstaben beginnen. (Ausnahme bei den Atomen: Statt paula kann man auch 'Paula' schreiben. ) Man erkennt unschwer, dass damit folgende Situation beschreiben wird: Aufgabe 2 • • • • Wählen Sie im Editor Datei/neu und tragen Sie die obigen Daten ein. Speichern Sie die Datei auf Ihrem Homeverzeichnis unter familie1.pl ab. Die Text-Färbung nimmt der Editor, wie sie es ja schon von vielen anderen Editoren gewohnt sind, der besseren Lesbarkeit wegen selbst vor. Erweitern Sie jetzt die Familie nach Ihren Vorstellungen. Was bedeutet das %-Zeichen zu Beginn an manchen Zeilen? Weshalb sollte in den Fakten kein weiterer Funktor person/1 auftauchen, mit der Bedeutung: p(X) ist wahr, wenn X eine Person ist? Um Anfragen im unteren Fenster durchzuführen, muss man zuvor die Datei familie1.pl compilieren. Das geht über Start/consultieren. Aufgabe 3 Führen Sie mindestens folgende Anfragen durch: 1. Ist Christine „Vater oder Mutter von“ (ivom) Maria? 2. Ist Jan Vater oder Mutter von Paul? 3. Ist Paula „ein Mädchen oder eine Frau“ (w) ? 6 Vermutlich scheinen Ihnen diese Anfragen banal. Man muss ja nur selbst kurz einen Blick auf die pl-Datei werfen,- dann hat man die Antwort! Hier ist das sicher richtig. Hat die Datei aber mehrer Millionen Einträge, dann reicht kein „kurzer Blick“ mehr. Außerdem haben wir die Möglichkeiten durch Anfragen noch nicht kennengelernt. Hierzu einige Beispiele: Wenn Sie alle Kinder von Christine erfragen wollen, dann geht das so: „Person“ ist eine Variable der Anfrage. Sie werden im Gegensatz zu den Atomen (in Delphi würde man Konstante sagen) immer groß geschrieben! Wenn man auf Enter drückt, dann wird zunächst nur ein Kind ausgegeben. Durch weiteres Drücken auf Enter werden alle weiteren Kinder ausgeben. Sind alle genannt, so erscheint in der letzten Zeile ein „No“. Sehr praktisch bei mehreren Lösungen ist die Funktion findall. Nur die Töchter von Christine bekommt man durch eine konjunktive Anfrage (links). Das Komma bedeutet "und zugleich". Testen Sie, ob die Reihenfolge eine Rolle spielt! Will man die Anfrage ist Vater oder Mutter von präzisieren, indem man etwa genauer wissen will, ob das Prädikat ist Vater von zutrifft, so könnte man zunächst auf den Gedanken kommen, wie weiter oben Relationen ist Vater von bzw. ist Mutter von zu definieren. Das wäre allerdings äußerst ungeschickt, denn diese Informationen stecken ja schon in unserer „Datenbank“. Beispielsweise durch die konjunktive Anfrage links bekommen wir die Auskunft, wer Vater von Maxi ist, - ohne ist Vater von definiert zu haben. 7 2. Regeln Dennoch wäre es natürlich leichter, wenn man mit einer einfachen Anfrage zum Ziel kommen könnte. Und das geht so: iv(V,K):ivom(V,K), m(V). („Kopf“ der Regel) („Rumpf“ der Regel) Man spricht hier von einer Prolog-Regel: Oben ist also die Regel für ist Vater von aus den vorhanden Relationen abgeleitet. Eine Regel wird mit den Zeichen :- nach dem zu Definierenden eingeleitet. Unter Klauseln versteht man die Gesamtheit aus Fakten und Regeln. Aufgabe 4 • • • • • • • • • Erstellen Sie eine Regel im/2 "ist Mutter von" nach obigem Vorbild. Weshalb kann man die Relation ist verheiratet mit nicht aus der vorliegenden Datenbank ableiten? Laden Sie die erweitererte Familien-Datenbank familie3.pl (grafische Darstellung siehe Bild unten) in den SWI-Editor. Ergänzen Sie eine Relation sindverh/2 "sind verheiratet" nach Ihren Vorstellungen. Bedenken Sie aber, dass gilt: Wenn A mit B verheiratet ist, dann ist auch B mit A verheiratet! Diese Eigenheit muss sich in der Regel wiederfinden. Hierzu ein Tipp: Wenn die einzelnen Bedingungen einer Regel durch Strichpunkt getrennt sind, dann bedeutet dies ein logisches oder. Sind sie durch ein Komma getrennt, dann ist dies ein logisches und. Erstellen Sie nun eine Regeln für isva/2 "ist Schwiegervater von". Erstellen Sie eine Regel für igvv/2 "ist Großvater von". Erstellen Sie eine Regel für hkk/1 "hat keine Kinder". Wie lautet eine einfache Regel für sindgeschw/2 "sind Geschwister"? Welches Problem gibt es bei einer Anfrage sindgeschw(X,Y). ? Zum Schluss: Wie prüft man ischwa/2 "ist Schwager von" ? Beachten Sie, dass es hier zwei Möglichkeiten gibt: Im obigen Beispiel ist Jan ein Bruder von Gernot und Sebastian ein Bruder von Paula. Aber nur in der Kombination Sebastian-Gernot ist die Beziehung ischwa/2 symmetrisch! 8 Und so sieht Familie3 aus: Einige werden die Regel für hkk/1 "hat keine Kinder" so aufgeschrieben haben: hkk(X):(m(X);w(X)), not(ivom(X,W)). Prolog gibt dann eine Warnung aus: [W] Singelton variable. Das bedeutet, dass die Variable W nur einmal in der Regel vorkommt und somit in keiner Beziehung zur Variablen X steht. Die Ausgabe dieser Warnung kann man vermeiden, wenn man für statt der Variablen W die anonyme Variable _W verwendet. Also: hkk(X):(m(X);w(X)), not(ivom(X,_W)). Wie kann eine Regel für esg/1 "Eltern sind geschieden von" formuliert werden? Ist dies eine Lösung? esg(X):ivom(Y,X), ivom(Z,X), Y\==Z, not(sindverh(Y,Z)). Stellt man eine Anfrage ?- esg(X). so bekommt man wie bei sindgeschw/2 manche Antworten mehrfach. Das ist zwar nicht falsch, aber lästig. Verstehen kann man die Mehrfachnennungen erst wenn man weiß, wie Prolog arbeitet. Damit beschäftigen wir uns im 4.Kapitel. Abstellen lässt sich das Verhalten erst, wenn man die Ergebnisse in Listen speichert und so überprüfen kann, ob das neue Element schon vorhanden ist. Damit Sie nicht glauben, das Prolog nur für die Kontrolle von Verwandtschaftsverhältnissen zu gebrauchen ist, hier eine ganz andere Anwendung: 9 Oben ist eine vereinfachte Landkarte dargestellt. Die Frage: Kann man diese Karte mit drei Farben einfärben, so dass keine gleichen Farben nebeneinander liegen? Wenn nein: Reichen vier Farben? Natürlich kann man das Problem auch ohne Prolog relativ leicht lösen. Das liegt hauptsächlich daran, dass wir bewusst einen einfachen Fall gewählt haben, um die nötigen Eingaben nicht zu umfangreich werden zu lassen. So kann man den Sachverhalt eingeben (bei vier Farben z.B. farbe(gelb) hinzufügen. Das Prädikat \= bedeutet „ sind ungleich“): %% Einfärben von Landkarten farbe(blau). farbe(gruen). farbe(rot). farbe(gelb). einfaerbung(F1,F2,F3,F4,F5,F6,F7):farbe(F1),farbe(F2),farbe(F3),farbe(F4),farbe(F5),farbe(F6),farbe(F7), F1\=F2,F1\=F3,F1\=F4,F1\=F5,F1\=F6,F2\=F3,F2\=F4,F3\=F6,F3\=F7,F4\=F5, F5\=F6,F6\=F7. Eine Anfrage mit vier Farben muss eine Lösung liefern (Vier-Farben-Satz!). Prolog gibt alle möglichen Lösungen aus. (Wieviele? Allein 60 sind es, die für Feld 1 die Farbe blau festlegen!!). Auf die Anfrage ?- einfaerbung(F1,F2,F3,F4,F5,F6,F7). bekommt man als erste Antwort die Lösung rechts. Um weitere Lösungen zu erhalten, drückt man die Entertaste. Einfacher geht es, wenn man bis zur letzten Anzeige die w-Taste drückt. Aber auch das ist zeitraubend. Deshalb verfassen wir eine neue Regel, die Prolog dazu zwingt, immer weitere Druckbefehle zu erzeugen. Entscheidend ist dabei der Eintrag fail, der, wie man im nächsten Kapitel erfahren wird, dafür sorgt, dass Prolog nach immer weiteren Möglichkeiten sucht, bis es keine mehr gibt: (nl erzeugt eine "new line"). Auch wenn es danach aussieht: Dies ist keine Schleife im prozeduralen Sinn! Besser beschrieben wird der Vorgang durch den umgangssprachlichen Aufruf: "Such weiter, bis nichts mehr gefunden wird!" alleEinfaerbungen(F1,F2,F3,F4,F5,F6,F7):( einfaerbung(F1,F2,F3,F4,F5,F6,F7), print(F1),print(' '), print(F2),print(' '), print(F3),print(' '), print(F4),print(' '), print(F5),print(' '), print(F6),print(' '), print(F7),print(' '), nl, fail ; true ). 10 3. Rekursion Zurück zu unserem Stammbaum. Wie würden Sie die Prozedur ige ist Großelter von (also ist Großvater oder Großmutter von) in Prolog implementieren? Und welche Regel erstellen Sie für die Prozedur iuge ist Urgroßelter von ? Eine Möglichkeit wäre: ige(X,Y):- ivom(X,Z), ivom(Z,Y). % ist Großelter von iuge(X,Y):- ivom(X,Z), ige(Z,Y). % ist Urgroßelter von Damit wird auch die Erweiterung zu iuuge ist Ururgroßelter von recht einfach: iuuge(X,Y):- ivom(X,Z), iuge(Z,Y). % ist Ururgroßelter von So könnte man beliebig fortfahren. Die Implementierung der Prozedur ivorfahr ist Vorfahr von würde uns so nicht gelingen. Wie die Überschrift erkennen lässt, muss das Problem „Vorfahr“ durch eine Rekursion gelöst werden: ivorfahr(X,Y):- ivom(X,Y). ivorfahr(X,Y):- ivom(X,Z), ivorfahr(Z,Y). Die erste Zeile der Rekursion ist, wie auch schon in Haskell, der Induktionsausstieg. Man braucht diese Zeile, damit es keine Dauerschleife gibt. Im nächsten Kapitel werden wir untersuchen, wie genau Prolog zum Beispiel die Anfrage ?- ivorfahr(margit, michael) beantwortet. Zuvor aber gehen wir die Rekursion selbst durch. Ein möglicher Weg wäre: ivorfahr(margit, thomas) ist wahr, denn ivom(margit, thomas) ist wahr. Damit wird das Problem auf ivorfahr(thomas, michael) reduziert. Das klappt wieder nicht direkt, denn ivom(thomas, michael) ist falsch. Aber ivorfahr(thomas, christine) klappte, weil ivom(thomas, christine) wahr ist. Damit wird das Problem auf ivorfahr(christine, michael) reduziert. Und das ist wahr, denn es gilt ivom(christine, michael). Die Anfrage ?- ivorfahr(margit, michael) wird also mit true beantwortet Aufgabe 5 Zeigen Sie, dass es auch andere "Wege" gibt, die bei der obigen Anfrage zur Antwort "true" führen. Veranschaulichen Sie den Sachverhalt an einer Skizze. In wie fern ist die erste Zeile der obigen Regel ein "Ausstieg" aus der Rekursion? Was wäre passiert, wenn man es oben für Z nicht mit thomas, sondern mit martin versucht hätte? Wie muss Prolog vorgehen, um auch alle Möglichkeiten testen zu können? 11 Aufgabe 6 Notieren Sie alle Fakten, die zu dem Bild der auf einem Stapel liegenden Klötzchen gehören: Es gibt drei Stapel x, y und z 4 liegt auf x, 3 liegt auf 4 etc. Formulieren Sie eine (rekursive) Regel liegtUeber(X,Y) mit der Bedeutung : X liegt über Y. Ein weiteres Beispiel: In einer großen Stadt haben die Planer die Busverbindungen so angelegt, dass man vom Bahnhof (1) mit nur einem Ticket zu den Sehenswürdigkeiten (2-11) fahren kann, falls man immer nur die durch Pfeile markierten Verbindungen benutzt. Ganz nach dem Motto: Wer sparen will, muss auch nicht alles sehen.... Ein Reisender mit PrologKenntnissen möchte die Gegebenheiten so in Prolog implimentieren, dass er nur noch eingeben muss: fahre(1,5) und sein prologfähiger Organizer gibt die Antwort Yes oder No Zunächst muss man Prolog die Gegebenheiten mitteilen. buslinie(1,2). bedeutet, dass es eine Busverbindung auf der „Spar-Buslinie“ von 1 nach 2 gibt. fahre(X,X). drückt aus, dass es auch erlaubt ist, an der Stelle X zu bleiben. fahre(X,Y). ist – wie erwartet- rekursiv definiert. Beachten Sie, dass dies hier nur funktioniert, weil es keine Rückwege gibt. Mit diesen würde die Rekursion unweigerlich in einer Schleife landen. Gespeichert wird alles unter sparbuslinie.pl 12 Beispiel einer Anfrage: Aufgabe 7 Erweitern Sie die Anzahl der Sehenswürdigkeiten und fügen Sie entsprechende Busverbindungen hinzu. Achten Sie darauf, dass keine Rückwege und damit mögliche Schleifen entstehen. Implementieren Sie die neue Situation in Prolog. Aufgabe 8 Prolog ist als Programmiersprache sicher nicht erste Wahl, wenn es um umfangreiche Rechnungen geht. Dennoch stellt die Sprache zahlreiche arithmetischer Operatoren und Funktionen zur Verfügung. Um mehr darüber zu erfahren, rufen Sie das Hilfe-Menu auf und geben Sie als Suchbegriff "Arithmetic" ein. Wählen Sie den obersten Eintrag in der Ergebnis-Liste. Die meisten Bezeichnungen sind selbsterklärend. Es gibt aber auch einige Besonderheiten. • Welche Operatoren und Funktionen gibt es? • Worin besteht der Unterschied zwischen is und =:= ? • Welche drei numerische Typen gibt es? Rekursionen sind nicht einfach zu verstehen. Daher sollen weitere Beispiele behandelt werden. 13 Beispiel Fakultät Unter n! versteht man 1*2*3*...*(n-1)*n, also gilt beispielsweise 5! = 120. Der GTR (Menu Math/PRB/ ! ) zeigt die Fakultät gerundet bis 69 an. Danach bekommt man eine Fehlermeldung. Warum das so ist und weshalb Prolog dies viel besser kann, erfährt man hier: % fakultaet/2 % fakultaet(N,F) ist wahr, wenn N! = F %Faktum fakultaet(0,1). %Regel fakultaet(N,F):N>0, N1 is N-1, fakultaet(N1,F1), F is N * F1. Aufgabe 9 • • • Erstellen Sie nach den obigen Angaben das Programm fakultaet.pl und prüfen Sie es durch folgende Anfragen: ?- fakultaet(5,F) , ?- fakultaet(250,F) etc. Gehen Sie die Anfrage ?- fakultaet(3,F) Schritt für Schritt durch. Wie kommt das Ergebnis 6 zustande? Wann ist die Rekursion beendet? Beispiel Fibonacci-Zahlen Kaum eine ander Folge von Zahlen hat so viel Aufmerksamkeit erfahren wie diese: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... Können Sie die Folge fortsetzten? Ursprünglich hatte Fibonacci die Folge durch das Wachstum einer Kaninchenpopulation veranschaulicht. Es gelten genau drei Regeln: 1. Nach jedem Monat wirft ein Paar ein weiteres Paar Kaninchen. 2. Erst ab dem zweiten Lebensmonat bekommt ein neugeborenes Paar Nachwuchs. 3. Alle Tiere befinden sich in einem abgeschlossenen Raum und leben unendlich lange. Aufgabe 10 Versuchen Sie die oben begonnene Zahlenfolge mit den drei Regeln in Einklang zu bringen. Wie lauten die folgenden Bestandzahlen von Kaninchen-Paaren? Versuchen Sie eine rekursive Regel mit Abbruchbedingung (Induktionsanfang) zu formulieren. 14 Informieren Sie sich hier zum Thema Fibonacci-Zahlen: http://www.matheprisma.uni-wuppertal.de/Module/Rekurs/index.htm http://www.youtube.com/watch?v=bE2EiI-UfsE Eine erste Lösung könnte so aussehen: % fibonacci1 % fibo/2 % fibo(N,F) ist wahr, wenn F die N-te Fibonacci-Zahl ist % Fakten fibo(0, 1). fibo(1, 1). %Regel fibo(N, F) :N > 1, N1 is N - 1, N2 is N - 2, fibo(N1, F1), fibo(N2, F2), F is F1 + F2. Viel Freude haben wir an dieser Lösung nicht, denn schon bei der Anfrage ?- fibo(40,F). bekommt man einen Stack-Überlauf, ein Zeichen dafür, das wir den Rechner überfordert haben. Aufgabe 11 • • • • Erstellen Sie nach den obigen Angaben das Programm fibonacci.pl und prüfen Sie es durch folgende Anfragen: ?- fibo(5,F) , ?- fibo(30,F) etc. Gehen Sie die Anfrage ?- fibo(3,F) Schritt für Schritt durch. Wie kommt das Ergebnis 3 zustande? Wann ist die Rekursion beendet? Weshalb ist die Anfrage ?-fibo(40,F). mit so viel Rechnung verbunden? Weshalb hatten wir in fakultaet dieses Problem nicht? Immerhin erlaubt uns dieses Beispiel, deutlich zu erkennen, dass nicht alle rekursiv aussehenden Aufgaben auch rekursiv gelöst werden sollten. Die 40.-te FibonacciZahl mit einer Schleife auszurechnen, erledigt der Rechner in einem Bruchteil einer 100-tel Sekunde! Beispiel Turm von Hanoi Die Spielregel ist einfach: Lege die Scheiben einzeln so um, dass sie am Ende wieder genau wie jetzt auf einem anderen Stab daliegen. Dabei darf nie eine größere Scheibe auf einer kleineren liegen. 15 Hier können Sie es einmal selbst versuchen: http://www.matheprisma.uni-wuppertal.de/Module/Rekurs/index.htm Aufgabe 12 • • Zeigen Sie dass man mit 7 Schritten einen Dreier-Turm nach der obigen Regel umsetzten kann. Weshalb sind nun noch genau 8 Schritte nötig, die Aufgabe zu lösen? (Das Bild rechts ist dabei sehr hilfreich!). Wer die obige Aufgabe gelöst hat, erkennt, dass man das Problem mit vier Scheiben auf das Problem mit drei Scheiben zurückführen kann. Verallgemeinert heißt dies, wenn man das Problem für n Scheiben gelöst hat, ist es auch für n+1 Scheiben gelöst. Nehmen wir an, man benötigt x Züge für n Scheiben. Dann benötigt man offensichtlich 2x +1 Züge für n+1 Scheiben. Wenn man die Stäbe von 1 bis 3 durchnummeriert, und ein Turm von n+1 Scheiben von Turm 1 auf Turm 3 gebracht werden soll, so muss man lediglich einen Turm von n Scheiben auf Turm 1 auf Turm 2 transportieren. Dann die letzte Scheibe auf Turm 3 setzten und nun die n Scheiben auf Turm 2 nach Turm 3 transportieren. Das ist aber ganz eindeutig eine Rekursions-Anweisung: Löse das Problem für 5 Scheiben, indem du das Problem für 4 Scheiben löst. Löse das Problem für 4 Scheiben, indem du das Problem für 3 Scheiben löst.... Am Ende muss man nur das Problem für eine Scheibe lösen - und das ist nun wirklich kein Problem! Aufgabe 13 Überprüfen Sie den folgende Quellcode dahingehend, ob er dem oben beschriebenen rekursiven Algorithmus gerecht wird. Kontrollieren Sie dabei vor allem die Regel verschiebe(N,Start,C,Ziel):% Türme von Hanoi % hanoi/1 : hanoi(N) gibt die minimale Zugzahl für einen Turm mit N Scheiben aus. % verschiebe/4 : verschiebe(N, links, mitte, rechts) hanoi(AnzahlScheiben):verschiebe(AnzahlScheiben, links, mitte, rechts). verschiebe(0,_,_,_). %Abbruchbedingung: keine Scheibe zu verschieben verschiebe(N,Start,C,Ziel):M is N-1, verschiebe(M,Start,Ziel,C), eineScheibe(Start,Ziel), verschiebe(M,C,Start,Ziel). eineScheibe(Von,Nach):print('Bewege Scheibe von '), print(Von), print(' nach '), print(Nach), nl. 16 4. Wie Prolog arbeitet Die Situation ist in einem Punkt völlig klar: Das Prolog-"Programm" besteht aus Klauseln (Fakten und Regeln) und der Anwender stellt eine Frage, die Prolog mit Hilfe der Klauseln beantworten soll. Man kann sich daher die Klauseln als eine Wissensbasis vorstellen. Hinzu kommt, dass Prolog in der Lage sein soll, logische Schlussfolgerungen aus den Klauseln zu ziehen. Das klingt zunächst ziemlich abenteuerlich, denn die Logik betrachten wir doch als eine allein dem Menschen zur Verfügung stehende Fähigkeit. Ein großer Irrtum! Logik basiert auf Axiomen und Regeln, die man ohne Ausnahme in einen Algorithmus packen kann. Man kann zeigen, dass beim Bearbeiten einer Anfrage drei verschiedene Methoden, Resolution, Unifikation und Backtracking, notwendig sind, um eine korrekte Antwort zu bekommen. In diesem Kapitel werden wir mit verschiedenen Techniken Prolog beim Beantworten einer Anfrage "über die Schulter schauen", und dabei lernen, was man unter den drei Begriffen versteht. ProVisor Das Programm Provisor ist ein DOS-Programm zur Visualisierung einer PrologAnfrage, das aber unter Windows XP problemlos (im DOS-Modus) funktioniert. Beachte: Dateinamen dürfen in DOS nur maximal 8 Zeichen haben. (Spätere Versionen von Windows benötigen eine "DOSBox". In jedem Fall muss aber der Pfad in der Datei PROVISOR.PTH angepasst werden.) In den Ordner BSP legen wir die Prolog-Datei familie1.pl. (Siehe Seite 5). Dann wird PROVISOR.EXE durch Doppelklick gestartet. Wenn alles gut geht, öffnet sich ein etwas spartanisches DOS-Fenster. Durch Drücken der Taste F4 kann man alle im Beispiel-Ordner vorhandenen pl-Dateien konsultieren. Wir geben oben familie1.pl ein. Im unteren Fenster erfolgt die Anfrage ?- ivv(max,Y). Dann erscheint ein erstes Bild: Anfrage-Knoten Funktor: ivv Variablen-Leiste Hier können Variable instanziert werden. (Schraffiert sind die Felder, weil sie noch nicht vom Interpreter "besucht" wurden. 17 Resolution: Der Funktor ivv wird in Teilziele aufgelöst. Die beiden durchgezogenen Strecken bedeuten "und zugleich". X_1 und Y_1 sind die (auch für die Teilziele benötigten) Variablen. Unifikation: Es wird unter den Fakten ein passendes Gegenstück gesucht. Gestrichelte Strecken bedeuten "oder". 18 Nicht-passende und überprüfte Fakten bekommen einen "Deckel" verpasst. Das passende Faktum ivom(max,paul) bewirkt, dass die Variable Y nun mit paul belegt wird: Nun muss noch überprüft werden, ob m(max) gilt: Gefundene Lösungen werden gestrichen: 19 Backtracking: Nach der gefundenen Lösung wird zum letzten Knoten zurückgesprungen und nach noch offenen Möglichkeiten gesucht. Nächste Fundstelle für die Belegung von Y ist paula Kontrolle, ob m(max) erfüllt werden kann: 20 Es nützt leider nichts, dass dies schon beim letzten Mal beim Treffer paul überprüft wurde. Gibt es einen neuen Treffer, werden bei jedem Mal alle Teile der Regel überprüft. Nach erneutem Backtracking wird erkannt, dass es keine weiteren Lösungen mehr gibt: Aufgabe 14 • • • Führen Sie mit FAMILIE1PLl in Provisor die Anfrage ?- im(dagmar,Y). durch. Überprüfen Sie, wann Resolution, wann Unifikation und wann Backtracking durchgeführt wird. Verwenden Sie FAMILIE6PLl und stellen im Provisor die Anfrage ?- sindgeschw(michael,Y). Erstellen Sie zur besseren Übersicht einen passenden Und-Oder-Baum. (Tipp: Unter Visualisierung/ Übersichtsbaum kann man den Baum ohne Beschriftung anzeigen lassen!) Konsultieren Sie unser erstes Faktuläts-Programm, hier FAKULT.PL genannt, und stellen Sie die Anfrage ?- faktultaet(3,F). Beobachten Sie den Ablauf im Einzelschritt-Modus und lassen Sie sich nach dem ersten Treffer den Übersichtsbaum anzeigen. trace-Modus Die visuelle Provisor-Methode eignet sich natürlich nur für nicht zu umfangreiche Fakten. Bereits bei 100 Fakten ist die Übersicht dahin. Aber auch in einer logischen Programmiersprache muss es eine Art "Debug-Modus" geben. Und in der Tat kann man jede Anfrage im sogenannten trace-Modus durchführen. Den erreicht man entweder über Test / Trace ein aus oder über das Brillen-Ikon. 21 Die Funktionsweise des trace-Modus kann durch folgende Zeichnung (Boxenmodell) am Beispiel "ist Mutter von" veranschaulicht werden: Man kann die Anfrage "ist Mutter von" ( im/2 und im2/2 in familie6.pl) auf zwei verschiedene Arten stellen: im(M,K):ivom(M,K), w(M). % In diesem Fall wird zuerst überprüft, ob M Vater oder % Mutter von K ist. Danach überprüft man, ob M weiblich im2(M,K):w(M), ivom(M,K). % Hier ist die Reihenfolge der Anfragen gegenüber oben % vertauscht. Was ist in der Regel schneller? Für uns macht es keinen Unterschied, ob wir nach der ersten oder zweiten Methode vorgehen. Es wird ja der gleiche Sachverhalt beschrieben. erkennt man, dass die beiden Wege durchaus verschieden lang dauern können- und damit also in Prolog anders abgearbeitet werden: (Die Unify-Zeile erhält man nur, wenn man zuvor die Anfrage ?- visible(+all). stellt.) 22 Aufgabe 15 Versuchen Sie anhand der obigen trace-Protokolle herauszubekommen, wie Prolog arbeitet. Verwenden Sie hierzu auch das oben dargestellte Boxenmodell. Weshalb wäre bei einer sehr großen Datenbasis das untere Verfahren langsamer? Sobald die Anfragen etwas komplexer werden, kann man sehr gut das sogenannte Backtracking (Zurücksetzen) beobachten. Als Beispiel dient uns zunächst die Anfrage: "Ist Paul ein Schwager von Gernot?" Prolog wird die erste Möglichkeit testen: "Ist Paul ein Bruder von Z und sind Z und Gernot verheiratet?" (sindgeschw(X,Z),m(X),sindverh(Z,Y)); Dabei muss sindgeschw(paul,Z) solange mit den vorhandenen Daten verglichen werden, bis sich eine Möglichkeit für Z ergibt (Das Verfahren zur Unifizierung nennt man matching). Paula erfüllt diese Bedingung. Paul ist männlich m(X),- die nächste Bedinung ist also ebenfalls erfüllt. Allerdings muss Paula mit Gernot verheiratet sein sindverh(Z,Y). Dies ist der Fall und Prolog kann ein Yes ausgeben: Erheblich mehr Mühe machen wir dem Programm, wenn wir umkehrt anfragen: "Ist Gernot ein Schwager von Paul?" Die erste Möglichkeit greift hier nicht und so muss Prolog nach vielen Irrungen und Wirrungen mit der zweiten Möglichkeit von vorn anfangen. Ganze 167 Zeilen hat das Protokoll dieses mal! Probieren Sie es aus! Sofort taucht die interessante Frage auf: Könnte man die zweite Möglichkeit nicht einfacher überprüfen: Also statt sindgeschw(Z,Y),sindverh(X,Z),m(X) besser: sindgeschw(X,Z),m(X),sindverh(Z,Y) wie im ersten Fall, aber mit der Zusatzbedingung: m(Y) ? Klar, denn wenn X ein Schwager von Y ist, dann gilt doch die Umkehrung nur, wenn Y männlich ist,- oder? Ihre Antwort? 23 Der trace-Modus wurde sicher nicht implementiert, damit der Benutzer die „Mühen“ beobachten kann, die Prolog hat, um eine Anfrage zu beantworten. Der eigentliche Sinn ist natürlich die Fehlersuche zu unterstützen! ivorfahr(X,Y):ivorfahr(X,Y):- ivom(X,Y). ivom(X,Z), ivorfahr(Z,Y). Um die Rekursion etwas besser zu verstehen, kann man die Anfrage ivorfahr(heike, heimo). im trace-Modus durchlaufen lassen (sieh Screenshot rechts). Prolog stellt hier zunächst fest, dass Heike keine Mutter oder Vater von Heimo ist. Da dies nicht der Fall ist, ergibt die Prüfung ein Fail. Danach wird nach der zweiten Zeile geprüft, ob Heike Vater oder Mutter von irgend jemandem ist. Die Suche führt zu Richard. Das ist allerdings eine Sackgasse (weshalb?). Um das zu erkennen, benötigt das Programm 13 Zeilen. Schließlich findet es den Weg über Simon... Prüfen Sie so, ob Margit ein Vorfahr von Justin ist. Schon bei dieser kleinen Faktenmenge ist es sehr schwierig einen Überblick, vor allem bei Rekursionen, zu behalten. Wünschenswert wäre, dass der besseren Lesbarkeit wegen, die Zeilen, je nach Tiefe der Resolution eingerückt sind. Genau dies leistet der Funktor spur/1 24 Spuren Die gleiche Anfrage wie oben (auf dem gleichen Datenmaterial) sieht deutlich übersichtlicher aus: Benötigt wird hierzu die Datei spur.pl , die man hier runterladen kann: http://lakk.bildung.hessen.de/netzwerk/faecher/informatik/swiprolog/swiprolog.html Abgelegt wird sie im library-Verzeichnis. Danach gibt man zum Einbinden im Anfrageabschnitt des Editor noch make. ein. Danach ist dem Interpreter spur/1 bekannt. Aufgabe 16 • • Beobachten Sie die Abarbeitung der Anfrage ?- spur(liegtUeber(X,x)). (aufgabe6Stapel.pl) Vergleichen Sie mit der Ausgabe bei der gleichen Anfrage, - aber im traceModus. Graphischer Debugg-Modus Wenn man im Menu Test einen Haken bei GUI-Tracer setzt, dann öffnet sich im traceModus ein Fenster, das noch etwas mehr Überblick erlaubt: 25 Im Teilfenster Call Stack wird der aktuelle Aufruf angezeigt. Das Hauptziel steht oben und neue Teilziele werden unten eingefügt. Rechts (hier mit Pfeil) sind Teilziele zu erkennen, für die es noch andere Lösungen geben kann. Klickt man auf die Ziele oder Teilziele, so wird im unteren Fenster der zugehörige Quellcode angezeigt. Im Teilfenster Bindings wird die aktuelle Belegung der Variablen dargestellt. Nun haben Sie vier Möglichkeiten kennengelernt, Prolog "auf die Finger zu schauen". Auch wenn besonders die letzten drei Möglichkeiten das Debuggen eines Programms unterstützen, - Ziel dieses Kapitels war das Beweis-Prinzip von Prolog mittels Resolution, Unifikation und Backtracking nachzuvollziehen. Das folgende mit Prolog zu lösende Rätsel dient dann auch dazu, ProVisor, trace, spur und GUI-trace erneut in Aktion zu beobachten. Zahlen-Rätsel Jeder Buchstabe vertritt eine andere Ziffer. Finden Sie alle Lösungen. Kleiner Tipp: "//" ist die Ganzzahl-Division. Beispiel: 41 // 9 ergibt 4. Hingegen für 41 mod 9 erhält man 5 Die Lösung erhält man, wenn man die Fakten stur genau so hinschreibt, wie sie dastehen. Zunächst werden die vorkommenden ziffern-Variablen formuliert: ziffer(L),ziffer(G),ziffer(A),ziffer(N),ziffer(F), Und nun muss festgehalten werden, dass alle verschieden sein müssen: L\=G,A\=L,A\=G,N\=L,N\=G,N\=A,F\=L,F\=G,F\=A,F\=G, 26 Eine Addition beginnt man mit den letzten Ziffern. Und das sind die Fakten dieser linken Spalte: S1 is L+L,G =:= S1 mod 10,Ue1 is S1//10, Hier steht, dass es zwei Ziffern L und G gibt, die verschieden sein müssen. Dann wird die Summe L + L gebildet. G ist dann die Summe mod 10, also einfach die Einerziffer. Der Übertrag Ue1 ist die Summe // 10, also die Zehnerziffer. Beachte: "is" ist der Auswertungsoperator, "=.=" bedeutet hingegen numerische Gleichheit. Und nun die zweite Spalte (von links). S2 is A+A+Ue1,N =:= S2 mod 10,Ue2 is S2//10, Man muss hier nur beachten, dass die Summe auch den Übertrag von oben enthält. Die dritte Spalte (hier ist der Übertag gleich F): S3 is A+A+Ue2,A =:= S3 mod 10,F =:= S3//10. Zwei Dinge fehlen noch: • Es gibt die Ziffern 0, 1, 2, ..... 9 • Die Regel für loesung(A,F,G,L,N) muss durch die drei obigen Zeilen definiert werden. Das erledigen wir so: ziffer(0). ziffer(1). ziffer(2). ziffer(3). ziffer(4). ziffer(5). ziffer(6). ziffer(7). ziffer(8). ziffer(9). loesung(A,F,G,L,N):- Hinter den Regelkopf müssen dann nur noch die drei Zeilen von oben eingetragen werden. Welche Lösungen gibt es? Wie sieht die Anfrage im trace- oder spur-Modus aus? Aufgabe 16 • • • Lösen Sie mit Prolog dieses Zahlenrätsel: Finden Sie eine Lösung, bei der M = 1 ist. Wenn Sie wie im obigen Beispiel vorgegangen sind, dann braucht Prolog sehr viel Zeit für die Lösung. Vergleichen Sie Ihre Lösung mit der Lösung buchstabenraetselMONEYschnell.pl im Tauschverzeichnis. Weshalb ist diese Lösung, die ja nur Umstellungen in der Regel vornimmt, so viel schneller? Verwenden Sie hierzu den spur-Modus. 27 Aufgabe 17 Den größten gemeinsamen Teiler zweier natürlicher Zahlen a und b (ggT(a,b)) kann man nach folgendem Algorithmus bestimmen: ggT(a,b) = a ; falls a = b ggT(a,b) = ggT(a-b,b); falls a > b ggT(a,b) = ggT(a, b-a); falls a < b Beispiel: ggT(49,56) = ggT(49, 7) = ggT(41,7) = ...= ggT(7,7) = 7 Schreiben Sie eine rekursive Regel in Prolog, mit der man den ggT zweier beliebiger natürlichen Zahlen berechnen kann. Beispielanfrage: ggT(163251,175001400,E):(E steht für Ergebnis) Beobachten Sie die Lösung von ggT(163251, 175001400,E) im spur-Modus. 28 5. Listen Wie oben schon erwähnt, gibt es keine Schleifen in Prolog. In diesem Kapitel wird gezeigt, dass Listen in einer logischen Programmiersprache die Schleifen einer prozeduralen Sprache ersetzen. Schauen wir uns das Programm spar_buslinien nochmal an. Etwas enttäuschend ist die Tatsache, dass man nur mitgeteilt bekommt, ob der geplante Weg möglich ist oder nicht. Viel besser wäre doch, wenn man eine oder mehrere Listen von möglichen Wegen bekäme. Unter Listen werden hier Datenstrukturen mit variabler Länge verstanden. Es gibt sie sowohl in prozeduralen (z.B. C#) aber auch in funktionalen Programmiersprachen (z.B. Haskell). In Prolog allerdings sind sie von grundlegender Bedeutung. Etwas vereinfacht ausgedrückt: Ohne Listen keine logische Programmiersprache. (Allerdings entspricht den Prolog-Listen eher Haskell-Tupel, denn Listen durften in Haskell ja keine verschiedenenartigen Daten-Typen enthalten. ) Die Schreibweise für Listen ist Standard: [margit, miriam, julian] oder [rot, blau, gruen, weiss, schwarz] sind Listen. Allerdings ist auch dies: [rot, blau, gruen, weiss, schwarz,1,2,[ margit, miriam, julian] ] . Die Elemente der Liste dürfen also nicht nur verschieden sein, - sie dürfen als Elemente selbst Listen enthalten. Es ist auch die „leere Liste“ [ ] zugelassen. Um auf einzelne Listenelemente zugreifen zu können, benötigt man einen „Separator“. Häufig ist dies der Doppelpunkt oder der Strichpunkt - hier in Prolog ist es der senkrechte Strich: [rot, blau | [gruen, weiss, schwarz] ] Hier ein Beispiel für eine Anfrage: Solche Separatoren sind für die Bearbeitung von Listen unverzichtbar. Es gibt jedoch noch weiter Möglichkeiten, durch vorgefertigte Operationen eine Bearbeitung zu vereinfachen: member(X,Liste). bedeutet: „Das Element X ist in der Liste vorhanden“. Beispiel: length(Liste,N). bedeutet: „Die Anzahl der Elemente der Liste ist N.“ Beispiel: 29 append(Liste1,Liste2,L). bedeutet: „L ist die Liste, die durch anhängen der Liste2 an Liste1 entsteht.“ Beispiel: Hier sehen Sie, dass Prolog gegebenenfalls drei Punkte für „usw“ schreibt! Hier wird die append-Operation zusammen mit dem Separator benutzt: Mitunter ist die Verwendung einer anonymen Variablen (z.B.: [ _| a] ) von Vorteil: Eine anonyme Variable ist eine Variable, deren Wert nicht benötigt wird. Entsprechend „anonym“ ist dann auch das Ergebnis der Anfrage!!! Aufgabe 18 • • • • • Hängen Sie mit append ein einzelnes Element an eine Liste hinten an. Stellen Sie einige Anfragen mit den Listenoperatoren length und member. Was ergibt die Anfrage:?- member(Zahl,[1,2,3,4,5]). Wie kann man alle Elemente einer Liste einzeln ausgeben lassen? gibAus([1,2,5,6]). soll 1 2 5 6 liefern. (Tipp: Separator und Rekursion benutzen). Schreiben Sie eine Prozedur element_n, die prüft, ob das n-te Element einer Liste gleich einem gegebenen Element ist (Zählung beginnt bei 0). Ein Beispiel: ?-element_n([a,b,c,d,e],2,c) sollte Yes ergeben. ?-element_n([a,b,c,d,e],1,X) sollte X=b ergeben. Der letzte Teil der Aufgabe war nicht einfach! Eine mögliche Lösung sieht so aus: element_n(Liste,N,X):append(LinkeSeite,[X|_],Liste), length(LinkeSeite,N). Angenommen, man gibt ein ?- element_n([a,b,c,d,e],3,X). 30 Was passiert dann nach dem obigen Programmcode eigentlich, bis es zur Lösung X=d kommt? Man kann es sich bildlich so vorstellen: [a,b,c,d,e] ist die „Liste“. Sie muss als Ergebnis bei der Operation append herauskommen. Weil die Liste „LinkeSeite“ die Länge 3 hat, kann sie nur so aussehen: [a,b,c]. Zusammengefasst: [a,b,c,X, Restliste] = [a,b,c,d,e]. Durch direkten Vergleich („matching“) kommt man jetzt zum Ergebnis: X = d Vielleicht etwas einfacher, aber noch eine Spur trickreicher, ist die Operation enthalten(X,L). Zum Beispiel soll die Anfrage enthalten(s,[a,b,t,s,y]). „Yes“ ergeben, da s in der anschließenden Liste enthalten ist. Und so könnte man enthalten(X,L). implementieren: enthalten(E,[E| _ ]). enthalten(E,[ _ |L]):- enthalten(E,L). Um zu verstehen, wie Prolog hier vorgeht, lohnt es sich, eine Anfrage im spur-Modus zu stellen: Das heißt: Sollte das erste Element der Liste das gesuchte Element E sein, dann wird Yes ausgeben. (Ausstieg aus der Rekursion). Ansonsten wird nach und nach jedes Kopfelement der Liste weggelassen und geprüft, ob jetzt das erste Element der neuen Liste das gesuchte E ist. Zwei Zeilen Programmcode! Einfach genial, oder?!! Wir benötigen für die Ausgabe von Listen bei unseren „Busfahrten“ noch zwei weitere Listenoperationen. Aufgabe 19 Die Operation anhaengen_an soll eine ein-elementige Liste [X] an eine gegebene Liste L2 hinten anhängen. Formulieren Sie die Operation mit Hilfe des appendBefehls. Die Operation an_denAnfang soll ein Element X an den Kopf einer gegebenen Liste L stellen. Testen Sie die neuen Operationen an einigen Beispielen! (Lösung auf der nächsten Seite erst anschauen, wenn keine eigene Lösung gefunden wurde.) 31 Eine Lösung für anhaengen_an : anhaengen_an([X],L2,L):-append(L2,[X],L). Eine Lösung für an_denAnfang : an_denAnfang(X,L,[X|L]). Kommen wir zurück zu unserem Spar-Buslinien-Problem. Leider konnten wir ja immer nur erfahren, ob ein eingebener Weg möglich ist oder nicht. Das wollen wir nun ändern. Das Programm spar_buslinien_alle soll uns sämtliche möglichen Wege als Liste ausgeben. So wird es klappen: an_denAnfang(X,L,[X|L]). allewege(X,X,[X]). allewege(X,Y,L):- buslinie(X,Z), allewege(Z,Y,NL), an_denAnfang(X,NL,L). Es ist auch hier ausgesprochen hilfreich, Prolog im spur-Modus bei der Lösung eines sehr einfachen Falls zuzusehen: Die Rekursion wird solange durchgeführt, bis der Ausstieg der Rekursion allewege(2,2,[2]). erreicht wird. Dann wird bekannt, was _G322 ist, nämlich [1,2]. Sehr schön sieht man auch, wie 1 in die Liste [2] vorn eingefügt wird. Drückt man nach Angabe der ersten Lösung die Enter-Taste, so sucht Prolog nach weiteren Lösungen. Hier ein kleiner Ausschnitt: Beispiel für Backtracking: da busline(7,9) nicht zu Ziel führt wird die Alternative busline(7,10) getestet. 32 Dass nun bei allewege(2,2,...) weitergesucht wird, haben wir ebenfalls dem Backtracking zu verdanken. Rein theoretisch könnte es ja den (unsinnigen) Weg 1-23-2 geben. Hier geht es dann nicht weiter und somit ist der Versuch mit buslinie(1,2) beendet und durch erneutes Backtracking werden nun noch die Versuche: und (vergeblich) durchprobiert. Erst danach gibt es keine weitere Möglichkeiten mehr und Prolog gibt No aus. Damit allewege(X,X,L) nach dem ersten Erreichen des Ziels nicht weiter verfolgt wird, sollte man die obige Regel leicht abändern: allewege(X,Y,L):- X\=Y, buslinie(X,Z), allewege(Z,Y,NL), an_denAnfang(X,NL,L). Vergleichen Sie diese Lösung im spur-Modus mit der vorherigen. Sollten Sie allerdings auf die Idee kommen, allewege(1,11,L) im spur-Modus beobachten zu wollen, dann werden Sie einige Zeit beschäftigt sein ;-)) Wieviele Spar-Buswege gibt es von Station 1 bis 11? Das erfahren Sie, wenn Sie nach jeder angegebenen Lösung durch die Enter-Taste ein Backtracking erzwingen. Es sollten 16 Lösungen sein! Welche Lösung fährt die meisten Busstationen an? Wieviele sind es? Die „Spar-Bus-Variante“ hat programmtechnisch den Vorteil, dass keine EndlosSchleifen entstehen können. Das ist eine sehr starke Einschränkung, die wir beim nächsten Problem loswerden wollen: In der oben erwähnten großen Stadt gibt es ein weiteres Sondern-Ticket. Es ist für Leute gedacht, die sich möglichst wenig Sehenwürdigkeiten entgehen lassen wollen. Die Bedingungen: • Man darf die Buslinien in alle Richtungen benutzen. • Allerdings darf man nicht zweimal die gleiche Busstation anfahren. • Man darf mit dem Ticket nur die schwarze oder die blaue Linie benutzen. So sieht der Busplan jetzt aus: 33 Damit man nicht alle Buslinien in die Gegenrichtung neu aufführen muss, kann man eine Regel verwenden (siehe auch weiter oben): bus(X,Y):- buslinie(X,Y); buslinie(Y,X). Die blauen Linien müssen natürlich (in einer Richtung) noch nachgetragen werden: buslinie(12,13). etc Es ist Ihnen sicher klar, dass die Menge der Lösungen selbst bei diesem kleinen Plan rapide zunimmt. Der Einfachheit halber schreiben wir zunächst nur ein Programm, dass uns mit Yes oder No angibt, ob ein bestimmter Weg möglich ist. Dies scheint auf den ersten Blick ziemlich sinnlos, denn dank der Farbgebung sind wir mindestens genau so schnell wie der Computer. Allerdings wollen wir mit dem Programm nur einfacher zu unserer eigentlichen Aufgabe kommen: Alle Listen von Station a nach Station b ausgeben zu lassen. Wir greifen dabei auf die Ihnen schon bekannten Operationen anhaengen_an und enthalten zurück. Lesen Sie nötigenfalls die Erklärungen weiter oben nochmals durch. (buslinie.pl) anhaengen_an([X],L2,L):- append(L2,[X],L). enthalten(E,[E|_]). enthalten(E,[_|L]):- enthalten(E,L). wege(X,X,_). wege(X,Y,L):- bus(X,Z), not(enthalten(Z,L)), anhaengen_an([Z],L,NL), wege(Z,Y,NL). (Lesen Sie, falls nötig, die Funktionsweise und Bedeutung für anhaengen_an/3 und enthalten/2 nochmals durch.) 34 Wie Sie erkennen wird eine Liste L mitgeführt. Zu Beginn besteht die L nur aus der ersten Busstation, also L = [x]. Wenn eine Zwischenstation Z angefahren wird, die noch nicht in der Liste L enthalten ist, dann wird sie in L hinten hinzugefügt. Schauen wir Prolog bei einer einfachen Anfrage im spur-Modus „über die Schulter“: Beobachten Sie, wie die Weganfrage rekursiv bearbeitet wird. Dies geht so lange bis der Interpreter an die Zeile wege(7,7,[5,6,7]), den Rekursionsausstieg kommt. Da bis hierher alles widerspruchsfrei ablief, bekommt man ein Yes als Antwort. Man erkennt ebenfalls, dass die Liste der Busstationen, die von Station 5 nach Station 7 führen, bereits vorhanden ist. Man muss sie nur noch auslesen lassen... Daher muss der Ausstieg der Reduktion einfach mit einem Schreibbefehl verknüpft sein. Das neue Programm buslinie_eine.pl unterscheidet sich daher nur wenig vom obigen Programm: wegeliste(X,X,L):-write(L). wegeliste(X,Y,L):- bus(X,Z), not(enthalten(Z,L)), anhaengen_an([Z],L,NL), wegeliste(Z,Y,NL). (Die Programmzeilen anhaengen_an und enthalten sind unverändert und wurden daher hier nicht aufgeführt!) Aufgabe 8 • Testen Sie das Programm mit der Anfrage wegeliste(2,10,[2]). 35 • Beobachten Sie mit dem trace-Modus eine sehr einfache Anfrage. Leider bekommt man höchstens eine Lösung, denn Prolog geht nach der Ausgabe der gefundenen Liste L alle Rekursionsschritte rückwärts und stellt schlussendlich zufrieden fest, dass die Anfrage mit Yes beantwortet werden kann. Das muss nicht sein! Mit einem simplen Trick liefert Prolog auch alle Lösungen auf einmal (Programm buslinieListeAlle.pl): wegeliste(X,X,L):- write(L),nl. wegeliste(X,Y,L):- bus(X,Z), not(enthalten(Z,L)), anhaengen_an([Z],L,NL), wegeliste(Z,Y,NL), fail. Das Prädikat fail führt dazu, dass Prolog am Ende einer Rekursion scheitert. So erzwingt man ohne Betätigung der Enter-Taste ein Backtracking. Neu ist das Prädikat nl. Es bedeutet einfach: „new line“. Eine Anfrage sieht dann zum Beispiel so aus: wegeliste(1,11,[1]). Mit dem obigen Busplan zählt Prolog bei dieser Anfrage 28 Möglichkeiten auf. Sollten Ihnen dies alles recht rätselhaft vorkommen, so lesen Sie gleich jetzt auf Seite 212 den Versuch einer Klarstellung...... Aufgabe 9 • Vergrößern Sie die Anzahl der Busstationen und lassen Sie sich dann durch eine Anfrage wegeliste(a,b,[a]) alle möglichen Wege ausgeben. 36 • Fügen Sie einige Einbahnstraßen hinzu. (Wie ist das realisierbar?) Stellen Sie erneut eine Anfrage! In der Mathematik heißen Konstrukte, wie im Bild oben, Graph. Die Punkte (hier Bushaltestellen) sind Knoten, die Linien dazwischen heißen (gerichtete) Kanten. Der Zweig der Mathematik, der sich mit Graphen beschäftigt, heißt Graphentheorie. Die vielleicht bekanntesten Anwendungen dieser Theorie sind der Vierfarbensatz und das Königsberger Brückenproblem. Der Vierfarbensatz gesagt, dass man zur Einfärbung einer Landkarte nur vier Farben benötigt, das Königsberger Brückenproblem untersucht, ob es einen Rundweg gibt, bei dem man alle Brücken in Königsberg (heute Kaliningrad) nur genau einmal verwendet, aber alle Brücken benutzt hat. Es ist dabei gleichgültig, wo man den Weg beginnt und wo man ihn beendet. Hier kann man es ausprobieren: http://www.matheprisma.uni-wuppertal.de/Module/Koenigsb/ Das nebenstehende Bild (Wikipedia) zeigt die Situation zur Zeit Leonhard Eulers (1736), der das Problem mit Hilfe der Graphentheorie gelöst hat. Sie können es auch lösen, - mit Prolog, ohne etwas von Graphentheorie verstehen zu müssen. Zunächst verwandeln wir die geografische Situation in einen Graphen wie bei den Bushaltestellen oben. 37 Im Gegensatz zum Busproblem darf hier jeder Knoten bliebig oft vorkommen. Dafür darf und muss jede Verbindung (Brücke) genau einmal benutzt werden. Das bedeutet, dass wir dieses Mal die Zahlen für die Brücken in einer Liste mitführen müssen. Da es hier genau 7 Brücken sind, muss Prolog bei jeder benutzen Verbindung eine abstreichen. Wer die Busprobleme verstanden hat, wird zweifelsohne auch hier eine Lösung finden: %% Königsberger Brückenproblem ueber(1,s,i). ueber(2,s,i). ueber(3,n,i). ueber(4,n,i). ueber(5,n,o). ueber(6,i,o). ueber(7,s,o). bruecke(B,X,Y):- ueber(B,X,Y); ueber(B,Y,X). anhaengen_an([X],L2,L):-append(L2,[X],L). enthalten(E,[E|_]). enthalten(E,[_|L]):- enthalten(E,L). weg(_,_,L,0):- write(L),nl. weg(X,Y,L,N):- bruecke(B,X,Z), not(enthalten(B,L)), anhaengen_an([B],L,NL), M is N-1, weg(Z,Y,NL,M). weg_von(Start):- weg(Start,_,[],7), fail. %% Um Backtracking zu erzwingen Vergleichen Sie mit dem Programm buslinie_alle_schnell und Sie werden feststellen, dass nur wenig geändert werden muss. Aus buslinie wurde ueber. Bei ueber muss ein weiterer Parameter mitgenommen werden, weil es mehrere Verbindungen zwischen zwei benachbarten Knoten geben kann. bruecke entspricht bus und sorgt dafür, dass die Verbindung in beide Richtungen zugelassen wird. Bei der Rekursion fällt nur die neue Zeile M is N-1 auf. Diese wird, wie oben schon erwähnt, gebraucht, um auch alle 7 Brücken zu benutzen, indem von N=7 auf 0 runtergezählt wird (Ausstieg der Rekursion). 38 Stellen Sie zunächst eine Anfrage, deren Antwort Sie kennen. Beispielsweise: Wie kommt man von Gebiet s nach Gebiet o, wenn man genau 3 Brücken benutzen darf: Aber das interessiert uns ja nicht. Wir wollen wissen, ob man einen Rundweg finden kann, bei dem jede Brücke genau einmal benutzt wird. Man müsste also viele Anfragen der Form ?- weg(Start,Ziel,[ ], 7). stellen. Da das Ziel gleichgültig ist, verwenden wir bequemer die letzte Zeile mit der Regel : weg_von(Start). So kann man die Anfrage, ob es einen Weg vom Südufer aus gibt, so formulieren: weg_von(s). Und? Gibt es eine Lösung? Aufgabe 10 Es gibt allerdings noch andere Fälle, von denen die zwei typischen Vertreter hier dargestellt sind: Im ersten Bild handelt es sich um die Stadt Meiningen mit dem Fluss Werra, im zweiten Bild ist es Passau (im Jahre 2050, denn die rot eingetragenen Brücken gibt es noch nicht....). Hier fließen die Flüsse Ilz vom Norden und Inn vom Süden in die Donau. Ändern Sie das Königsberger-Brücken-ProblemProgramm ab und untersuchen Sie, ob man in Meiningen oder Passau das tun kann, was in Königsberg offensichtlich nicht möglich war. Achten Sie besonders darauf, ob es darauf ankommt, wo man startet! Falls es eine Möglichkeit gibt, - ist dies dann die Einzige? (oben: Meiningen) (rechts: Passau) Kommen wir zurück zu unseren Buslinien. Prolog sagt uns bisher, welche Möglichkeiten es gibt, um von A nach B zu kommen. Das mag ja ganz hilfreich sein, perfekt ist es jedenfalls noch nicht. Gibt man bei einem Navigationsgerät oder bei einem Routenplaner Start und Ziel ein, so listet einem das Programm ja auch nicht 39 alle Möglichkeiten auf, wie das Ziel zu erreichen ist! Vielmehr informiert man das Programm zuvor, welche Vorgaben bei der Planung zu beachten sind: Schnellste Strecke, kürzeste Strecke, Stecke ohne Autobahnen, Strecke mit den meisten Sehenswürdigkeiten ….. Damit wird aus dem Graph ein „gewichteter Graph“. Zum Beispiel kann das „Gewicht“ die Streckenlänge zwischen den Knoten sein: Beachten Sie, dass Graphen in der Regel (wie oben) nicht maßstabsgetreu sind. Man hat daher keine „Landkarte“ vor sich und entsprechend schwer fällte es einem, selbst in einem so extrem einfachen Beispiel, die kürzeste Strecke zwischen Knoten 1 und Knoten 9 zu finden. Finden Sie die kürzeste Strecke? Zu Beginn wollen wir uns damit zufrieden geben, dass Prolog einfach nur die Streckenlänge zu allen gefundenen Strecken mit angibt. Und das geht so: %% Streckenlaengen linie(1,2,55). linie(1,3,12). linie(1,4,70). linie(2,5,38). linie(2,6,72). linie(3,6,82). linie(3,4,94). linie(4,6,69). linie(5,6,58). linie(6,7,83). linie(6,8,65). linie(7,9,51). linie(7,10,45). linie(8,9,26). linie(9,11,47). linie(9,10,22). linie(10,11,51). strecke(X,Y,Z):- linie(X,Y,Z); linie(Y,X,Z). 40 anhaengen_an([X],L2,L):-append(L2,[X],L). enthalten(E,[E|_]). enthalten(E,[_|L]):- enthalten(E,L). %%Beispielanfrage: laenge_weg_von_bis(1,11). wegeliste(X,X,L,N):-write(N), write(L),nl. wegeliste(X,Y,L,N):- strecke(X,V,Z), not(enthalten(V,L)), ahaengen_an([V],L,NL), M is N + Z, wegeliste(V,Y,NL,M ), fail. laenge_weg_von_bis(X,Y):- wegeliste(X,Y,[X],0). Aus buslinie(X,Y) wird jetzt linie(X,Y,N). Wie zu erwarten ist in N die Entfernung gespeichert. is ist ein built-in Prädikat. Der rechte Term wird berechnet und an die linke Variable (M) gebunden. Falls die linke Seite keine Variable ist, dann wird ein Vergleich durchgeführt. Weshalb schreibt man nicht M = N+ Z , wie man es in Delphi (mit Doppelpunkt) machen würde? Ganz einfach: Bei der obigen Schreibweise testet Prolog lediglich, ob der Term links mit dem Term rechts „matcht“ oder nicht. Wie wäre es mit M =:= N + Z ? Geht auch nicht, da hier beide Seiten instanzierte Variable sein müssen. M wird aber erst erzeugt. Die letzte Zeile ist wie im letzten Beispiel aus reiner Bequemlichkeit entstanden. Die beiden letzten Parameter von wegeliste sind redundant. Denn natürlich soll der erste Eintrag in der Liste L der Startort sein. Und außerdem ist beim Start die Streckenlänge N noch Null. Damit wir der Wirklichkeit eines Routenplaners wenigstens ein wenig gerechter werden, soll der Graph vergrößert werden: (keine Einbahnstraßen) 41 Aufgabe 11 Vervollständigen Sie die Liste der „Linien“ anhand des obigen gewichteten Graphen. Erstellen Sie dann den vollständigen Quelltext zu laenge_weg_von_bis. Wie lang ist die kürzeste und wie lang ist die längste Strecke zwischen den Orten 1 und 12? Wieviele Wege gibt es von 1 nach 12 ? (Zur Erinnerung: Ein Knoten darf höchstens einmal angefahren werden!) Da es einige tausend Wege von 1 nach 12 gibt, ist kaum anzunehmen, dass Sie darunter den kürzesten Weg ausfindig machen konnten. Es muss also, wie in den Programmen für Routenplanung auch, eine Grenze S festgelegt werden, ab der eine weitere Verfolgung des Weges abgebrochen wird. (Man kann auf die Eingabe einer Obergrenze auch verzichten, wenn man das Zwischenergebnis mit der letzten Streckenlänge vergleicht. Sobald dies größer als der letzte Wert ist, wird abgebrochen. Ist aber die aktuelle Streckenlänge kürzer, als das letzte Ergebnis, so wird die kürzere Strecke als neuer Referenzwert übernommen. Dieses Verfahren der immer neuen Speicherbelegung für das Minimum wird im Anschluss behandelt.) Bei geschickter Wahl der Obergrenze S wird die Menge der Möglichkeiten deutlich geringer. Und so kann man es realisieren: 42 wegeliste(N,X,X,L,S):- append([N],L,NL),print(NL),nl. wegeliste(N,X,Y,L,S):- strecke(X,V,Z), not(member(V,L)), anhaengen_an([V],L,NL), M is N + Z, M<S, wegeliste(M,V,Y,NL,S), fail. weg_von_bis_kleiner(X,Y,S):- wegeliste(0,X,Y,[X],S). Durch die Bedingung N < S werden nur die Strecken verfolgt und anschließend ausgegeben, deren Länge kleiner als die vorgegeben Größe S ist. Beachten Sie, dass member(V,L) die prolog-eigene Anfrage für unserer Anfrage enthalten(V,L) ist. Wir hoffen dabei auf einen kleinen Geschwindigkeitsvorteil!! Anfrage aus Aufgabe 11: ?- weg_von_bis_kleiner(1,12,250). [219, 1, 2, 5, 13, 12] [217, 1, 2, 14, 13, 12] [249, 1, 3, 6, 8, 9, 12] [245, 1, 15, 2, 5, 13, 12] [243, 1, 15, 2, 14, 13, 12] No Das sieht doch jetzt bedeutend übersichtlicher aus! Dennoch, - so richtig befriedigend ist auch diese Lösung nicht. Man muss die Obergrenze S schon sehr genau erraten, damit die Zahl der Lösungen einigermaßen gering bleiben. Aufgabe 12 Erstellen Sie den Programmcode weg_von_bis_min, der auch bei noch größerer Punktemenge schnell und übersichtlich den kürzesten Weg berechnet. Testen Sie das Programm durch einige Anfragen. 43 Die hierfür nötige Datenbank für Prolog können Sie aus dem Tauschverzeichnis laden. Die obige Aufgabe ist alles andere als einfach. Daher werden weiter unten einige Ideen dazu vorgestellt. Zunächst einige grundsätzliche Bemerkungen zum Thema „Suchen in Graphen“. Solange man die Punkte (Knoten) nicht mit Koordinaten versieht und die Verbindungen auch nicht „gewichtet“, wie bei uns durch die „Entfernung“ zwischen den Punkten, ist jeder Weg von A nach B gleich zu werten. Können Sie sich in etwa vorstellen, wieviele verschiedene Wege es etwa im obigen Bild von Knoten 1 nach Knoten 51 gibt? Obwohl innerhalb eines Weges kein Knoten zwei mal vorkommen darf, ist die Zahl der Wege unvorstellbar groß. Prolog meldet in der Situation (20 Knoten) S.210 genau 1364 Wege. Eine Warnung vorweg: Wenn Sie jetzt Prolog die Anzahl der Wege mit 80 Knoten (Bild oben) ausrechnen lassen wollen, dann schauen Sie besser erst morgen nach dem Ergebnis! Auf diesem „niederen Struktur-Niveau“ muss jede Suche erschöpfend sein. Es darf kein Weg ausgelassen werden! Dass dies eine sehr theoretische Fragestellung ist, 44 wird Ihnen klar sein: Die Knoten der Graphen in einem Navigationsgerät verfügen über Koordinaten und damit automatisch auch über „Gewichtungen“ der Verbindungen. Sonst wäre eine Anfrage bezüglich einer „kürzesten Verbindung“ zwischen A und B schlicht sinnlos. Es gibt hier zwei Arten nach allen Wegen zwischen A und B zu suchen. Alle beiden werden auch „Blindsuchen“ genannt, weil sie einfach alles „blind“ durchspielen müssen, anstatt „sehenderweise“ manche Wege als unsinnig ad acta legen zu dürfen. Was wir bisher gemacht haben, als wir in den obigen Anfragen alle Wege von A nach B gesucht haben, war eine Tiefensuche oder DFS (depth first search). Der Interpreter selbst von Prolog arbeitet mit der DFS. Deshalb ist Prolog bei dieser Art der Suche allen anderen Programmiersprachen überlegen. Weshalb der Ausdruck „Tiefensuche“? In der Tat ist die Bezeichnung sinnvoll gewählt, denn Prolog geht bei der Suche eines Weges zwischen A und B zunächst ganz in die „Tiefe“, - bis es nicht mehr weiter geht! Und dann beginnt das Backtracking. In nebenstehendem einfachen Graphen wird der Algorithmus deutlicher: Es ist ein Weg von Knoten 6 nach Knoten 5 zu finden. Zunächst geht es bis Knoten 2. Wählt Prolog jetzt als nächstes Knoten 1, so werden mit mehrfachem Backtracking die blauen Linien durchlaufen. Schließlich landet man wieder bei Knoten 2 und dieses mal wird der richtige Weg zu Knoten 5 gewählt. Wie würde Prolog jetzt die anderen möglichen Wege durch DFS finden? Welche Nachteile bzw. welche Vorteile hat diese Methode? Rufen wir uns nochmal ins Gedächtnis: Es müssen in jedem Fall alle Wege getestet werden. Kein Alogrithmus kann uns dies ersparen. Möglich ist aber, dass durch ein geschickteres Vorgehen, der Prozessor weniger Zeit für die gleiche Arbeit benötigt. Stellen Sie sich einen PC mit einem riesigen RAM vor. Dann ist die Breitensuche oder BFS (breadth first search) ein interessantes Konzept. Hier die gefundenen Wege als Ganzes auf den Speicher gelegt und nach und 45 nach durch längere Wege ersetzt. Ein Bild soll dies veranschaulichen: Gleiche Aufgabe wie oben: Es soll ein Weg zwischen Knoten 6 und Knoten 5 gefunden werden. Prolog stellt nun anhand der Datenbank fest, welche Wege von Knoten 6 wegführen und notiert sich dann die Wege [6,2], [6,8] und [6,9]. Es entsteht also eine Liste von Wegen:[ [6,2], [6,8] und [6,9] ]. Diese Liste wird nach folgender Regel ausgebaut: Nehme der Reihe nach jeden Weg und versuche ihn auszubauen. Dann entsteht aus [6,2] : [6,2,5], [6,2,1] und [6,9,8]. Die drei werden jetzt in der Liste gespeichert und dafür wird [6,2] gestrichen. Wenn alle zwei-Knoten-Wege ersetzt sind, sieht die neue Liste so aus: [ [6,2,5], [6,2,1], [6,9,8], [6,9,7] ]. Man kann sich leicht vorstellen, dass bei vielen Knoten die Liste sehr schnell sehr groß werden kann! Ist der Prozessor in der Lage, parallel viele Wege gleichzeitig zu prüfen, so ist dieses Verfahren natürlich deutlich schneller! Ein Weg von 6 nach 5 ist hier bereits beim zweiten Anlauf gefunden, - oben waren es neun Schritte! Dennoch: Da ja alle Wege gefunden werden müssen, ist der Geschwindigkeitsvorteil aber nur bei Parallelverarbeitung vorhanden. Hier nun der Quellcode für eine Tiefensuche. Wenn Sie die Anfrage auf eine Knotenmenge größer 20 anwenden, dann wird Ihnen der Stack Probleme machen. Aber testen Sie selbst: strecke(X,Y,Z):- linie(X,Y,Z); linie(Y,X,Z). % Wege in Nachfolgewege expandieren expandiere([E|R], Wege) :findall(X,strecke(E,X,_), Nachfolger), e([E|R], Nachfolger, [], Wege). % Hilfsprädikat für expandiere e(_, [], X, X). e(Weg, [E|R], Bisher, Ergebnis) :not(memberchk(E,Weg)), e(Weg, R, [[E|Weg]|Bisher], Ergebnis). e(Weg, [E|R], Bisher, Ergebnis) :memberchk(E,Weg), e(Weg, R, Bisher, Ergebnis). % Tiefensuche % ts(Superliste, Ziel, Ergebnisweg) ts([[Ziel|R]|_RestWege], Ziel, [Ziel|R]):- Loesung = [Ziel|R],print(Loesung),nl. ts([Weg1 | RestWege], Ziel, Weg) :expandiere(Weg1, NachfolgeWege), append(NachfolgeWege,RestWege, NeueSuperliste), ts(NeueSuperliste, Ziel, Weg). 46 % bequemer Aufruf tiefensuche(Start, Ziel, Weg) :- ts([[Start]], Ziel, Gew),fail. Schnell ist der Code ohne Zweifel, - aber leider nicht verwendbar für größere Knotenmengen! Ganz anders sieht die Sache aus, wenn die Kanten (Verbindungen) „gewichtet“ sind. Die „Gewichte“ sind bei uns oben die Entfernungen in Kilometern zwischen den Knoten. Jetzt kann durch bestimmte Nebenbedingungen (zum Beispiel darf der Weg nicht länger sein als 100 km) die Rechenarbeit reduziert werden. Eine solche Suche nennt man eine heuristische Suche. Aufgabe 13 Überlegen Sie sich kurz, welche unserer Prolog-Anfragen heuristisch waren und welche nicht! Wenn man alle Wege von A nach B sucht, die eine gewisse Länge nicht überschreiten, ist dann die Breiten- oder die Tiefensuche günstiger? Wie könnte man bei einer heuristischen Tiefensuche das Überlaufen des Stacks verhindern? Zurück zu Aufgabe 12. Es wurde eine Prolog-Lösung gesucht, die auch bei einer größeren Anzahl von Knoten, relativ schnell den kürzesten Weg bestimmt. Der folgende Code stammt von Mario Schreiner. Er fand heraus, dass man auch in Prolog globale Variablen verwenden kann. initGlobals:- nb_setval('KuerzesterWeg',0), nb_setval('KuerzesterWegPunkte',[]). Das nutzt er für eine heuristische Tiefensuche aus, indem er die Länge des ersten erfolgreichen Weges von A nach B in die globale Variable schreibt und fortan alle längeren Wege nicht weiter verfolgt. Gleichzeitig wird aber ein kürzerer und erfolgreicher Weg die globale Variable erneut verändern, so dass zum Schluss der kürzeste Weg übrig bleibt: Der komplette sehr gut kommentierte Code kann im Tauschverzeichnis geladen werden. ( kuerzesterWeg80Mario.pl ) Die Optimierung ist noch nicht perfekt, denn eine Anfrage wie kuerzesterWeg(1,25,Weg). dauert deutlich mehr als 10 Minuten. Eine andere Idee stammt von Thomas Fröhle. Die entscheidenden Zeilen sind diese: kurz(X,Y):setof(N,weg(X,Y,N,[X],[0]),Liste), 47 % setof sucht alle möglichen Lösungen für die Strecke X Y und speichert die Lösung %(N) in einer Liste. Diese ist gleich schon so sortiert, das die kürzeste Strecke vorne %steht Sein gesamter, ebenfalls sehr gut kommentierter Code, kann im Tauschverzeichnis geladen werden. Mit der Geschwindigkeit war Thomas nicht zufrieden und entwickelte daher ein ganz anderes Konzept, das davon ausgeht, dass die Knoten Koordinaten haben. Davon später mehr. Inspiriert von den Ideen von Mario und Thomas kann man auf einen sehr schnellen Code kommen: initGlobals:- nb_setval('Minimum',1000). strecke(X,Y,Z):- linie(X,Y,Z); linie(Y,X,Z). istkleiner(X,Y,Z):- (X>=Y ,Z is Y) ; (Y>X , Z is X). anhaengen_an([X],L2,L):- append(L2,[X],L). %%Beispielanfrage: minweg(2,53). wegeliste(N,X,X,L):- append([N],L,NL),print(NL),nl, nb_setval('Minimum',N). wegeliste(N,X,Y,L):- nb_getval('Minimum',Minimum), strecke(X,V,Z), not(member(V,L)), anhaengen_an([V],L,NL), M is N + Z, M<Minimum, wegeliste(M,V,Y,NL), fail. minweg(X,Y):- initGlobals, nb_getval('Minimum',Minimum), wegeliste(0,X,Y,[X]). Damit werden wirklich nur die benötigten Zweige berechnet. Damit man beobachten kann, wie die Streckenlänge immer kleiner wird, werden alle kürzerwerdenden Wege ausgegeben. 29 ?- minweg(2,25). [970, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 27, 29, 31, 32, 33, 25] [964, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 27, 29, 32, 33, 25] [905, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 27, 29, 28, 33, 25] [884, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 28, 33, 34, 22, 25] [810, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 28, 33, 25] [750, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 22, 25] [698, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 24, 26, 25] 48 [670, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 27, 29, 28, 33, 25] [629, 2, 5, 6, 7, 9, 11, 12, 13, 14, 38, 27, 26, 25] [627, 2, 5, 6, 7, 9, 11, 12, 13, 39, 27, 29, 28, 33, 25] [586, 2, 5, 6, 7, 9, 11, 12, 13, 39, 27, 26, 25] [574, 2, 5, 6, 7, 9, 11, 12, 40, 30, 31, 32, 33, 25] [560, 2, 5, 6, 7, 9, 11, 12, 40, 30, 27, 26, 25] [527, 2, 5, 6, 7, 9, 11, 12, 40, 30, 29, 28, 33, 25] [521, 2, 5, 6, 7, 9, 12, 40, 30, 29, 28, 33, 25] [517, 2, 5, 6, 8, 9, 11, 12, 40, 30, 27, 26, 25] [484, 2, 5, 6, 8, 9, 11, 12, 40, 30, 29, 28, 33, 25] [478, 2, 5, 6, 8, 9, 12, 40, 30, 29, 28, 33, 25] [469, 2, 5, 6, 8, 13, 14, 38, 27, 26, 25] [467, 2, 5, 6, 8, 13, 39, 27, 29, 28, 33, 25] [426, 2, 5, 6, 8, 13, 39, 27, 26, 25] [408, 2, 5, 13, 14, 38, 24, 26, 25] [380, 2, 5, 13, 14, 38, 27, 29, 28, 33, 25] [339, 2, 5, 13, 14, 38, 27, 26, 25] [337, 2, 5, 13, 39, 27, 29, 28, 33, 25] [296, 2, 5, 13, 39, 27, 26, 25] [242, 2, 15, 36, 21, 22, 25] [215, 2, 15, 36, 24, 26, 25] [211, 2, 37, 24, 26, 25] Rechenzeit bei 80 Knoten: Weniger als eine Sekunde! Ob das Verfahren bei mehreren tausend Knoten noch sinnvoll funktioniert, darf bezweifelt werden. Hier kommen nun Koordinaten ins Spiel. Man muss sich zunächst klar machen, dass damit die Information in der Datenbank zunimmt. Denn jetzt ist es möglich, von „Richtung“ zu sprechen. Damit fallen viele Knoten weg, die in der „falschen Richtung“ liegen. Durch die Reduktion einer sehr großen Knotenmenge auf eine kleine, wird die Rechenzeit drastisch verkürzt. Allerdings kann man nicht immer sicher sein, auch wirklich den kürzesten Weg gefunden zu haben! Man muss sich daher bei einer sehr großen Knotenmenge mit einer Näherungslösung zufrieden geben. Hier nun der Kommentar von Thomas Fröhles zu seinem Quellcode (Greedy_ThomasFroehle.pl). Der Greedy-Algorthmus ist eine optimierte Tiefensuche. Weitere Informationen dazu: http://de.wikipedia.org/wiki/Greedy-Algorithmus Daten: punkt (ID, X-Wert, Y-Wert) Punkte in Koordinatenform linie (PunktA, PunktB) Verbindungen zwischen Punkten Unterprogramme: wurzel (X, Y, MaxAbw) Berechnet Y, die Quadratwurzel von X mit der Abweichung MaxAbw 49 abstand2p (P, Q, Abstand) Berechnet den Abstand zwischen P und Q strecke(PunktA,PunktB,Strecke) Checkt, ob eine Verbindung zwischen PunktA und PunktB existiert und berechnet mit abstand2p den Abstand zwischen den Punkten Greedyprogramme: strecke_luftlinie (Anfang, Ende, Punkte, H) Weist Punkte einen H- Wert zu: 1. Suche alle Punkte, die eine Verbindung zu Anfang haben. 2. H- Wert ist die Summe aus der Strecke von Anfang zu Punkte und der Luftlinie von Punkte zu Ende suche (Start, Ziel, Punkt) Sucht den Punkt der den geringsten H- Wert hat. Wegfindungsprogramme: weg (X, Y, N, L, R, L2) X:Startpunkt Y:Endpunkt N:Bisher zurückgelegter Weg L:Bisher zurück gelegte Wegliste R:Maximaler Weg L2:Lösung (N,L) Zuerst wird überprüft ob Ziel erreicht wurde (X= : =Y). Wenn Ziel erreicht wurde speicher den Weg als kürzesten in der Globalen Variablen und speicher N und L in L2 Wenn Ziel noch nicht erreicht wurde: 1. Überprüfe, ob der bisher zurückgelegte Weg größer als der maximale Weg ist. 2. Wenn Ja: Weg abbrechen 3. Wenn Nein: Strecken, die von X ausgehen auslesen, Punkt V wird als nächstes überprüft 4. Falls V noch nicht in L enthalten ist: V in L speichern 5. Falls noch kein Weg gefunden wurde ist die Globale Variable Null -> Rekursion vom Punkt Vaus 6. Falls schon ein Weg gefunden wurde wird die bisherige Weglänge mit der Luftlinie von V bis Y addiert und mit der Globalen Variable Kurz verglichen -> wenn größer: abbrechen 7. Wenn kleiner -> Rekursion. faktor (X, Y, R) X:Startpunkt Y:Endpunkt R:Faktor Globale Variablen erstellen bzw. Null setzen. 50 Die maximale Weglänge ist der Faktor mal Luftlinie zwischen Start und Endpunkt. Finde alle Lösungen des Weg-Programms, mit den Variablen X, Y und Max, die Lösungen, werden in der Liste Loesung gespeichert. Falls diese Liste leer ist, wird der Faktor um 0,1 erhöht und Faktor neugestartet, sonst wird die Lösung ausgegeben, die Globalen Variablen kurz und liste sind jetzt mit dem kürzesten Weg gefüllt -> Diese werden dann aufgerufen. kurz (X, Y) Programm, das vom Benutzer ausgeführt wird, ruft faktor mit dem Anfangswert 1,5 auf. Die Greedy-Suche basiert auf dem gleichnamigen Algorithmus. Die Idee dahinter ist, dass der nächste Punkt der „vielversprechendste“ ist. Der Greedy Algorithmus ist erst ab einer großen Anzahl an Punkten und Verbindungen schneller (siehe Wenig_Punkte.pl und Viele_Punkte.pl). Bei wenigen Punkten ist die normale Suche schneller. weg_greedy (X, Y, N, L, R, L2) Basiert auf dem weg- Programm. Die einzigeÄnderung: Es wird nicht willkürlich eine Strecke gesucht von X aus, sondern per suche(X, Y, V) ausgeführt (siehe oben) faktor_greedy (X, Y, R) und greedy (X, Y) Sind die gleichen Programme wie bei der normalen Suche ANMERKUNG: Die Werte 1,5 als Standardfaktor und 0,1 als Erhöhungsschritt sind natürlich individuell einstellbar, muss man ausprobieren, je nachdem wie viele Verbindungen existieren. % Autor: Thomas Fröhle % Datum: 08.05.2008 wurzel(X,Res,MaxAbw) :wurzelberechnung(X,Res,_,MaxAbw), !. wurzelberechnung(X,Res,TRes,MaxAbw) :var(TRes), TRes1 is X / 2, wurzelberechnung(X,Res,TRes1,MaxAbw). wurzelberechnung(X,Res,TRes,MaxAbw) :Quadrat is TRes * TRes, LowBound is X - MaxAbw, HighBound is X + MaxAbw, LowBound =< Quadrat, Quadrat =< HighBound, Res = TRes, !. wurzelberechnung(X,Res,TRes,MaxAbw) :Abw is (X - TRes * TRes) / (2 * TRes), TResNeu is TRes + Abw, wurzelberechnung(X,Res,TResNeu,MaxAbw). 51 abstand2p(P,Q,Abstand):punkt(P,X1,Y1), punkt(Q,X2,Y2), Z is ((X2-X1)*(X2-X1)+(Y2-Y1)*(Y2-Y1)), wurzel(Z,Abstand,1). strecke(PunktA,PunktB,Strecke):(linie(PunktA,PunktB); linie(PunktB,PunktA)), abstand2p(PunktA,PunktB,Strecke). strecke_luftlinie(Anfang,Ende,Punkte,H):strecke(Anfang,Punkte,Strecke), abstand2p(Punkte,Ende,Luftlinie), H is Strecke + Luftlinie. suche(Start,Ziel,Punkt):setof(Z,strecke_luftlinie(Start,Ziel,Z,_),Liste), append([Punkt|_],_,Liste). weg_greedy(X,Y,N,L,R,L2):- (X=:=Y,nb_setval('Kurz',N), nb_setval('Liste',L),append([N],L,L2)) ; (N>R -> fail; ( suche(X,Y,V), abstand2p(X,V,Strecke), not(member(V,L)), append(L,[V],NL), Zwischensumme is N + Strecke, nb_getval('Kurz',Kurz), (Kurz>0 -> ((abstand2p(V,Y,Luft), Z_Luft is Luft + Zwischensumme, Z_Luft > Kurz)->fail; weg_greedy(V,Y,Zwischensumme,NL,R,L2)) ; weg_greedy(V,Y,Zwischensumme,NL,R,L2) ))) . faktor_greedy(X,Y,R):nb_setval('Kurz',0), nb_setval('Liste',[]), abstand2p(X,Y,Luft), Max is Luft*R, findall(L,weg_greedy(X,Y,0,[X],Max,L),Loesung), length(Loesung,N), (N =:= 0) -> (R_neu is R + 0.1,faktor_greedy(X,Y,R_neu)) ; 52 (nb_getval('Kurz',Laenge), nb_getval('Liste',Weg), write('Kuerzester Weg:'),nl, write(Weg),nl, write('Laenge Des Weges:'),nl, write(Laenge) ). greedy(X,Y):- faktor_greedy(X,Y,1.5). Zum Testen hat Thomas 2500 Punkte und fast 30000 Verbindungen erzeugt. (Quellcode: Viele_Punkte.pl). Die Wegsuche greedy(1,2455). dauert auf einem schnellen Rechner zirka 2 Minuten. Das ist ein hervorragendes Ergebnis! Allerdings ist der Greedy-Algorithmus ein Näherungs-Algorithmus. Mit anderen Worten: Das Ergebnis ist möglicherweise nicht das kürzeste Ergebnis! 53 6. Anhang: Hilfestellung zu Prolog Es ist sehr wahrscheinlich, dass Sie anfangs mit Prolog einige Schwierigkeiten haben werden. Das liegt zum Einen daran, dass in allen bisherigen Programmiersprachen Speicherzuweisungen und anschließene Operationen, den Speicherinhalt betreffend, durchgeführt wurden. Eine Zeile wie: X:= 5; besagt, dass die Integerzahl 5 in den Speicher namens X geschrieben wurde. An späterer Stelle kann man auf diesen Wert erneut zugreifen. Solche Speicherzuweisungen gibt es in Prolog nicht! Wenn man beispielsweise X is 5. schreibt, so bedeutet dies nur, dass in dieser Zeile die Variable X mit 5 verknüpft ist. Vor oder nach der Anfrage, die X is 5 enthält, ist die Verknüpfung unbekannt. Prolog „rechnet“ auch nicht, wie wir es etwa von C# gewohnt sind. In Anfragen wird nur versucht, die Anfrage mit dem Datenbestand abzugleichen (= matchen). Dabei greift Prolog im Wesentlichen nur auf Rekursionen zurück. Schleifen gibt es in Prolog nicht. Auch „Ausgaben“ sind hier immer als Vergleich mit dem Datenbestand zu verstehen. Dazu ein Beispiel: In der Funktion anhaengen_an wird mit Hilfe der appendFunktion ein einzelnes Listenelement an Liste L2 hinten anghängt. Das Ergebnis ist L. Schauen Sie sich die folgenden Anfragen genau an! anhaengen_an([X],L2,L):- append(L2,[X],L). Hier gibt es keine Variablen, also kann nur Yes oder No ausgeben werden. In den drei obigen Fällen gibt es jeweils eine Variable. Sie wird, falls möglich, gematcht und ausgegeben. Da keine weitere Lösung existiert, wird schließlich ein No ausgegeben. 54 Dieser Fall ist typisch für die Arbeitsweise von Prolog: Es gibt zwei Variable und Prolog versucht mit Hilfe von „Platzhaltern“ (wie _G405) alle Möglichkeiten auszugeben. Dass dies hier zu keinem Ende führen kann, ist verständlich. Die Anfrage wurde daher angehalten. Nun stellen Sie sich aber vor, dass im Programmcode an anderer Stelle auf den Datenbestand zugegriffen wird. Und dort gibt es für LG = [_G406,_G411,_G415,a] eine Entsprechung, wie [a,b,b,a]. Dann „matcht“ Prolog die Platzhalter und gibt ein kurzes aber prägnantes Yes aus! Sehen wir uns jetzt Zeile für Zeile den Programmcode zu buslinie.pl an: %%Datenbestand: buslinie(1,2). buslinie(1,3). buslinie(1,4). buslinie(2,5). buslinie(2,6). buslinie(3,6). buslinie(4,6). buslinie(5,6). %% und so weiter bus(X,Y):- buslinie(X,Y); buslinie(Y,X). %% beide Richtungen erlaubt anhaengen_an([X],L2,L):append(L2,[X],L). enthalten(E,[E|_]). enthalten(E,[_|L]):- enthalten(E,L). wege(X,X,_). wege(X,Y,L):- bus(X,Z), 55 not(enthalten(Z,L)), anhaengen_an([Z],L,NL), wege(Z,Y,NL). Hier nun die Anfrage wege(1,6,[1]). im trace-Modus, - Zeile für Zeile kommentiert: [trace] 21 ?- wege(1,6,[1]). Call: (7) wege(1, 6, [1]) ? creep Call: (8) bus(1, _L170) ? creep Call: (9) buslinie(1, _L170) ? creep Exit: (9) buslinie(1, 2) ? creep Exit: (8) bus(1, 2) ? creep ^ Call: (8) not(enthalten(2, [1])) ? creep Call: (9) enthalten(2, [1]) ? creep Call: (10) enthalten(2, []) ? creep Fail: (10) enthalten(2, []) ? creep Fail: (9) enthalten(2, [1]) ? creep ^ Exit: (8) not(enthalten(2, [1])) ? creep Call: (8) anhaengen_an([2], [1], _L171) ? creep Call: (9) lists:append([1], [2], _L171) ? creep Exit: (9) lists:append([1], [2], [1, 2]) ? creep Exit: (8) anhaengen_an([2], [1], [1, 2]) ? creep Call: (8) wege(2, 6, [1, 2]) ? creep Call: (9) bus(2, _L231) ? creep Call: (10) buslinie(2, _L231) ? creep Exit: (10) buslinie(2, 5) ? creep Exit: (9) bus(2, 5) ? creep ^ Call: (9) not(enthalten(5, [1, 2])) ? creep Call: (10) enthalten(5, [1, 2]) ? creep Call: (11) enthalten(5, [2]) ? creep Call: (12) enthalten(5, []) ? creep Fail: (12) enthalten(5, []) ? creep Fail: (11) enthalten(5, [2]) ? creep Fail: (10) enthalten(5, [1, 2]) ? creep ^ Exit: (9) not(enthalten(5, [1, 2])) ? creep Call: (9) anhaengen_an([5], [1, 2], _L232) ? creep Call: (10) lists:append([1, 2], [5], _L232) ? creep Exit: (10) lists:append([1, 2], [5], [1, 2, 5]) ? creep Gibt es einen Weg von 1 nach 6 ? (Startpunkt 1 steht schon in der Liste) 1 ungleich 6, daher kein Abruch. Gesucht in der Datenbank: buslinie(1,_L170) Versuch: _L170 = 2 Z = 2 klappt Ergebnis: 2 ist nicht in L = [1] enthalten. Also wird 2 hinten angefügt. Somit L2 = [1,2] 2 ungleich 6, daher kein Abruch sondern rekursiver Aufruf von wege mit L = L2 Gesucht in der Datenbank: buslinie(1,_L231) Versuch _L231 = 5 Z = 5 klappt Ergebnis: 5 ist nicht in L = [1,2] enthalten. 56 Exit: (9) anhaengen_an([5], [1, 2], [1, 2, 5]) ? creep Call: (9) wege(5, 6, [1, 2, 5]) ? creep Call: (10) bus(5, _L292) ? creep Call: (11) buslinie(5, _L292) ? creep Exit: (11) buslinie(5, 6) ? creep Exit: (10) bus(5, 6) ? creep ^ Call: (10) not(enthalten(6, [1, 2, 5])) ? creep Call: (11) enthalten(6, [1, 2, 5]) ? creep Call: (12) enthalten(6, [2, 5]) ? creep Call: (13) enthalten(6, [5]) ? creep Call: (14) enthalten(6, []) ? creep Fail: (14) enthalten(6, []) ? creep Fail: (13) enthalten(6, [5]) ? creep Fail: (12) enthalten(6, [2, 5]) ? creep Fail: (11) enthalten(6, [1, 2, 5]) ? creep ^ Exit: (10) not(enthalten(6, [1, 2, 5])) ? creep Call: (10) anhaengen_an([6], [1, 2, 5], _L293) ? creep Call: (11) lists:append([1, 2, 5], [6], _L293) ? creep Exit: (11) lists:append([1, 2, 5], [6], [1, 2, 5, 6]) ? creep Exit: (10) anhaengen_an([6], [1, 2, 5], [1, 2, 5, 6]) ? creep Call: (10) wege(6, 6, [1, 2, 5, 6]) ? creep Exit: (10) wege(6, 6, [1, 2, 5, 6]) ? creep Exit: (9) wege(5, 6, [1, 2, 5]) ? creep Exit: (8) wege(2, 6, [1, 2]) ? creep Exit: (7) wege(1, 6, [1]) ? creep Yes Also wird 5 hinten angefügt. Somit L2 = [1,2,5] 5 ungleich 6, daher kein Abruch sondern rekursiver Aufruf von wege mit L = L2 Gesucht in der Datenbank: buslinie(1,_L292) Versuch _L292 = 6 Z = 6 klappt Ergebnis: 6 ist nicht in L = [1,2,5] enthalten. Also wird 6 hinten angefügt. Somit L2 = [1,2,5,6] 6 = 6 also Ausstieg aus der Rekursion und L = L2 = [1,2,5,6] Rekursion jetzt wieder schrittweise rückwärts…. mit dem Ergebnis, dass die Anfrage mit Ja beantwortet werden kann!! Nun hoffentlich ist Ihnen Prolog etwas weniger undurchsichtig.... 57