Suchstrategien


... [ Seminar "Programmierkonzepte und Programmiersprachen" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ...

Übersicht: Titel


Klassifizierung

Zur Ermittelung einer oder mehrerer Lösungen wenden Constraintsolver, wie sie beispielsweise im ECLiPSe-System zum Einsatz kommen, verschiedene Suchstrategien im Ergebnisraum an. Diese können zuerst in constructive und moved-based Algorithmen unterschieden werden.

Bei constructive Algorithmen erfolgt eine inkrementelle Wertezuweisung an die Variablen. Dies impliziert, dass der Suchraum anhand einer Baumstruktur organisiert wird. Moved-based Algorithmen arbeiten hingehen mit total assignments, also mit zeitgleichen Zuweisungen von Werten an alle Variablen. Der Suchraum wird hierbei durch methodische Modifikation einzelner Problemvariablen ausgehend von vorherigen Zuweisungen untersucht, soll heißen, dass daraus neue total assignments abgeleitet und getestet werden, bis eine oder alle Lösungen gefunden worden sind. Diese Art der Suche erweist sich jedoch als sehr speicherintensiv, da alle überprüften möglichen Lösungen gemerkt werden müssen. Gerade bei sehr komplexen Problemen kann dies zu Schwierigkeiten führen. Im weiteren Verlauf werden nur noch constructive Algorithmen betrachtet.

Die Suchstrategien können zudem noch nach vollständiger und unvollständiger Suche klassifiziert werden. Bei der vollständigen Suche wird der gesamte Suchbaum traversiert. Dies ist beispielsweise bei der Suche nach der optimalen Lösung notwendig. Hingegen wird bei der unvollständigen Suche nur ein Teil des Suchbaums betrachtet. So kann bei sehr komplexen Aufgabenstellungen die zulässige Anzahl an Rücksprüngen (auf Rücksprünge wird im Folgenden noch näher eingegangen) angegeben werden. Sollte bis dahin noch keine Lösung gefunden worden sein, wird die Suche abgebrochen.


Generate and Test

"Generate and Test" stellt die einfachste Suchstrategie dar. Hierbei handelt es sich um die einfache Tiefensuche von links nach rechts im Suchbaum. Alle Variablen werden mit Werten gemäß ihrer Domain belegt. Danach erfolgt die Überprüfung, ob alle aufgestellten Bedingungen eingehalten werden. Ist dies nicht der Fall, wird die nächst mögliche Wertekombination im Baum gewählt. Da alle möglichen Wertkombinationen im Baum von links nach rechts abgearbeitet werden müssen, ist dies sehr laufzeitintensiv und der Aufwand steigt mit jeder Variablen exponentiell an.

Schematische Darstellung von "Generate and Test"
Schematische Darstellung von Generate and Test

In obiger Grafik repräsentiert jeder Knoten für eine Problemvariable, jede Kante für einen Wert, den die Problemvariable annehmen kann und jeder Punkt für eine mögliche Wertkombination. Das Prinzip der Tiefensuche wird auch in den anderen vorgestellten Suchstrategien verwendet, es erfolgt allerdings jeweils eine Weiterentwicklung, um effizienter zu einer möglichen Lösung zu gelangen.


Backtracking

Beim Backtracking erfolgt eine schrittweise Belegung der Variablen mit Werten aus ihren Wertebereichen, dass heißt, dass der ersten Variablen ein Wert aus ihrer Domain zugewiesen wird und danach sofort überprüft wird, ob die Teillösung gegen ein primitives Constraint verstößt. Falls kein Verstoß vorliegt, wird der nächsten Variablen ein Wert zugewiesen und es erfolgt wieder eine Überprüfung auf Verstoß gegen ein Constraintatom. Sollte ein Verstoß vorliegen, wird die Variable mit dem nächsten Wert aus der Domain belegt. Für den Fall, dass der Wertebereich der Variablen erschöpft ist, erfolgt ein Rückschritt zur nächst höheren Variablen im Suchbaum, welche dann mit einem neuen Wert belegt wird. Hierbei wird der Suchbaum beschnitten, da Variablen unter der aktuellen Variablen mit keinen Werten belegt werden. Man spricht in diesem Fall auch von Pruning (engl.: to prune: beschneiden). Sollte der Wertebereich der Variablen erschöpft und auch kein Rückschritt mehr möglich sein, da man sich am obersten Baumknoten befindet, gibt es keine Lösung für das betrachtete Problem. Die im vorherigen Kapitel vorgestellte labeling-Regel der fd-Bibliothek arbeitet mit diesem Suchalgorithmus.

Das Backtracking soll an folgendem einfachen Beispiel verdeutlicht werden.
:- lib(fd).

solve(Triple) :-

    Triple = [X, Y ,Z],
    [X, Z] :: 1..2,
    Y :: 1..3,

    X #> 1,
    Y #>= X,
    Z #< Y,

    labeling(Triple).

Der Suchbaum sieht in diesem Fall folgendermaßen aus:

Suchbaum Backtracking
Suchbaum Backtracking

Zur Erklärung: Die erste Variable, X, wird mit dem ersten Wert aus ihrer Domain belegt (1). Da dieser Wert jedoch auch gegen das erste Constraintatom verstößt, wird der nächste Wert aus der Domain gewählt. Somit wird zugleich der linke Teilbaum abgeschnitten, sodass bereits nach der ersten Variablenbelegung die Hälfte aller möglichen Lösungen ausgeschlossen werden kann. Die oben beschriebene Systematik wird nun weiter verfolgt, bis man zur ersten Lösungsmenge (2,2,1) gelangt. Um hiernach eine weitere Lösung zu erhalten, muss darauf ein Rückschritt von der bereits belegten Variable Z zu der Variablen Y erfolgen, welche nun mit 3 belegt wird. Aus dieser Position werden dann durch Belegen der Variablen Z die anderen beiden Lösungen gefunden.


Forward Checking

Das Forward Checking ist eine Erweiterung des normalen Backtrackings, die zu einer enormen Effizienzsteigerung in der logischen Programmierung mit Einschränkungen führen kann. Hierbei wird zuerst einer Variablen abermals ein Wert zugewiesen und diese Zuweisung anhand des Constraint überprüft. Darauf erfolgt die dynamische Anpassung der Domains der zukünftigen Variablen. Dass heißt, dass alle Werte der Domains, die mit dieser Zuweisung in Konflikt stehen, temporär aus dem Wertebereich gelöscht werden. Somit wird sichergestellt, dass nur noch konsistente Werte in den Domains der noch zu behandelnen Variablen sind. Sollte bei diesem Verfahren ein leerer Wertebereich einer zukünftigen Variablen auftreten, steht bereits zu einem gegebenenfalls sehr frühen Zeitpunkt fest, dass dieser Ansatz zu keiner Lösung führen kann. Das Forward Checking nutzt daher das Constraint, um ein effizienteres Pruning durchführen zu können als es beim reinen Backtracking der Fall ist.

Das Forward Checking soll an dieser Stelle an obigem Beispiel nochmals kurz verdeutlicht werden. Unter der Annahme, dass X mit 2 belegt ist, wird der Wert 1 aus der Domain der Variablen Y belegt. Hierdurch wird der Baum schon ohne die Belegung von Y weiter beschnitten.


Heuristik

Bis jetzt wurde der Aufbau des Suchbaums noch nicht näher betrachtet. Es wird davon ausgegangen, dass die Variablen in der Reihenfolge ihrer Definition im Programm verwendet werden. Auch wird davon ausgegangen, dass die möglichen Werte der Variablen in der Reihenfolge ihres Auftretens in der Domain verwendet werden. Beides muss nicht zwangsläufig der Fall sein. In den folgenden beiden Punkten wird gezeigt, wie eine dynamische Variablen- und Werteauswahl zur Effizienzsteigerung beitragen kann, aber auch, wie bereits zur Entwurfszeit die spätere Laufzeit minimiert werden kann.

Variablenordnung

Die Variablenordnung beschreibt, in welcher Reihenfolge die Variablen im Suchbaum auftreten. Die Änderung der Reihenfolge bewirkt unter Umständen auch die Änderung der Anzahl der Knoten im Suchbaum. Die Variablenordnung kann statisch zur Entwurfszeit festgelegt oder aber dynamisch zur Laufzeit berechnet werden. Während die statische Festlegung beim reinen Backtracking Anwendung findet, kann die dynamische Auswahl von Variablen erst sinnvoll beim Forward Checking erfolgen. Dies beruht darauf, dass nur in diesem Fall neue Informationen über die jeweiligen Domains zur Verfügung stehen.

Generell soll die Variablenordnung nach dem Fail-First-Prinzip festgelegt werden. Dieses besagt: "Um erfolgreich zu sein, probiere das zuerst, was am wahrscheinlichsten zum Fehler führt!". Hieraus leitet sich ab, dass immer die Variable gewählt werden soll, welche den kleinsten (im dynamischen Fall: verbleibenden) Wertebereich hat. Für den Fall, dass mehrere Variablen gleich große Domains haben, so soll die Variable gewählt werden, welche am häufigsten in den primitiven Constraints vorkommt.

In folgenden Beispiel soll verdeutlicht werden, wie die Variablenordnung zur Entwurfszeit das simple Backtracking beeinflussen kann.

Suchräume bei unterschiedlicher Variablenordnung
Suchräume bei unterschiedlicher Variablenordnung

Würde man annehmen, dass in diesem Beispiel die Constraintatome
X1 #> 0 ,
X2 #> X1
gelten, so wird ersichtlich, dass sich aufgrund einer überlegten Variablenordnung, welche sich an die oben aufgestellte Regel hält, eine Reduzierung der notwendigen Backtrackingschritte bis zur erfolgreichen Lösung erreichen lässt.

Die dynamische Variablenordnung soll an dieser Stelle am Beispiel einer eigenen labelling-Regel "labelingff(Liste)" dargestellt werden, die die Variablenauswahl berücksichtigt. Dies geschieht durch die Regel "deleteff(Vs, V, Rest)", welche über den Parameter V die Variable mit der kleinsten Domain zurückgibt.
labelingff([]).
labelingff(Vs) :-
    deleteff(Vs, V, Rest),
    indomain(V),
    labelingff(Rest).

deleteff([V0|Vs], V, Rest) :-
    get_domain_size(V0, S0),
    min_size(Vs, S0, V0, V, Rest).

min_size([], _, V, V, []).
min_size([V1|Vs], S0, V0, V, [V0|Rest]) :-
    get_domain_size(V1, S1),
    S1 #< S0
    min_size(Vs, S1, V1, V, Rest).
min_size([V1|Vs], S0, V0, V, [V1|Rest]) :-
    get_domain_size(V1, S1),
    S1 #>= S0
    min_size(Vs, S0, V0, V, Rest).


Werteordnung

Nach der Auswahl der Variablen erfolgt nun noch die Bestimmung, welcher Wert der Domain der Variablen als nächstes zugeordnet werden soll. Die Wertauswahl erweist sich aber nur als sinnvoll anwendbar, wenn nur eine oder wenige Lösungen gesucht werden, da ansonsten so oder so alle Werte betrachtet werden müssen. Abermals ist diese Systematik nur beim Forward Checking sinnvoll anwendbar, da man nur hierbei neue Informationen über die Werte(bereiche) der Variablen erhält.

Die Auswahl kann nach verschiedensten Algorithmen erfolgen, welche sehr problemspezifisch gewählt werden sollten. So kann es in einem Fall von Vorteil sein, die Domain aus der Mitte heraus nach außen hin abzuarbeiten. In anderen Fällen erscheint es aber auch vernünftig, die Kosten der Wertwahl durch den Prozentsatz der vergefallenen Werte in den zukünftigen Domains zu berechnen und den Wert mit den geringsten Kosten zu nehmen. Daher kann an dieser Stelle keine allgemein gültige Regel dem interessierten Leser vorgestellt werden.


... [ Seminar "Programmierkonzepte und Programmiersprachen" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ...