Hochschule Regensburg Übung 12_7 Grenzen der Berechenbarkeit Spezielle Algorithmen (SAL) Name: ________________________ Lehrbeauftragter: Prof. Sauer Vorname:_______________________ Generell kann man für Algorithmen folgende Grenzfälle bzgl. der Rechenbarkeit beobachten: - kombinatorische Explosion Es gibt eine Reihe von klassischen Problemen, die immer wieder in der Mathematik oder der DVLiteratur auftauchen, weil sie knapp darzustellen und im Prinzip einfach zu verstehen sind. Manche von ihnen sind nur von theoretischem Interesse, wie etwa die Türme von Hanoi. Ein anderes klassisches Problem ist dagegen das Problem des Handlungsreisenden 1 (Travelling Salesman Problem, TSP). Es besteht darin, dass ein Handlungsreisender eine Rundreise zwischen einer Reihe von Städten machen soll, wobei er am Ende wieder am Abfahrtort ankommt. Dabei will er den Aufwand (gefahrene Kilometer, gesamte Reisezeit, Eisenbahn- oder Flugkosten, je nach dem jeweiligen Optimierungswunsch) minimieren. So zeigt bspw. die folgende Entfernungstabelle die zu besuchenden Städte und die Kilometer zwischen ihnen: München Frankfurt Heidelberg Karlsruhe Mannheim Frankfurt 395 - Heidelberg 333 95 - Karlsruhe 287 143 54 - Mannheim 347 88 21 68 - Wiesbaden 427 32 103 150 92 Grundsätzlich (und bei wenigen Städten, wie in diesem Bsp., auch tatsächlich) ist die exakte Lösung dieser Optimierungsaufgabe mit einem trivialen Suchalgorithmus zu erledigen. Man rechnet sich einfach die Route der Gesamtstrecke aus und wählt die kürzeste. Der benötigte Rechenaufwand steigt mit der Zahl N der zu besuchenden Städte sprunghaft an. Erhöht man bspw. N von 5 auf 10, so verlängert sich die Rechenzeit etwa auf das „dreißigtausendfache“. Dies nennt man kombinatorische Explosion, weil der Suchprozess jede mögliche Kombination der für das Problem relevanten Objekte einzeln durchprobieren muss. Der Aufwand steigt proportional zur Fakultät (N!). - exponentielle Explosion Wie kann man die vollständige Prüfung aller möglichen Kombinationen und damit die kombinatorische Explosion umgehen? Naheliegend für das TSP ist, nicht alle möglichen Routen zu berechnen und erst dann die optimale zu suchen, sondern sich immer die bis jetzt beste zu merken und das Ausprobieren einer neuen Wegkombination sofort abzubrechen, wenn bereits eine Teilstrecke zu größeren Kilometerzahlen führt als das bisherige Optimum, z.B.: Route 1 München Karlsruhe Heidelberg Mannheim Wiesbaden Frankfurt München Streckensumme 0 287 341 362 454 486 881 Route 2 München Streckensumme 0 1 Vorbild für viele Optimierungsaufgaben, wie sie vor allem im Operations Research immer wieder vorkommen. 1 Wiesbaden Karlsruhe Frankfurt Heidelberg 429 722 865 960 Route 2 kann abgebrochen werden, weil die Teilstrecke der Route 2 (960) bereits länger ist als die Gesamtstrecke der Route 1. Diese Verbesserung vermeidet die kombinatorische Explosion, ersetzt sie aber leider nur durch die etwas schwächere exponentielle Explosion. Die Rechenzeit nimmt exponentiell, d.h. mit aN für irgendeinen problemspezifischen Wert zu. Im vorliegenden Fall ist a etwa 1.26. - polynomiales Zeitverhalten In der Regel ist polynomiales Zeitverhalten das beste, auf das man hoffen kann. Hiervon redet man, wenn man die benötigte Rechenzeit durch ein Polynom T a n N n ... a 2 N 2 a1 N a 0 ausgedrückt werden. „N“ ist bestimmt durch die zu suchenden problemspezifischen Werte, n beschreibt den Exponenten. Da gegen das erste Glied mit der höchsten Potenz bei größeren Objektzahlen alle anderen Terme des Ausdrucks vernachlässigt werden können, klassifiziert man das polynomiale Zeitverhalten nach dieser höchsten Potenz. Man sagt, ein Verfahren zeigt polynomiales Zeitverhalten O(Nn), falls die benötigte Rechenzeit mit der nten Potenz der Zahl der zu bearbeitenden Objekte anwächst. Die einzigen bekannten Lösungen des TSP, die in polynomialer Zeit ablaufen, verzichten darauf, unter allen Umständen die beste Lösung zu finden, sondern geben sich mit einer recht guten Lösung zufrieden. In der Fachsprache wird das so ausgedrückt, daß das TSP NP-vollständig sei. Das bedeutet: In polynomialer Zeit kann nur eine nichtdeterministische Lösung berechnet werden, also eine, die nicht immer deterministisch ein und dasselbe (optimale) Ergebnis findet. Ein Verfahren, für das nicht garantiert werden kann, daß es in allen Fällen ein exaktes Resultat liefert, wird heuristisch genannt. Eine naheliegende heuristische Lösung für das TSP ist der „Nächste Nachbarn-Algorithmus“. Er beginnt die Route mit der Stadt, die am nächsten zum Ausgangsort liegt und setzt sie immer mit derjenigen noch nicht besuchten Stadt fort, die wiederum die nächste zum jeweiligen Aufenthaltsort ist. Da in jeder der N Städte alle (d.h. im Durchschnitt (N-1)/2) noch nicht besuchte Orte nach dem nächsten benachbarten durchsucht werden müssen, ist der Teitaufwand für das Durchsuchen proportional N ( N 1) / 2 , d.h. O(N2), und damit polynomial „in quadratischer Zeit“ Die meisten Algorithmen für Datenstrukturen bewegen sich in einem schmalen Band rechnerischer Komplexität. So ist ein Algorithmus von der Ordnung O(1) unabhängig von der Anzahl der Datenelemente in der Datensammlung. Der Algorithmus läuft unter konstanter Zeit ab, z.B.: Das Suchen des zuletzt in eine Schlange eingefügten Elements bzw. die Suche des Topelements in einem Stapel. Ein Algorithmus mit dem Verhalten O(N) ist linear. Zeitlich verhält er sich proportional zur Größe der Liste. Bsp.: Das Bestimmen des größten Elements in einer Liste. Es sind N Elemente zu überprüfen, bevor das Ende der Liste erkannt wird. Andere Algorithmen zeigen „logarithmisches Verhalten“. Ein solches Verhalten läßt sich beobachten, falls Teildaten die Größe einer Liste auf die Hälfte, ein Viertel, ein Achtel... reduzieren. Die Lösungssuche ist dann auf die Teilliste beschränkt. Derartiges Verhalten findet sich bei der Behandlung binärer Bäume bzw. tritt auf beim „binären Suchen“. Der Algorithmus für binäre Suche zeigt das Verhalten O(log 2 N ) , Sortieralgorithmen wie der Quicksort und Heapsort besitzen eine rechnerische Komplexität von O( N log 2 N ) . Einfache Sortierverfahren (z.B. „BubbleSort“) bestehen aus Algorithmen mit einer Komplexität von O( N 2 ) . Sie sind deshalb auch nur für kleine Datenmengen brauchbar. Algorithmen mit kubischem Verhalten O( N 3 ) sind bereits äußerst langsam (z.B. der Algorithmus von Warshall zur Bearbeitung von Graphen). Ein Algorithmus mit einer Komplexität von O( 2 N ) zeigt exponentielle Komplexität. Ein derartiger Algorithmus ist nur für kleine N auf einem Rechner lauffähig. 2 Auch praktische Probleme passen in das angegebene Lösungsschema, z.B. "das Optimierungsproblem des Handlungsreisenden (Traveling Salesman Problem)": Ein Handlungsreisender hat eine Liste von Städten. Er muß jede Stadt genau einmal besuchen. Die Städte haben paarweise eine direkte Verbindung. Der Handlungsreisende soll eine Rundreise zwischen den Städten machen, wobei er am Ende wieder am Abfahrtsort ankommt. Dabei soll er den Aufwand (gefahrene Kilometer) minimieren. Folgende Entfernungstabelle zwischen Städten ist bspw. gegeben: M F HD KA MA WI München (M) - 395 333 287 347 429 Frankfürt(F) 395 - 95 143 88 32 Heidelberg(HD) 333 95 - 54 21 103 Karlsruhe(KA) 287 143 54 - 68 150 Mannheim (MA) 347 88 21 68 - 92 Wiesbaden (WI) 429 32 103 150 92 - Das Problem kann mit Hilfe einer einfachen, bewegungserzeugenden und systematischen Kontrollstruktur bearbeitet werden. Man rechnet sich einfach für jede mögliche Route die Gesamtstrecke aus und wählt dann die kürzeste. Leider zeigt sich, daß der notwendige Rechenaufwand mit der Zahl N der zu besuchenden Städte sprunghaft ansteigt. Wenn es N Städte gibt, dann ist der Anzahl der verschiedenen Wege zwischen den Städten: N ( N 1)! . Der für die Suche erforderliche Aufwand ist O(N !) . Erhöht man N bspw. von 5 auf 10, so verlängert sich die Rechenzeit auf das "dreißigtausendfache". Diese Erscheinung heißt kombinatorische Explosion. Sie tritt auf, wenn der Suchprozeß jede mögliche Kombination der für das Problem relevanten Objekte einzeln durchprobieren muß. Für diese Vorgehensweise hat sich der Ausdruck „British Museum"-Verfahren“ eingebürgert. Man findet alles, wenn man nur lange genug herumsucht. Diese Vorgehensweise beschreibt für das Travelling-Salesman-Problem das folgende Programm (pr21104.pro)2: domains symbolliste = symbol* database anzahl(integer) kante(symbol,symbol,integer) rundreise(symbolliste,integer) predicates start(symbol) reiseroute(symbol) weg_1(symbol,symbol,symbolliste,symbolliste,integer,symbol) eintragen(symbolliste,integer) kanten_best(symbol,symbol,integer) laenge(symbolliste,integer) enthalten(symbol,symbolliste) verketten(symbolliste,symbolliste,symbolliste) clauses start(Anfang) :/* Initialisieren */ asserta(anzahl(6)), assertz(kante(muenchen,frankfürt,395)), 2 Vgl. ueb12_7 3 assertz(kante(muenchen,heidelberg,333)), assertz(kante(muenchen,karlsruhe,287)), assertz(kante(muenchen,mannheim,347)), assertz(kante(muenchen,wiesbaden,429)), assertz(kante(frankfürt,heidelberg,95)), assertz(kante(frankfürt,karlsruhe,143)), assertz(kante(frankfürt,mannheim,88)), assertz(kante(frankfürt,wiesbaden,32)), assertz(kante(heidelberg,karlsruhe,54)), assertz(kante(heidelberg,mannheim,21)), assertz(kante(heidelberg,wiesbaden,103)), assertz(kante(karlsruhe,mannheim,68)), assertz(kante(karlruhe,wiesbaden,150)), assertz(kante(mannheim,wiesbaden,92)), assertz(kante(wiesbaden,muenchen,429)), /* Berechnung der optimalen Rundreise */ reiseroute(Anfang), /* Ausgabe */ retract(rundreise(Weg,Laenge)), verketten(Weg,[Anfang],Rundreiseweg), write(Rundreiseweg), nl, write(Laenge), nl, /* Loeschen der Daten in der dynm. Datenbank */ retractall(kante(_,_,_)), retractall(anzahl(_)). reiseroute(Start) :weg_1(Start,Kno2,[Start],Weg,Laenge,Start), eintragen(Weg,Laenge), fail. reiseroute(Start) :- !. weg_1(Kn,Kn,Weg,[Kn],Entf,Start) :anzahl(N), laenge(Weg,N), kanten_best(Kn,Start,Entf). weg_1(Kn1,Kn2,L,[Kn1|Weg],Summe,Start) :kanten_best(Kn1,Z,Wert), not (enthalten(Z,L)), weg_1(Z,Kn2,[Z|L],Weg,S,Start), Summe = S + Wert. eintragen(W1,L1) :rundreise(W2,L2), !, L1 < L2, retract(rundreise(W2,L2)), asserta(rundreise(W1,L1)). eintragen(W1,L1) :asserta(rundreise(W1,L1)). kanten_best(X,Y,E) :kante(X,Y,E) ; kante(Y,X,E). /* Hilfsroutinen */ laenge([],0). laenge([Kn|R],N) :laenge(R,M), N = M + 1. enthalten(K,[K|_]). enthalten(K,[_|R]) :- enthalten(K,R). verketten([],L2,L2). verketten([E1|R1],L2,[E1|R]) :verketten(R1,L2,R). Zur Bekämpfung der kombinatorischen Explosion ist eine neue Kontrollstrategie erforderlich. Ein naheliegender Versuch ist, nicht alle möglichen Routen zu berechnen und erst dann die optimale zu suchen, sondern sich immer die bisher beste zu merken und das Ausprobieren einer neuen Kombination sofort abzubrechen, wenn bereits eine Teilstrecke zu größeren Kilometerzahlen führt als das bisherige Optimum. 4 Diese Verbesserung vermeidet die kombinatorische Explosion, ersetzt sie aber leider nur durch die etwas schwächere "exponentielle Explosion". Die folgende Tabelle stellt einige Werte für verschiedene Wachstumsgesetze zusammen: N 1 2 3 5 7 10 20 50 100 N! 1 2 6 120 5040 3628800 ....... ....... ....... exp( N ) 3 7 20 148 1097 22026 ...... ...... ...... NN 1 4 7 25 49 100 400 2500 10000 log( N ) 0 0.693 1.099 1.609 1.946 2.303 2.996 3.912 4.604 Neben der kombinatorischen und exponentiellen Explosion zeigt die Abbildung noch die quadratische und die logarithmische. Die Punkte zeigen Werte an, bei denen die numerischen Kapazitätsgrenzen überschritten werden. Ideal wäre natürlich ein Ansteigen des Aufwands mit dem Logarithmus der Objektzahl. Leider gibt es nur wenige Probleme, die so auf die Zunahme des Problemumfangs reagieren (z.B. die binäre Suche). Expertenwissen kann nur selten so geordnet werden, daß man zur Bearbeitung einer Anfrage eine sehr effiziente Suchmethode einsetzen kann. In der Regel ist ein polynomiales Zeitverhalten das beste, was man erhoffen kann. Die benötigte Rechenzeit wird durch ein Polynom Z a n N n ... a 2 N 2 a1 N a0 beschrieben. Da gegen das 1. Glied mit der höchsten Potenz bei größeren Objektzahlen alle anderen Terme des Ausdrucks vernachlässigt werden können, klassifiziert man polynomiales Zeitverhalten nach der höchsten Potenz. Man sagt das Verfahren zeigt das polynomiale Zeitverhalten O( N n ) , wenn die benötigte Rechenzeit mit der n-ten Potenz der Zahl der zu bearbeitenden Objekte wächst. Zur Lösung vieler Probleme ist es deshalb erforderlich, eine Kontrollstrategie zu erzeugen, die nicht mehr gewährleistet, daß die beste Antwort gefunden wird, sondern lediglich, daß eine ziemlich gute Lösung ermittelt wird. In polynomialer Zeit kann bspw. für das Handlungsreisenden-Problem nur eine nichtdeterministische Lösung berechnet werden, d.h: Die Lösung führt nicht immer "deterministisch" auf ein und dasselbe (optimale) Ergebnis. Das TSP-Problem ist NP vollständig. Das bedeutet: Nach den derzeitigen Stand des Wissens kann in polynomialer Zeit nur eine nichtdeterministische Lösung berechnet werden, also eine, die nicht immer „deterministisch“ ein und dasselbe (optimale) Ergebnis findet. Heuristische Suche Ein Verfahren, für das nicht garantiert werden kann, dass es in allen Fällen das beste Resultat findet, wird heuristisch genannt. Heuristisch ist abgeleitet vom griechischen Ausdruck "heuristiken"3 (für "entdecken"). Das Entdecken bezieht sich hier auf eine Lösungsorder, die aus den Gegebenheiten der Problemstellung, aus Vorwissen oder "aus gutem Menschenverstand" heraus einen einfachen Weg als die erschöpfende oder die abgekürzte Suche ermöglicht. 3 Vgl. Skriptum: 3.3 5 Bsp.: Heuristische Suche für das Handlungsreisenden-Problem (Nächster NachbarAlgorithmus) Naheliegend ist die Route mit einer Stadt zu beginnen, die am nächsten zum Ausgangsort liegt. Man setzt sie dann immer mit derjenigen noch nicht besuchten Stadt fort, die wiederum am nächsten zum Ausgangsort liegt (Algorithmus des nächsten Nachbarn). Da in jeder der N Städte alle (d.h. im Durchschnitt N 1 noch nicht besuchte 2 Orte nach dem nächsten Ort durchsucht werden müssen, ist der Zielaufwand für das Verfahren proportional zu N 1 N , d.h.: O ( N N ) (polynomial). 2 Anschaulich ist klar, dass dieses Verfahren keine unbedingt schlechte Reisestrecke liefert. Es wurde nachgewiesen, dass die so erhaltenen Lösungen im Durchschnitt um 20% schlechter und im schlimmsten Fall um den Faktor (log N 1) 2 schlechter sind als die optimale Lösung. Das folgende Programm (pr21105.pro) zeigt eine Implementierung4: domains symbolliste = symbol* database kante(symbol,symbol,integer) rundreise(symbolliste,integer) predicates start(symbol) reiseroute(symbol) weg(symbol,symbolliste,symbolliste,symbol,integer,integer) vorgabe(symbol,symbolliste) initialisiere(symbolliste) kanten_best(symbol,symbol,integer) kopiereliste(symbol,symbolliste,symbolliste) verketten(symbolliste,symbolliste,symbolliste) minimum(symbol,symbolliste,symbol,integer) min_dist(symbol,symbolliste,symbol,integer,symbol,integer) clauses start(Anfang) :/* Initialisieren */ assertz(kante(muenchen,frankfürt,395)), assertz(kante(muenchen,heidelberg,333)), assertz(kante(muenchen,karlsruhe,287)), assertz(kante(muenchen,mannheim,347)), assertz(kante(muenchen,wiesbaden,429)), assertz(kante(frankfürt,heidelberg,95)), assertz(kante(frankfürt,karlsruhe,143)), assertz(kante(frankfürt,mannheim,88)), assertz(kante(frankfürt,wiesbaden,32)), assertz(kante(heidelberg,karlsruhe,54)), assertz(kante(heidelberg,mannheim,21)), assertz(kante(heidelberg,wiesbaden,103)), assertz(kante(karlsruhe,mannheim,68)), assertz(kante(karlruhe,wiesbaden,150)), assertz(kante(mannheim,wiesbaden,92)), /* Berechnung einer nahezu optimalen Rundreise */ reiseroute(Anfang), /* Ausgabe */ retract(rundreise(Weg,Laenge)),verketten([Anfang],Weg,Reiseweg), write(Reiseweg), nl, write(Laenge), nl, /* Loeschen der Daten in der dynm. Datenbank */ retractall(kante(_,_,_)). reiseroute(Start) :4 vgl. ueb12_7 6 vorgabe(Start,StListe), weg(Start,StListe,Weg,Start,0,Distanz), asserta(rundreise(Weg,Distanz)). vorgabe(Start,StListe) :initialisiere(StListe1), kopiereliste(Start,StListe1,StListe). weg(Kn,[],[Kn],Kn1,Entf,Dist) :kanten_best(Kn,Kn1,Entf2), Dist = Entf + Entf2, !. weg(Kn,StListe,[X|Weg],Kn1,Entf,Distanz) :minimum(Kn1,StListe,X,Entf2), Ergebnis = Entf + Entf2, kopiereliste(X,StListe,StListe1), weg(Kn,StListe1,Weg,X,Ergebnis,Distanz). initialisiere([X|Liste]) :kanten_best(X,Y,_), findall(B,kanten_best(X,B,_),Liste), !. kanten_best(X,Y,E) :kante(X,Y,E) ; kante(Y,X,E). /* Hilfsroutinen */ verketten([],L2,L2). verketten([E1|R1],L2,[E1|R]) :verketten(R1,L2,R). kopiereliste(_,[],[]). kopiereliste(Knoten,[Kopf|Rest],[Kopf|Restliste]) :Kopf <> Knoten, kopiereliste(Knoten,Rest,Restliste), !. kopiereliste(Knoten,[Kopf|Rest],Restliste) :kopiereliste(Knoten,Rest,Restliste), !. /* Bestimmt den naechsten Knoten mit der kuerzesten Entfernung. */ min_dist(_,[],Start,Entfernung,Start,Entfernung). min_dist(Kn,[Ziel|Rest],Kn1,Entfernung,Knoten,Dist) :kanten_best(Kn,Ziel,Entf), Entfernung > Entf, min_dist(Kn,Rest,Ziel,Entf,Knoten,Dist). min_dist(Kn,[_|Rest],Kn1,Entfernung,Knoten,Dist) :min_dist(Kn,Rest,Kn1,Entfernung,Knoten,Dist). minimum(Kn,[Ziel|Rest],Kn1,Entf1) :kanten_best(Kn,Ziel,Entf), min_dist (Kn,Rest,Ziel,Entf,Kn1,Entf1). 7