Ein Logical ist ein Rätsel, bei dem aus einer textuellen Problembeschreibung durch logisches Schliessen eine Lösung zu finden ist. Mithilfe von Tabellen, mehr oder weniger formalen Notizen und durch das Ausschlussverfahren lassen sich diese Rätsel lösen; je nach Umfang (Anzahl der relevanten Informationen) mehr oder weniger gut. Zuverlässig und effizient lassen sich Logicals mit einer logischen Programmiersprache wie Prolog lösen. Deren Gebrauch soll anhand eines konkreten Rätsels demonstriert werden. Als Implementierung wird swi-prolog verwendet:
Code: Alles auswählen
# apt install -y swi-prolog
Das folgende Rätsel ist dem Buch Logicals für Erwachsene: von leicht bis teuflisch schwer (ISBN-13: 979-8361812943) entnommen (Originaltext in Kursiv wiedergegeben):
Die Transsibirische Eisenbahn ist mit ihren 9.288 Kilometern die längste Eisenbahnstrecke der Welt. Seit 1916 verbindet sie Moskau mit Wladiwostok (an der Pazifikküste, gegenüber von Japan). Eva reist mit der Transsibirischen Eisenbahn in Richtung Osten. Ihr Zug hat soeben die Stadt Jekaterinburg verlassen. Finden Sie heraus, in welchen Städten der Zug als nächstes halten wird und wie viele Einwohner dort leben.
- Krasnojarsk ist nicht die Stadt mit der höchsten Einwohnerzahl.
- Die vierte Station ist eine Stadt mit 1.090.811 Einwohnern.
- Die zweite Haltestelle ist Omsk.
- Die dritte Haltestelle befindet sich nicht in der Stadt mit weniger als einer Million Einwohner.
- Die Haltestelle unmittelbar nach Tjumen ist in der Stadt mit einer Einwohnerzahl von 1.172.070.
Städte:
- Krasnojarsk
- Novosibirsk
- Omsk
- Tjumen
- 768.358
- 1.090.811
- 1.172.070
- 1.612.833
Prolog basiert auf zweierlei Arten von Prädikaten: Fakten und Regeln. Die oben genannten Städtenamen und Einwohnerzahlen sind Fakten. Die als fünf Punkte geschilderten Bedingungen sind Regeln.
Fakten
Fakten können folgendermassen definiert werden:
Code: Alles auswählen
city(krasnojarsk).
city(novosibirsk).
city(omsk).
city(tjumen).
population(768358).
population(1090811).
population(1172070).
population(1612833).
Regeln
Die Beziehung zwischen einer Stadt und einer Bevölkerungszahl kann als Regel codiert werden:
Code: Alles auswählen
has_population(City, Population) :-
city(City),
population(Population).
Die Regel besagt, dass einerseits City einen der Werte annehmen kann, der mit dem city-Prädikat definiert worden ist, und andererseits, dass analog dazu Population einen mit population definierten Wert annehmen muss. Bei Prolog gilt die sogenannte Closed World Assumption: Sachen, die nicht definiert worden sind, existieren nicht. So gibt es im Universum unseres Rätsels nur gerade vier Städte und vier Einwohnerzahlen.
Eine Regel das Rätsel zu lösen
Das eingangs gestellte Rätsel kann mit einer grossen, zusammengesetzten Regel gelöst werden. Zunächst muss man sich überlegen, welche Informationen man erhalten will: Es sind die Stationen; nennen wir sie A, B, C und D, sowie deren Bevölkerungszahlen PA, PB, PC und PD (P steht für Population). Die entsprechende Regel wird folgendermassen eingeleitet:
Code: Alles auswählen
stations(A, PA, B, PB, C, PC, D, PD) :-
Was ist über diese Variablen bekannt? Zunächst einmal bezeichnen A, B, C und D alles Städte; PA, PB, PC, PD bezeichnen deren Bevölkerungszahl:
Code: Alles auswählen
city(A),
city(B),
city(C),
city(D),
population(PA),
population(PB),
population(PC),
population(PD),
Weiter muss es sich um vier unterschiedliche Städte handeln. Hierzu wird eine neue Regel namens unique ausprogrammiert:
Code: Alles auswählen
unique([]).
unique([_]).
unique([H|T]) :-
\+ contains(T, H),
unique(T).
- Die Elemente einer leeren Liste [] sind einzigartig.
- Das Element einer Liste bestehend aus einem Element [_] ist einzigartig.
- Die Elemente einer Liste bestehend aus einem Kopf (head: h) und einem Rest (tail: t) sind einzigartig, wenn…
- der Rest den Kopf nicht enthält (\+ ist der Negationsoperator) und
- die Elemente vom Rest auch einzigartig sind.
Code: Alles auswählen
contains([], _) :- false.
contains([H|_], H).
contains([_|T], X) :- contains(T, X).
- Eine leere Liste enthält kein beliebiges Element.
- Eine Liste mit dem ersten Element H enthält dieses Element H.
- Eine Liste mit einem beliebigen ersten Element enthält das Element X, falls dieses im Rest enthalten ist.
Die beiden Regeln unique/1 und contains/2 sollen nun isoliert getestet werden (utils.pl):
Code: Alles auswählen
unique([]).
unique([_]).
unique([H|T]) :-
\+ contains(T, H),
unique(T).
contains([], _) :- false.
contains([H|_], H).
contains([_|T], X) :- contains(T, X).
Code: Alles auswählen
$ swipl utils.pl
?- contains([1,2,3,4,5],3).
true .
?- contains([1,2,3,4,5],6).
false.
?- unique([1,2,3,4,5]).
true .
?- unique([1,2,3,4,5,1]).
false.
Code: Alles auswählen
stations(A, PA, B, PB, C, PC, D, PD) :-
city(A),
city(B),
city(C),
city(D),
unique([A, B, C, D]),
population(PA),
population(PB),
population(PC),
population(PD),
unique([PA, PB, PC, PD]).
Nachdem die Grundstruktur des Problems beschrieben worden ist, sollen nun die eigentlichen Regeln des Rätsels verarbeitet werden.
Erste Regel
Die erste Regel besagt: Krasnojarsk ist nicht die Stadt mit der höchsten Einwohnerzahl. Diese Regel liesse sich allgemein ausdrücken, indem die höchste aller Einwohnerzahlen mit einem Prädikat wie max/1 ermittelt werden würde. Man kann es sich aber auch einfach machen, da die Bevölkerungszahlen schon im Voraus bekannt sind.
Der intuitive Ansatz scheitert jedoch:
Code: Alles auswählen
not(has_population(krasnojarsk, 1612833)),
Ein Prädikat not(predicate(X)) bedeutet: Existiert kein X, wofür predicate/1 gilt (universelle Quantifikation)? Und nicht: Existiert ein X, wofür predicate/1 nicht gilt (existenzielle Quantifikation)?
Die Regel lässt sich jedoch so formulieren, dass eine der anderen drei Städte diese höchste Bevölkerungszahl haben muss:
Code: Alles auswählen
(
has_population(novosibirsk, 1612833);
has_population(omsk, 1612833);
has_population(tjumen, 1612833)
),
Zweite, dritte und vierte Regel
Die zweite Regel besagt: Die vierte Station ist eine Stadt mit 1.090.811 Einwohnern.
Die vierte Stadt D muss die angegebene Einwohnerzahl haben:
Code: Alles auswählen
has_population(D, 1090811),
Code: Alles auswählen
B = omsk,
Da es nur eine Bevölkerungszahl unter der Million gibt, lässt sich auch diese Regel anhand einer vorgegebenen Zahl formulieren. Die Negation erfordert aber wieder eine mit dem Oder-Operator verkettete Unterregel, bei der die dritte Stadt C nicht als eine der Möglichkeiten auftaucht:
Code: Alles auswählen
(
has_population(A, 768358);
has_population(B, 768358);
has_population(D, 768358)
),
Die fünfte Regel besagt: Die Haltestelle unmittelbar nach Tjumen ist in der Stadt mit einer Einwohnerzahl von 1.172.070.
Jetzt wird es noch einmal kompliziert: Die Stadt Tjumen muss nach einer Stadt X kommen, welche eine Bevölkerungszahl von 1.172.070 hat. Hierzu ist ein neues Prädikat erforderlich: comes_right_after_in/3:
Code: Alles auswählen
comes_right_after_in(_, _, []) :- false.
comes_right_after_in(_, X, [X]) :- false.
comes_right_after_in(X, H, [H|[X|_]]).
comes_right_after_in(Y, X, [_|T]) :- comes_right_after_in(Y, X, T).
- In einer leeren Liste [] gibt es keine zwei beliebigen Elemente _, die aufeinanderfolgen.
- In einer Liste bestehend aus einem Element [X] gibt es nichts, was auf X folgt.
- In einer Liste bestehend aus einem Kopf H und einem Rest, der mit X anfängt, folgt X auf H.
- In einer Liste mit beliebigem Kopf folgt Y auf X, sofern Y X im Rest folgt.
Code: Alles auswählen
$ swipl utils.pl
?- comes_right_after_in(bob,alice,[]).
false.
?- comes_right_after_in(bob,alice,[bob]).
false.
?- comes_right_after_in(bob,alice,[bob,alice]).
false.
?- comes_right_after_in(bob,alice,[alice,bob]).
true .
?- comes_right_after_in(bob,alice,[alice,mallory,bob]).
false.
Code: Alles auswählen
comes_right_after_in(X, tjumen, [A, B, C, D]),
has_population(X, 1172070).
Des Rätsels Lösung?
So wird es Zeit, das Rätsel mithilfe von Prolog aufzulösen:
Code: Alles auswählen
$ swipl transsib.pl
?- stations(A, PA, B, PB, C, PC, D, PD).
A = krasnojarsk,
PA = 768358,
B = omsk,
PB = 1090811,
C = tjumen,
PC = 1172070,
D = novosibirsk,
PD = 1612833 .
Übungen
Wer Prolog einmal selber ausprobieren möchte, kann sich an folgenden Übungen versuchen:
- Installiere swi-prolog.
- Lade den Code von 42052 herunter und rufe das stations/8-Prädikat wie oben beschrieben auf.
- Die gefundene Lösung scheint nicht der Realität zu entsprechen. Ist die Implementierung oder das Rätsel falsch?
- Suche dir ein weiteres Logical und beschreibe es hier. Versuche es mit Prolog zu lösen und poste den Code hier ‒ egal ob es funktioniert oder nicht: Vielleicht kann ja jemand weiterhelfen.