ueb12_7 - oth

Werbung
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
......
......
......
NN
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
Herunterladen