Ausarbeitung - Sebastian Schöps

Werbung
Optimale Wege
Projektseminar zur Wirtschaftsmathematik
Apl. Prof. Ernst-Peter Beisel
Stefan Lackner 1
Matrikel-Nr. 128446
Sebastian Schöps 2
Matrikel-Nr. 131450
15. Juli 2004
1 lucky
[email protected]
2 [email protected]
It is practically impossible to teach good programming to
students that have had a prior exposure to BASIC: as potential programmers they are mentally mutilated beyond
hope of regeneration.
- Prof. Dr. Edsger W.Dijkstra, 1975
Inhaltsverzeichnis
1 Einleitung
4
2 Optimale Wege
2.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Mathematische Grundlagen . . . . . . . . . . . . . . . . . . . . .
5
5
5
3 Datenstrukturen
3.1 Array . . . . . . . . . .
3.2 Verkettete Listen . . . .
3.3 Collections . . . . . . . .
3.4 Strukturen für Graphen
3.5 Vergleichende Analyse .
3.5.1 Queue . . . . . .
3.5.2 Stack . . . . . . .
3.5.3 Dequeue . . . . .
3.5.4 List . . . . . . .
3.5.5 Heap . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4 Verfahren
4.1 Label-Correcting . . . . . . .
4.1.1 Fifo . . . . . . . . . .
4.1.2 Dequeue . . . . . . . .
4.1.3 2-Queue . . . . . . . .
4.1.4 PSP-Verfahren . . . .
4.1.5 PSP-GS Algorithmus .
4.1.6 Schwellenalgorithmus .
4.2 Label-Setting . . . . . . . . .
4.2.1 Diijkstra . . . . . . . .
4.2.2 Dial’s Implementation
4.3 Pseudocode . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
8
9
9
10
10
12
12
13
15
15
17
.
.
.
.
.
.
.
.
.
.
.
19
19
19
20
20
21
21
22
23
23
23
24
INHALTSVERZEICHNIS
5 Beispielerzeugung
5.1 Sprand . . . . .
5.2 Spacyc . . . . .
5.3 Spgrid . . . . .
5.4 Spadja . . . . .
5.5 Spsym . . . . .
.
.
.
.
.
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6 Vergleichende Analyse
6.1 Implementationen des Fifo
6.2 Zyklische Graphen . . . .
6.3 Azyklische Graphen . . . .
6.4 Gittergraphen . . . . . . .
6.5 Steigende Kosten . . . . .
6.6 Unlösbare Graphen . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
26
27
27
28
29
29
.
.
.
.
.
.
30
31
32
35
37
41
42
7 Zusammenfassung
43
A Tabellen: Datenstrukturen
45
B Tabellen: Vergleichende Analyse
48
Literaturverzeichnis
57
Kapitel 1
Einleitung
Diese Ausarbeitung ist im Rahmen des Projektseminars zur Wirtschaftsmathematik im Sommersemester 2004 entstanden. Ihr liegt die Vorlesung von Herrn
Prof. Dr. Ernst-Peter Beisel aus dem Wintersemester 2003/04 zu Grunde [Bei03].
Die wesentliche Aufgabe war, Algorithmen zur Bestimmung optimaler Wege als
Planungswerkzeug in Visual Basic for Applications (VBA) zur Verfügung zu stellen. Dabei sollten die jeweiligen Verfahren auch vergleichend analysiert werden.
Hauptbestandteil dieser Ausarbeitung ist demnach die Präsentation und Bewertung der Analyseergebnisse in Kapitel 6. Zuvor werden die Grundlagen des ,,Optimale Wege“-Problems in Kapitel 2 erörtert. Ebenso wird der mathematische Hintergrund dargelegt, soweit er für das Verständnis der Verfahren nötig ist (Kapitel
2.2). In Kapiteln 3 und 4 werden die verwandten Datenstrukturen und Verfahren
beschrieben, in Kapitel 5 die Beispielerzeuger. Es werden jeweils auch Hinweise
zur konkreten Implementierung in VBA gegeben.
Das Programmpaket ,,or2“ inklusive Dokumentation und dieser Ausarbeitung ist
unter http://www.schoeps.org/or2.zip zum Download erhältlich. Das Programm dient Lehrzwecken und kann in diesem Sinne beliebig vervielfältigt und
weitergegeben werden. Eine kommerzielle Nutzung als Ganzes oder in Teilen ist
ausdrücklich untersagt.
4
Kapitel 2
Optimale Wege
2.1
Einführung
Die Frage nach optimale Wegen ist ein sehr alltägliches Problem, will man etwa
eine Wegstrecke mit möglichst geringem Aufwand oder in kürzester Zeit zurück
legen, wird man heutzutage einen Routenplaner zu Rate ziehen. Dieser berechnet
dann mit Hilfe des ihm zu Verfügung stehenden Kartenmaterials einen optimalen
Weg. Meistens derart, dass er die kürzeste Verbindung vom Anfangs- zum Endpunkt berechnet. Daher spricht man auch geometrisch von ,,kürzesten Wegen“
(engl. ,,Shortest Path“).
Die Rolle des Kartenmaterials übernimmt bei der theoretischen Formulierung
des Problems ein Graph, in dem Orte durch Knoten und Straßen durch Pfeile dargestellt werden. Dieses allgemeinere Konstrukt lässt sich jetzt auch auf
viele andere Anwendungsbereiche übertragen. Pfeile können nicht nur Längen
verkörpern, sondern auch Kosten oder Zeitdauern sein. Es ist sogar so flexibel,
dass die Lösung vieler komplexerer Probleme in Graphen die Lösung des kürzesten
Wege Problems voraussetzen.
2.2
Mathematische Grundlagen
Wir wollen noch einmamal kurz die wichtigsten Begriffe aus der Theorie der Graphen und Netzwerke erklären, die in diesem Bericht verwendet werden.
Wir gehen dabei von einem gerichteten Graphen oder kurz Digraphen (engl. ,,directed graph“) G = hN, Ai aus. N = {1, ..., n} sei dabei die nichtleere Menge von
Knoten (engl. ,,Nodes“) und |A| = m sei die Menge der Pfeile (engl. ,,Arcs“).
Jedem Element a ∈ A ist dabei ein geordnetes Paar von Knoten i, j zugeordnet.
Wir wollen a = hi, ji Pfeil nennen mit dem Anfangsknoten i und dem Endknoten
j. Haben Pfeile den gleichen Anfangs- und Endknoten, so sprechen wir von parallelen Pfeilen. Diese sind in unserem Programm nicht explizit ausgeschlossen, da
5
KAPITEL 2. OPTIMALE WEGE
6
die Algorithmen und Datenstrukturen auch parallele Pfeile verarbeiten können.
Dennoch verwenden wir aus Bequemlichkeitsgründen die Schreibweise mit Doppelindizes für Pfeile, die eine Eindeutigkeit suggeriert. Dem Benutzer sollte klar
sein, dass im Programm nur der Pfeil mit der kleinsten Bewertung berücksichtigt
wird, da nur über ihn ein kürzester Weg gefunden werden kann. Fallen bei einem
Pfeil Anfangs- und Endknoten zusammen, so sprechen wir von einer Schlinge.
Diese sind in unserem Programm nicht erlaubt, da sie bei positiver Bewertung
ohne Bedeutung sind. Bei negativen Bewertungen stellen sie aber ein Problem
dar, da sie dann einen negativen Zyklus (Def. siehe unten) erzeugen.
i
i
j
Abbildung 2.1: Schlinge und parallele Pfeile
Gibt es im Digraphen einen Pfeil hi, ji, so nennen wir i Vorgänger von j und j
Nachfolger von i. Im Programm verwenden wir das Array pred() (engl. ,,predecessor“), das den Vorgänger eines jeden Knotens im Ergebnisbaum angibt.
Ein Knoten i eines Digraphen, der keinen Vorgänger besitzt heißt Quelle und
ein Knoten j ohne Nachfolger heißt Senke. Besitzt ein Knoten weder Vorgänger
noch Nachfolger, so wird er isoliert genannt. Zwei Knoten i und j in einem Graphen werden zusammenhängend genannt, wenn G mindestens einen Weg von i
nach j oder von j nach i enthält. Ein Digraph wird schwach zusammenhängend
genannt, wenn jedes Knotenpaar zusammenhängend ist. Ein Digraph wird stark
zusammenhängend genannt, wenn jedes Knotenpaar i und j sowohl ein Weg von
i nach j als auch von j nach i führt. Die Dichte ist der Quotient aus Pfeil- und
).
Knotenanzahl ( m
n
Den Pfeilen ordnen wir Bewertungen zu, die wir im folgenden als Kosten (cost)
bezeichnen möchten. Diese können in der Realität tatsächlich Kosten, aber auch
Längen, Zeitdauern, Durchflusskapazitäten, Entfernungen u.a. sein. Für die Kosten eines Pfeils a = hi, ji schreiben wir cij ∈ R ∪ {∞}.
Eine Folge i0 , hi0 , i1 i, i1 , ..., ir−1 , hir−1 , ir iir von Knoten und Pfeilen bezeichnen
wir als Pfeilfolgen mit dem Anfangsknoten i0 und dem Endknoten ir . Abkürzend
schreiben wir hi0 , i1 , ..., iP
r i . Einer Pfeilfolge F = hi0 , i1 , ..., ir i können wir also immer die Kosten c(F ) = rk=1 cik−1 ik zuordnen. Die maximalen Kosten mit denen
ein Pfeil innerhalb des gesamten Digraphen bewertet ist, bezeichnen wir mit C.
Es ist also C = max{cij } (mit i 6= j und i, j ∈ {1, ..., n}).
Eine Pfeilfolge hi0 , i1 , ..., ir i, mit unterschiedlichen Anfangs- und Endknoten i0 6=
ir und mit lauter verschiedenen Knoten heißt Weg. Eine Pfeilfolge mit gleichem
KAPITEL 2. OPTIMALE WEGE
7
2
2
1
1
4
2
1
4
6
6
6
5
5
3
4
7
7
3
3
Abbildung 2.2: Graph, Baum und Weg
Anfangs- und Endknoten aber verschiedenen Zwischenknoten (i0 = ir und ik 6= il
für k 6= l; k, l ∈ {1, ..., r}) nennen wir Zyklus. Enthält ein solcher Zyklus Pfeile
mit negativen Kosten, so dass die Gesamtkosten aller Pfeile des Zyklus negativ
sind, so sprechen wir von einem negativen Zyklus. Diese stellen bei der Suche
nach kürzesten Wegen ein unlösbares Problem dar. Enthält nämlich ein bewerteter Digraph einen Zyklus negativer Länge, so kann dieser beliebig oft durchlaufen
werden, was jedes Mal zu einer Verkürzung des Weges führen würde. Die Länge
des kürzesten Weges wäre in diesem Fall also −∞. Ein gerichteter Graph, der
keine Zyklen enthält wird zyklenfrei oder azyklisch genannt.
Ein schwach zusammenhängender, zyklenfreier Digraph heißt gerichteter Baum
oder Wurzelbaum mit der Wurzel r, wenn r keinen und jeder der übrigen Knoten
von G genau einen Vorgänger besitzt. Die Vorgänger eines Knoten werden auch
als dessen Väter und die Nachfolger als Söhne bezeichnet. Knoten, die keinen
Söhne besitzen werden auch Blätter genannt. Ein Weg von der Wurzel r zu einem
Blatt heißt Ast. Die Tiefe eines Knotens i in einem Wurzelbaum ist die Anzahl
der Pfeile des Weges von der Wurzel r nach i. Die Wurzel r habe dabei die Tiefe
0. Die Tiefe eines Wurzelbaums ist die maximale Tiefe seiner Knoten.
Die Optimale-Wege-Algorithmen haben die Aufgabe, von einem gewählten Startknoten, den wir mit r bezeichnen wollen, den kostenoptimalen Weg zu einem
gewählten Endknoten l, oder zu allen anderen Knoten zu bestimmen. Also einen
Weg, für den crl den kleinst möglichen Wert annimmt. Die Länge des kürzesten
Wegs von r zu einem beliebigen Knoten i bezeichnen wir dabei mit dist(i) (für
engl. ,,distance“).
Zur Beschreibung des Rechenaufwands wird das Landausche Symbol O verwendet1 .
1
Man sagt, zwei auf (0, ∞) definierte, positive Funktionen f,g erfüllen die Beziehung f (x) =
(x)
O(g(x)) für (x 7→ ∞), wenn es positive Zahlen M,K gibt, so dass | fg(x)
| ≤ M für alle x ≥ K
gilt. Siehe dazu [Bei03][Kapitel 1.2.3]
Kapitel 3
Datenstrukturen
Datenstrukturen spielen bei den in dieser Arbeit implementierten Verfahren eine
große Rolle. Wir widmen ihnen daher ein eigenes Kapitel. Effiziente Strukturen
sind Voraussetzung für gute Verfahrensimplementationen. Vielfach besteht sogar
der einzige Unterschied zwischen den Verfahren in der Wahl der Datenstruktur.
Das gilt zum Beispiel für die Shortest-Path-Tree-Verfahren. Algorithmen dieser
Klasse basieren auf Schlangen (,,Queue“) oder Stapeln (,,Stacks“) bzw. Mischformen. Eine solche Form ist das Verfahren Dequeue (,,Double-Ended-Queue“),
in dem eine Kombination der Datenstrukturen Schlange und Stack genutzt wird.
Komplexere Strukturen wie dynamische Listen (,,List“) und Haufen (,,Heap“)
werden etwa in den Varianten ,,Dial“ und ,,Heap“ des Dijkstra-Verfahrens eingesetzt. Die Algorithmen unterscheiden sich darüberhinaus nicht. Außerdem kann
man sowohl Schlangen, als auch Stacks mit einfach und doppelt verketteten Listen (,,Single/Doubly Linked List“) implementieren.
Bei einem Vergleich der Verfahren kann also eine Untersuchung der Datenstrukturen nicht vermieden werden. Alle Strukturen wurden daher in VBA als eigenständige Klassenmodule programmiert. Jeder Typ hat die gleichen Schnittstellen, so dass Module leicht ausgetauscht werden können.
Als obere Grenze dieser Untersuchung wurde eine halbe Stunde vorgegeben, das
heißt, Tests mit einer Laufzeit über 30 Minuten wurden abgebrochen. Das sollte
für die einfachen Vergleiche, die angestellt wurden, reichen. Wir wollten schließlich nur Flaschenhälse von Anfang an durch die Datenstrukturen vermeiden und
keine raffinierte Testsuite erarbeiten. Die Unterschiede fallen letztendlich in der
Praxis sowieso geringer aus.
Im folgenden wird ein kurzer Überblick über die benutzten Datenstrukturen gegeben. Detailliertere Erläuterungen finden sich in Büchern zu Algorithmen und
Datenstrukturen, oder auch in [AMO93, Appendix A].
8
KAPITEL 3. DATENSTRUKTUREN
3.1
9
Array
Arrays sind die einfachste geordnete Datenstruktur, sie sind geordnete Listen
gleicher Variabelentypen, deren Zugriff über einen Index erfolgt. Der Zugriff auf
das k-te Element erfolgt durch Aufruf mit Index k. Das erfordert nur einen Aufwand O(1), da der Computer die Speicherstelle direkt adressieren kann.
Arrays sind in ihrer Struktur statisch, dass heißt, die Festlegung ihrer Größe findet im Allgemeinen während der Entwicklungszeit statt und nicht erst während
der Laufzeit, zu der womöglich schon Elemente hinzugefügt wurden.
In VBA ist allerdings beides möglich: Eine Größenfestlegung (,,Redimensionierung“) kann mittels des Befehls Redim zu jeder Zeit im Programm beliebig oft
vorgenommen werden. Durch die Erweiterung Preserve kann ein Array sogar
in der Größe verändert werden, so dass der Inhalt erhalten bleibt. Beide Operationen haben aber eine schlechte Performance, so dass sie möglichst vermieden
werden sollten. Das ReDim (Preserve) Statement allokiert nämlich zuerst einen
neuen Speicherbereich und erzeugt dort ein neues Array. Danach werden ggf.
automatisch die Elemente des alten Arrays in das neue kopiert und schließlich
der alte Speicher freigegeben. Das verursacht erheblichen Aufwand, siehe dazu
[GG01, Alternatives to ReDim Preserve]. Unter diesem Gesichtspunkt kann in
VBA nur bedingt von ,,dynamischen Arrays“ gesprochen werden.
3.2
Verkettete Listen
Verkettete Listen (,,Linked Lists“) bestehen anders als Arrays aus einzelnen
Listenelementen, die nur ihre Nachbarn kennen. Sie liegen im Speicher nicht
zwangsläufig nebeneinander wie die Elemente eines Arrays. Die Elemente einer
einfach verketteten Liste haben nur über ihren unmittelbaren Nachfolger Kenntnis, doppelt verkettete zusätzlich über ihren Vorgänger. Darüber hinaus sind nur
noch das erste und evtl. letzte Elemente bekannt. Man kann also nicht direkt auf
das k-te Element zugreifen, sondern muss zuerst über alle vorherigen iterieren.
Das entspricht einem Aufwand von O(k). Der Zugriff auf das jeweils nächste Element in der Liste erfordert nur den minimalen Aufwand O(1).
Die doppelt verkettete Liste ist sehr effizient für Einfüge- und auch Löschoperationen, da nur die Verweise der Nachbarn geändert werden müssen. Gleiches
1
2
k
n-1
Abbildung 3.1: Einfach verkettete Liste
n
KAPITEL 3. DATENSTRUKTUREN
1
2
k
10
n-1
n
Abbildung 3.2: Doppelt verkettete Liste
gilt für die einfach verkettete mit Einschränkungen, was die Effizienz bei Löschoperationen betrifft.
Gewöhnlich nutzt man Zeiger (,,Pointer“) um die einzelnen Listenelemente zu
verketten. Jedes Element besteht dann aus zwei bzw. drei Bausteinen: Den eigentlichen Daten und ein oder zwei Zeigern jeweils auf den Nachfolger bzw. Vorgänger
in der Liste (siehe Abbildungen 3.1 und 3.2). Visual Basic verfügt anders als C
oder C++ nicht über eine Pointerarithmetik, deren Platz nehmen hier einfache
Referenzen (,,Links“) auf Objekte ein.
3.3
Collections
Zum Sprachumfang von Visual Basic for Applications gehört die Klasse ,,Collection“, eine intern in C++ implementierte verkettete Liste [McK97, Chapter
4: Collecting Objects]. Es handelt sich also hierbei nicht um eine eigene Datenstruktur, sondern vielmehr um eine Implementierung. Der Zugriff erfolgt, wie
bei einem Array üblich, über einen Index, so dass sich Collections sehr komfortabel benutzen lassen. Allerdings erfordert der Zugriff auf das k-te Element
intern – wie bei einer verketteten Liste üblich – immer einen hohen Aufwand,
nämlich O(k). Besonders deutlich wird das Problem bei Schleifen, in denen mit
dem Index auf die Collection zugegriffen wird: for-to-next. Der Zugriff auf das
nächste Element in der Collection erfolgt dann nicht mehr, wie bei einer Verketteten Liste üblich mit Aufwand O(1), sondern jeweils mit O(k). Ein Ausweg ist
die for-each-in-next-Syntax, die eine Collection nicht zählergesteuert sondern
elementweise durchläuft.
3.4
Strukturen für Graphen
Graphen, wie sie in Kapitel 2.2 vorgestellt wurden, müssen im Rechner zur Bearbeitung abgespeichert werden. Übliche Strukturen sind
• Kantenliste
• Adjazenzliste
KAPITEL 3. DATENSTRUKTUREN
11
2
1
2
3
3
5
6
4
2
5
7
6
4
7
4
2
1
4
7
6
5
3
Abbildung 3.3: Adjazenzliste
• Vorwärtsstern
• Adjazenz- und Inzidenzmatrix
Die Kanten- oder besser Pfeilliste ist eine Folge von m Elementen, jeder Eintrag beschreibt einen Pfeil mit Anfangs- und Endknoten, sowie etwaiger Kosten.
Diese Struktur benötigt wenig Speicherplatz O(m), beschreibt aber durch ihre
lineare Form den Graphen nur unzureichend. Die Ermittlung von Nachfolgeknoten gestaltet sich aufwendig, dabei sollte gerade dieser Vorgang mit geringem
Aufwand verbunden sein. Das Programm zu dieser Ausarbeitung nutzt nur zum
Im- und Export Kantenlisten im ,,DIMACS Graph Format“ ([DIM90]).
Programmintern wird der Graph in Form einer Adjazenzliste verwaltet (siehe Abbildung 3.3). Die Vorgehensweise dabei ist, für jeden Knoten eine eigene
Liste seiner Nachfolger anzulegen und diese ggf. mit Kosten abzuspeichern. Der
Speicherbedarf liegt bei O(n + m). Das wurde in VBA durch ein Array in Größe
der Knotenanzahl (n) realisiert, wobei jedes Feld des Arrays auf eine Nachfolgerliste (Collection) verweist.
Erste Versionen dieses Programms nutzten noch Arrays für die Speicherung der
Nachfolgerlisten, wie es in [NM04, Kapitel 2.2.2] vorgeschlagen wird. Diese Struktur heißt dann auch Vorwärtsstern. Sie erwies sich jedoch als zu langsam beim
Import der Kantenliste. Zum einen weil die Arrays wegen ihrer statischen Größe
nicht sukzessive aufgebaut werden konnten, zum anderen weil der benutze Algorithmus zur Umwandlung einen Aufwand von O(n2 ) hatte.
Die Formate Adjazenz- und Inzidenzmatrix sind nur interessant, wenn ausschließlich sehr dichte Graphen bearbeitet werden, weil sie in jedem Fall Speicher
der Größe O(n2 ) bzw. O(m · n) benötigen.
KAPITEL 3. DATENSTRUKTUREN
3.5
12
Vergleichende Analyse
3.5.1
Queue
Dequeue
Enqueue
Abbildung 3.4: Funktionsweise einer Queue
Die Queue ist eine Warteschlange, in der Elemente in der Reihenfolge abgearbeitet werden, wie sie hinzugefügt wurden (First-In-First-Out). Traditionell wird das
Hinzufügen eines Elements in eine Queue mit ,,Enqueue“ und das Entfernen mit
,,Dequeue“ bezeichnet (letzteres bezeichnet hier die Operation und sollte nicht
mit der gleichnamigen Datenstruktur aus Kapitel 3.5.3 verwechselt werden). Beide Operationen haben theoretisch den Aufwand O(1).
Der Test bestand darin n Elemente in die Datenstruktur hinzuzufügen und danach wieder zu entfernen. Tabellen A.1, A.2 und A.3 zeigen die – auf Grund der
verschiedenen Datenstrukturen – unterschiedlichen Ergebnisse. Eine Übersicht
der Gesamtlaufzeiten bei wachsender Zahl der Elemente bietet Abbildung 3.5.
1000000
100000
Millisekunden
10000
Statisches Array
1000
Collection
Verkettete Liste
100
10
1
10
100
1000
10000
100000
1000000
Anzahl Elem ente
Abbildung 3.5: Gesamtlaufzeiten der Queues
Das Array wird jeweils mit der festen Größe von einer Million initialisiert und
benötigt daher unabhängig von der Größe etwa 30ms für die Allokierung des Spei-
KAPITEL 3. DATENSTRUKTUREN
13
chers. Bei kleiner Elementanzahl stellt der Initialisierungsprozess den Flaschenhals dar, noch bei 10.000 Einträgen sind es 30% der Gesamtzeit. Darüber hinaus
ist der Speicherverbrauch immens. Allerdings sind die Enqueue- und DequeueAufrufe dadurch die schnellsten unter allen Datenstrukturen. Das verwundert
nicht, da im Grunde nur die Zeit für das Lesen und Schreiben eines Arrays
gemessen wird. Ebenso entspricht der strikt lineare Verlauf der Enqueue und
Dequeue-Operationen den Erwartungen.
Die Collection – Microsofts Version einer verketteten Liste – schlägt sich im
Test hervorragend, die Gesamtlaufzeit liegt bei weniger als einer Million Einträge
nur wenig über der des Arrays. Bei mehr als einer Million Einträgen dürfte die
Collection weiter zurückfallen, das war allerdings nicht Bestandteil dieses Tests.
Aber gerade bei kleiner Elementanzahl ist die Collection weit überlegen, da die
Allokierung des gesamten Speichers in der Initialisierungsphase entfällt und statt
dessen sukzessive allokiert wird. In Abbildung 3.5 ist deutlich das lineare Verhalten der Gesamtlaufzeit zu sehen, anders als beim Array zuvor.
Die Implementierung mit einer Verketteten Liste verhält sich ähnlich wie die
Collection; es liegt immerhin die gleiche Datenstruktur zu Grunde. Die Liste ist
allerdings langsamer, weil sie weniger stark optimiert und weniger systemnah programmiert wurde. Im direkten Vergleich der beiden Implemetierungen benötigen
die Enqueue-Operationen 3-mal länger und die Dequeue-Operationen immer noch
das 1,5-fache. Das bedeutet unter dem Strich die doppelte Gesamtlaufzeit und
damit bei kleiner Elementanzahl Platz 2 und sonst Platz 3.
3.5.2
Stack
Pop
Push
Abbildung 3.6: Funktionsweise eines Stapels
Ein Stack entspricht der Arbeitsweise eines Stapels, bei dem zuletzt hinzugefügte
Elemente zuerst abgearbeitet werden (Last-In-First-Out). Traditionell wird das
Hinzufügen eines Elements in einen Stack mit ,,Push“ und das Entfernen mit
,,Pop“ bezeichnet. Beide Operationen weisen einen Aufwand von O(1) auf. Gemessen wurde wieder die Zeit für das Hinzufügen und Entfernen. Tabellen A.4,
A.5 und A.6, sowie Abbildung 3.7 zeigen die Ergebnisse des Tests.
KAPITEL 3. DATENSTRUKTUREN
14
1000000
100000
Millisekunden
10000
Dynamic Array
1000
Collection
Linked List
100
10
1
10
100
1000
10000
100000
1000000
Anzahl Elem ente
Abbildung 3.7: Gesamtlaufzeiten der Stacks
Das Array ist diesmal dynamisch implementiert, wird also bei Bedarf vergrößert.
Um die Anzahl der Redimensionierungen gering zu halten, wird das Array, wenn
die Größe nicht ausreicht, jeweils um einen Puffer (,,Chunk“) vergrößert und
nicht nur um ein Element. Für diesen Test wurde ein Chunk von 100 Elementen verwendet, größere Werte führen zu besserer Laufzeit, aber auch zu mehr
Speicherbedarf. Abbildung 3.7 zeigt deutlich den schnell wachsenden Aufwand
beim Anlegen des neuen Speichers, und den internen Kopieroperationen. Daraus
resultiert eine Gesamtlaufzeit, die nicht linear mit der Elementanzahl verläuft.
Dieses Verhalten ist unerwünscht und gerade bei großer Elementanzahl ist diese
Implementierung nicht zu empfehlen.
Für die Collection boten sich zwei verschiedene Implementierungen an. Zum
einen das Lesen und Schreiben am Ende oder aber am Anfang der Liste. Die
Collection ist grundsätzlich für das Lesen am Anfang und schreiben am Ende
optimiert, sodass sich diese Frage bei der Queue nicht stellte. Für den Stack ist
es aber bedeutsam, an welchem Ende beide Operationen durchgeführt werden.
Die Löschoperation des letzten Elements in einer Collection erfordert nämlich
einen viel höheren Aufwand als die des ersten, während das Hinzufügen vorne
und hinten gleich schnell erfolgt (siehe dazu Tabellen A.2 und A.5).
Das hat fatale Konsequenzen: Die Gesamtlaufzeit der Collection, die ihre Operationen vorne durchführt, liegt in der gleichen Größenordnung wie die verwandte Queue und ist damit die beste Wahl unter den Stacks. ,,Push“ und ,,Pop“Operationen benötigen in etwa die gleiche Zeit. Die andere Variante ist aber
völlig unbrauchbar: schon bei 10.000 Elementen ist die Gesamtlaufzeit statt einer
Sekunde eine halbe Minute.
KAPITEL 3. DATENSTRUKTUREN
15
Die Verkettete Liste zeigt wie bei der Queue eine langsamere Performance als
die Collection, wobei hier der Unterschied etwas geringer ausfällt. Der CollectionStack geht nämlich im Vergleich langsamer zur Sache als die Collection-Queue,
während der List-Stack geringfügig schneller arbeitet als die List-Queue. Es fällt
wieder auf, dass das Hinzufügen erheblich länger dauert als das Entfernen aus
der Liste.
3.5.3
Dequeue
Pop
Push
Enqueue
Abbildung 3.8: Funktionsweise einer Dequeue
Wie bereits in der Einführung zu diesem Kapitel angedeutet, erlaubt die Datenstruktur Dequeue (,,Double-Ended-Queue“) Elemente am Anfang (wie bei einem
Stack) und am Ende (wie bei der Queue) hinzuzufügen. Beide Operationen erfordern natürlich weiterhin nur O(1) Operationen. Abgegriffen wird nur am Anfang,
ebenfalls mit einem Aufwand von O(1). Diese Datenstruktur findet beim gleichnamigen Verfahren von Gallo und Pallotino Anwendung, siehe Kapitel 4.1.2.
Aufgrund der engen Verwandtschaft zu den bereits behandelten Datenstrukturen
wurde auf einen Test verzichtet. Es empfiehlt sich in VBA auch hier die Implementation mit einer Collection vorzunehmen.
3.5.4
List
Auf Listen können nicht nur Operationen am Anfang und am Ende wie bei den
Sonderfällen Schlange und Stapel durchgeführt werden, sondern an beliebigen
Stellen. Möglich sind Such- (,,Search“) und Lese- (,,Item“), Einfüge- (,,Add“)
sowie Löschoperationen (,,Remove“). Diese erfordern aber im Allgemeinen nicht
nur einen Aufwand von O(1).
Die im Rahmen dieses Projekts implementierten Verfahren benötigen nur einen
Bruchteil der gesamten Möglichkeiten zur Manipulation einer Liste, daher richtet sich das Augenmerk in diesem Test auf das Suchen und direkte Löschen von
Elementen. Diese Operationen werden im Verfahren von Dial benutzt. Die dort
ebenfalls benötigten Operationen Enqueue und Dequeue wurden bereits im Kapitel 3.5.1 zur Queue auf Seite 12 behandelt.
KAPITEL 3. DATENSTRUKTUREN
16
1000000
100000
Millisekunden
10000
Collection
1000
Doubly Linked List
Collection (slow )
100
10
1
10
100
1000
10000
100000
1000000
Anzahl Elem ente
Abbildung 3.9: Gesamtlaufzeiten der Listen
Die doppelt verkette Liste benötigt für ihre Hinzufüge-Operationen (,,Enqueue“) etwa 3-4 mal länger als die Collection (Tabelle A.8). Bei der einfach
verketteten Liste ging dieser Vorgang noch etwas schneller, liegt aber in der gleichen Größenordnung. Würde man auf der doppelt verketteten Liste ,,Dequeue“Operationen durchführen, so wäre diese ebenfalls entsprechend langsamer.
Der Suchvorgang ist bei allen Varianten der zeitaufwendigste Prozess. Die doppelt verkette Liste ist dabei die zweitschnellste Implementierung; sie benötigt
erheblich weniger Zeit als die indexbasierte Suche in einer Collection. Die Löschoperationen sind immer schneller als die der anderen Varianten. Allerdings fällt
dieser Vorsprung bei weniger als einer Millionen Einträge nicht ins Gewicht.
Die Erweiterung der Collection offenbart – wie bereits erwähnt – die Schwächen
des Zugriffs über eine Indexvariable (Tabelle A.7). Das Hinzufügen (am Ende)
geht wie beim Stack (siehe 3.5.2) sehr schnell, da hier die Liste nicht durchlaufen
werden muss. Durchsucht man die Liste indexbasiert, so ist die Search-Operation
katastrophal langsam (,,Collection (slow)“ in Abbildung 3.9). Während hier die
doppelt verkettete Liste für die Abarbeitung von 10.000 Elementen weniger als
eine halbe Sekunde benötigt, lässt die Collection über 10 Minuten verstreichen.
Wählt man die elementweise Syntax for-each-in-next ist die Collection beim
Suchen von Elementen wesentlich schneller als die verkettete Liste. Das Löschen
bleibt allerdings langsamer, weil man hier auf die Adressierung mit einem Index
angewiesen ist. Dieser Vorgang benötigt daher jeweils O(n). Der Flaschenhals ist
also der Index, wie es in Kapitel 3.3 bereits erwähnt wurde.
Die Collection ist also bis zu einer Millionen Elemente unter Benutzung der
Syntax for-each-in-next die bessere Wahl. Bei größerer Elementanzahl wird
KAPITEL 3. DATENSTRUKTUREN
17
der Mehraufwand beim Löschen die Vorteile beim Hinzufügen und Durchsuchen
übersteigen.
3.5.5
Heap
2
4
5
10
12
2
7
6
8
11
4
5
10
7
6
8
12
11
Abbildung 3.10: Binärer Heap
Wenn man von einem Heap spricht, meint man meist einen binären Heap (Abbildung 3.10). Das ist ein Binärbaum mit den Eigenschaften, dass jeder Knoten im
Allgemeinen zwei Söhne hat, deren Werte immer größer sind als die des Vaters.
Darüberhinaus ist ein Heap bis auf die letzte Ebene vollbesetzt. Diese wird von
links besetzt, so dass maximal ein Vater nur einen Sohn hat.
Einen Heap kann man nun auf mehrere Arten modifizieren. Zum einen kann die
Sortierung geändert werden, so dass die Wurzel nicht das kleinste, sondern das
größte Element darstellt. Darüberhinaus kann die Anzahl der Söhne variiert werden, dann spricht man von einem d-Heap, wobei das d für die Anzahl der Söhne
steht.
Ein d-Heap lässt sich sehr einfach in einem Array speichern: Die Wurzel des
Baums wird an Position 1 des Arrays abgelegt, die d Nachfolger eines Knotens
an der Arrayposition k werden an den Positionen (k − 1) · d + 2 bis k · d + 1 gespeichert.
Ein Heap ist eine mögliche Implementierung einer so genannten priorisierten
Warteschlange (,,Priority Queue“)1 . Das oberste Element ist stets das Minimum, dessen Entfernung (,,delete-min“) erfordert nur O(1) Operationen. Das
Einfügen neuer Elemente (,,insert“) ist – unter Beibehaltung der Heap-Struktur
1
Eine Priority Queue lässt sich auch durch andere Datenstrukturen implementieren, wie etwa
den ,,Fibonacci Heap“ oder den ,,Radix Heap“, im Grunde auch durch das Bucket-Arrangement
von Dial. Siehe [AMO93, Appendix A]
KAPITEL 3. DATENSTRUKTUREN
18
1000000
100000
Millisekunden
10000
Binary Heap
1000
3-Heap
100
10
1
10
100
1000
10000
100000
1000000
Anzahl Elem ente
Abbildung 3.11: Gesamtlaufzeiten der Heaps
– im binären Heap mit O(log2 n) Operationen möglich. In diese Überlegung geht
wesentlich die Tiefe des Baumes von log2 n ein. Zum Vergleich: Das Entfernen des
Minimums benötigt in jeder linearen Datenstruktur O(n) Operationen, während
das Hinzufügen etwa zu einer Queue nur einen Aufwand von O(1) erfordert.
Der durchgeführte Test entspricht einem Sortierverfahren, denn genau das ist das
sukzessive Ausgeben der Minima. Hierbei zeigt der Heap eine sehr gute Performance. Das statische Array, auf dem der Heap beruht, ermöglicht eine geringe
Gesamtlaufzeit: Der binäre Heap (Tabelle A.9) benötigt für die Sortierung von
einer Millionen Elementen etwa genauso lange wie ein dynamisches Array nur für
das Hinzufügen (Tabelle A.4). Anders als bei der Queue-Variante (Tabelle A.1),
bei der das Array statisch mit einer Konstanten deklariert wurde, wird jetzt dynamisch zur Laufzeit dimensioniert.
Es wurden zwei Heaps mit d=2 und und d=3 getestet. Beide verhalten sich sehr
ähnlich, allerdings ist der d-Heap für d=3 immer schneller als die binäre Variante.
Die Leistung der Datenstruktur im Algorithmus wird also auch von der Wahl des
Parameters d abhängen.
Kapitel 4
Verfahren
Im folgenden stellen wir kurz die Algorithmen zur Bestimmung von optimalen
Wegen vor, deren Leistungsfähigkeit wir in unserem Projekt gegenüberstellen
wollen.
Ausgehend von einem Startknoten r ermitteln die Verfahren optimale Wege zu
einem gewählten Endknoten oder zu allen, von r aus erreichbaren Knoten. Die
Länge des kürzesten Wegs von r nach i bezeichnen wir mit dist(i).
Bei den Algorithmen unterscheiden wir zwischen Label-Correcting- und LableSetting-Verfahren. Von einem Lable-Correcting-Verfahren sprechen wir, wenn ein
(von r aus erreichbarer) Knoten i mehrmals in die Menge der markierten Knoten
Q aufgenommen und wieder eliminiert wird und somit die Marke (engl. label)
dist(i) des Knotens i neu bestimmt wird. Der Knoten i wird also neu markiert.
Solche Verfahren sind beispielsweise: Der Fifo-Algorithmus, Dequeue, 2-Queue,
der Schwellenalgorithmus und das PSP-GS-Verfahren.
Dagegen sprechen wir von einem Lable-Setting-Verfahren, wenn jeder (von r aus
erreichbare) Knoten i nur einmal in die Menge der markierten Knoten Q aufgenommen bzw. aus ihr entfernt wird. Die Marke dist(i) wird also bei der Elimination vom Knoten i aus Q bereits endgültig festgesetzt. Beispiele hierfür sind das
Verfahren von Dijkstra und die Version von Dial.
4.1
4.1.1
Label-Correcting
Fifo
Dieses Verfahren wurde von Bellmann 1956 entwickelt und ist auch unter dem
Namen Bellmann-Verfahren bekannt. Beim Fifo-Algorithmus (first-in-first-out)
wird eine Liste verwendet, die als Schlange (queue) implementiert wird, da diese
Datenstruktur bereits nach dem Fifo-Prinzip arbeitet, d.h. Knoten werden am
Anfang (am Kopf der Schlange) entfernt und weitere Knoten nur am Ende ein-
19
KAPITEL 4. VERFAHREN
20
gefügt. In VBA wurde das unter Benutzung der Collection-Klasse realisiert, siehe
Kapitel 3.5.1.
Ausgehend vom Startknoten r wird jeweils ein Knoten vom Kopf der Liste Q
entnommen und seine Nachfolger auf Verkürzungen der Distanzmarken dist(i)
untersucht. Falls diese Nachfolger noch nicht in Q enthalten sind, werden diese
am Ende der Liste Q hinzugefügt. Das Verfahren wird solange durchlaufen, bis
in der Liste Q kein Knoten mehr enthalten ist.
Das Verfahren hat eine Laufzeit von O(mn), da in jedem Iterationsschritt jeder
Pfeil maximal einmal untersucht wird. Der Rechenaufwand pro Iterationsschritt
beträgt also O(m). Da der Digraph G n Knoten besitzt hat jeder Weg in G
höchstens n − 1 Pfeile. Folglich sind alle Entfernungen von r nach n − 1 Iterationsschritten ermittelt, da in jedem Iterationsschritt immer die Nachfolger mit
Verbesserungen aufgenommen werden. Am Ende jedes Iterationsschrittes sind also immer alle kürzesten Wege mit der Pfeilanzahl ≤ v, wobei v = 1, ..., n − 1 ist,
ermittelt.
4.1.2
Dequeue
Dieses, von Gallo und Pallottino 1986 entwickelte Verfahren unterscheidet bei
der Aufnahme von Knoten in Q, ob diese erstmalig aufgenommen werden oder
ob diese schon zuvor in Q waren. Wird ein Knoten erstmalig in Q aufgenommen,
so wird dieser an den Anfang von Q, andernfalls an das Ende gestellt. Abgegriffen wird immer am Ende der Liste Q. Diese Datenstruktur ist eine Mischung
aus Queue und Stack (siehe Kapitel 3.5.3). Die Knoten, die zum wiederholten
mal in Q aufgenommen werden, werden also als erstes wieder abgearbeitet. Diese Modifikation trägt der Beobachtung Rechnung, dass so schneller verkürzende
Wege gefunden werden. Allerdings verschlechtert sich dadurch die Abschätzung
der Laufzeit, so dass sie nicht mehr polynomial, sondern exponentiell ist. Das
Verfahren endet, wenn kein Knoten mehr in Q vorhanden ist.
4.1.3
2-Queue
Dieses ebenfalls auf Gallo und Pallottino (1986) zurückgehende Verfahren arbeitet ähnlich wie Dequeue. So wird auch bei diesem Algorithmus unterschieden ob
ein Knoten das erste oder zum wiederholten mal in Q aufgenommen wird. Dieses Verfahren arbeitet allerdings mit zwei Queues q1 und q2 und dadurch einer
polynomialen Laufzeit von O(m2 n). In q1 werden die Knoten aufgenommen, die
erstmalig untersucht werden, während in q2 die Knoten, die schon früher einmal in q1 oder q2 waren, aufgenommen werden. Diese Knoten werden immer am
Anfang der jeweiligen Liste angefügt. Abgegriffen wird bei diesem Verfahren am
Ende von q2. Sollte q2 leer sein, wird am Ende von q1 abgegriffen. Es werden
KAPITEL 4. VERFAHREN
21
also auch bei diesem Algorithmus zuerst die Knoten abgearbeitet, die wiederholt
in eine der beiden Listen aufgenommen wurden.
4.1.4
PSP-Verfahren
In diesem Partitioning-Shortest-Path-Verfahren von Glover und Klingman (1989)
werden zwei Listen q1 und n1 geführt. Ihre Implementierung ist dabei unbedeutend. Bei uns wurden beide Listen als Queue gewählt, so dass das Verfahren im
Grunde eine Variante des Fifo-Algorithmus ist. Beim Start des Verfahrens gehen wir vom trivialen Vermessungsansatz aus. Dabei enthält q1 alle Knoten mit
endlichem dist-Wert, also den Startknoten. Die zweite Liste ist anfänglich leer.
In jedem Durchgang wird dann q1 vollständig abgearbeitet. Neu aufzunehmende
Knoten, die noch in keiner Liste enthalten sind, werden n1 zugefügt. Am Ende
eines Durchgangs wird dann q1 durch n1 ersetzt und n1 = ∅ gesetzt. Das Verfahren endet, wenn beide Listen leer sind.
4.1.5
PSP-GS Algorithmus
Bei diesem Verfahren wird eine Verkleinerung der Marke dist(i) eines Knotens
i direkt an seine Nachfolger im aktuellen Wegebaum weitergegeben. Wird also
dist(i) im Rahmen des PSP-Verfahrens verkleinert, so werden direkt die distMarken aller Knoten des Astes von i aufgerufen und aktualisiert. Um sich besser
und schneller im aktuellen Wegebaum zurecht zu finden, werden zusätzlich noch
zwei Hilfsfunktionen eingeführt.
Die Fadenfunktion (,,thread“) hat folgende Eigenschaften, wobei t : K → K und
G = (K, p) ein Wegebaum sei.
1. K = {r, t(r), t2 (r), ..., tn−1 (r)}
2. tn (r) = r
3. Für alle k ∈ K ist t(k) ein direkter Nachfolger (im Wegebaum) von k, sofern
ein solcher existiert. Besitz k keinen Nachfolger, so ist t(k) ein noch nicht
aufgezählter direkter Nachfolger des ersten Knotens k 0 der bei Rückwärtsverfolgen der Vorgängerfunktion von k aus angetroffen wird und der einen
noch nicht aufgezählten direkten Nachfolger besitzt.
Die Tiefenfunktion (,,depth“) gibt die Tiefe des Wegebaums an (siehe Kapitel
2.2), also die Anzahl der Pfeile von der Wurzel r zum gewünschten Knoten i.
KAPITEL 4. VERFAHREN
22
Für Digraphen ohne negative Zyklen beträgt die Laufzeit des Verfahrens O(mn2 ),
sofern mit jeweils O(1) aus q1 abgegriffen und in n1 abgelegt wird.[Bei03][Kapitel
1.3.2].
Die Implementierung dieses Verfahrens ist im Rahmen des Seminars nicht erfolgt.
4.1.6
Schwellenalgorithmus
Bei diesem Algorithmus von Glover, Glover und Klingman werden 3 Listen q1,
q2 und n1 verwendet. In unserem Programm wurden diese als Queues implementiert. Dies ist jedoch nicht zwingend notwendig, da die Listen entweder immer
ganz abgearbeitet oder ganz durchsucht werden. Eine Implementierung mit anderer Datenstruktur, z.B. Stack wäre also genauso denkbar und in der Zeitmessung
ähnlich, wenn nicht gleich. Der Algorithmus verwendet einen Schwellenwert t,
der die erreichten Knoten nochmals nach ihrer Längenbewertung dist(i) in zwei
verschiedene Listen q1 und n1 bzw. q2 und n1 aufteilt. Gestartet wird mit einem Schwellenwert von t=0. Im Laufe des Verfahrens wird dieser bei Bedarf, d.h.
wenn q1 und q2 leer sind, neu errechnet. Hierzu wird unter anderem vor dem
Verfahren ein Wert P ∈ [0, 25; 1, 5] gewählt. Dieser soll hoch angesetzt werden,
falls der Graph regelmäßige Strukturen aufweist, wie z.B. ein Gittergraph. Dagegen soll der Wert in dem Maße niedrig angesetzt werden, wie dies nicht der
Fall ist, also z.B. bei zufällig erzeugten Graphen. Unsere Beobachtungen haben
gezeigt, dass unsere Implementation bei einem Wert von P = 0,25 die besten
Geschwindigkeiten zeigt. Wir kürzen das Verfahren mit ,,Thresh“ ab, da es im
englischen Sprachraum unter dem Titel ,,Threshold“ bekannt ist.
Ausgehend vom Startknoten r werden alle Nachfolgerknoten i der Knoten in q1
auf Verkürzungen von dist(i) untersucht. Ist dies der Fall, werden diese, falls
dist(i) ≤ t ist in q2, andernfalls in n1 abgelegt. Wenn die erste Liste q1 abgearbeitet ist, werden die Knoten aus q2 in q1 gegeben, sofern diese nicht leer ist.
Sollte dies der Fall sein, so wird der Schwellenwert nach obigem Schema aktualisiert und n1, die einzige nichtleere Liste wird durchsucht. Dabei werden alle
Knoten deren Bewertung dist(i) unter dem Schwellenwert t liegt, in q1 gegeben.
Danach startet das Verfahren erneut mit der Untersuchung der Nachfolger.
Dieser Algorithmus läuft nur für nichtnegative Längenbewertungen mit zufriedenstellender Laufzeit, nämlich O(mn).
KAPITEL 4. VERFAHREN
4.2
4.2.1
23
Label-Setting
Diijkstra
Bei diesem Algorithmus, der bereits 1959 von Dijkstra entwickelt wurde, wird der
Knoten i mit der kleinsten Marke dist(i) aus der Menge der markierten Knoten,
der Liste Q, ausgewählt. Es wird versucht die Marke dist(j) jedes Nachfolgers j
von i zu verbessern. Dabei wird jeder Knoten höchstens einmal in Q aufgenommen und damit jeder Pfeil auch nur höchstens einmal inspiziert. Diese klassische
Implementation hat eine Laufzeit von O(n2 ). Wird für Q eine priorisierte Warteschlange gewählt (siehe Kapitel 3 Datenstrukturen) erhält man eine sehr effiziente
Variante. In unserem Programm wird ein binärer Heap genutzt, daher heißt dieses
Verfahren zur Abgrenzung ,,Heap“. Die Laufzeit dieser Variante ist O(m log n).
Diese Datenstruktur ermöglicht nämlich einen schnelleren Zugriff mit O(log2 n)
auf den Knoten mit minimaler Marke, da nicht die ganze Liste Q durchsucht
werden muss, was ein Aufwand von O(n) erforderte. Siehe dazu auch [NM04].
Wird nur ein kürzester Weg vom Startknoten r zu einem erreichbaren Zielknoten
gesucht, so kann das Dijkstra-Verfahren im Gegensatz zu den Label-CorrectingVerfahren abgebrochen werden, sobald der Zielknoten aus Q entfernt wurde.
Das Verfahren von Dijkstra kann allerdings nur für positive Längenbewertungen
sinnvoll verwendet werden, da es sonst möglich wäre, dass ein Knoten i O(2n )mal in Q aufgenommen und wieder entfernt wird.
4.2.2
Dial’s Implementation
Bei diesem Algorithmus, der von Dial schon 1969 vorgestellt wurde, handelt es
sich nur um eine spezielle Version des Dijkstra-Algorithmus. Diese beiden unterscheiden sich durch die verwendeten Datenstrukturen. Die Nachfolgerknoten,
ausgehend vom Startknoten r, werden nun entsprechend ihrer Marke dist(i) in n·C
durchnummerierte Buckets (Körbe) angeordnet. Ausgehend vom ersten Bucket
mit dem Wert 0 werden diese Nacheinander abgearbeitet.
Eine effizientere Implementierung des Verfahrens ordnet diese Buckets nicht linear an, sondern in einem Kreis (siehe Abbildung 4.1 auf Seite 24). Dies hat den
Vorteil, das nur C + 1 Buckets implementiert werden müssen. Der Zugriff im
Programm auf den richtigen Bucket erfolgt dann mittels modulo (C + 1). Dies
gibt dann die richtige Nummerierung des Buckets im Array an und ermöglicht
so den Zugriff auf das richtige Feld im Array. Die Laufzeit beträgt O(m + nC).
Implementiert wurde die Struktur als ein Feld von verketteten Listen (siehe Kapitel 3.5.4) unter Benutzung der Collection-Klasse.
KAPITEL 4. VERFAHREN
24
0
C
...
...
2
C1
1
...
k
...
Abbildung 4.1: Bucketarrangement nach Dial
4.3
Pseudocode
Zur Verdeutlichung der zuvor beschriebenen Verfahren, werden die beiden grundlegenden Algorithmen noch einmal im Pseudocode abgedruckt. Die Syntax wurde
dabei an Pascal angelehnt, siehe dazu auch [NM04].
Das Listing 4.1 zeigt den Prototypen eines Label-Correcting-Verfahrens. Abbruchbedingungen zur Erkennung von negativen Zyklen sind noch nicht enthalten.
Wählt man Q als Schlange (siehe Kapitel 3.5.1), entspricht es exakt dem FifoVerfahren.
Listing 4.2 implementiert den Dijkstra-Algorithmus mit Priority Queue beispielhaft für die Label-Setting-Verfahren. Wählt man für H einen binären Heap, entspricht es der Variante Heap, wählt man hingegen das Bucketarrangement erhält
man die Version nach Dial.
KAPITEL 4. VERFAHREN
25
Listing 4.1: Label-Correcting-Verfahren
program L a b e l C o r r e c t i n g ;
begin
dist(r) := 0 and pred(r) := 0 ;
dist(j) := ∞ for each node j ∈ N \ {r} ;
Q := r ;
while Q 6= Ø do
begin
remove an el eme nt j from Q;
for each a r c (i, j) ∈ A(i) do
i f dist(j) > dist(i) + cij then
begin
dist(j) := dist(i) + cij ;
pred(j) := i ;
if j ∈
/ Q then add node j to Q ;
end ;
end ;
end ;
Listing 4.2: Dijkstra (mit Priority-Queue H)
program D i j k s t r a ;
begin
create(H) ;
dist(j) := ∞ for a l l j ∈ N ;
dist(r) := 0 and pred(r) := 0 ;
insert(r, H) ;
while H 6= Ø do
begin
f indM in(i, H) ;
deleteM in(i, H) ;
for each (i, j) ∈ A(i) do
begin
value := dist(i) + cij ;
i f dist(j) > value then
i f dist(j) = ∞ then dist(j) := value , pred(j) := i , insert(j, H) ;
e l s e dist(j) := value , pred(j) := i , decreaseKey(value, i, H) ;
end ;
end ;
end ;
Kapitel 5
Beispielerzeugung
Um möglichst viele verschiedene Probleme generieren zu können, wurden von uns
fünf verschiedene Beispielerzeugungsprogramme entwickelt. Diese haben wir unter einer einzigen Benutzeroberfläche zusammengeführt. Bei fünf verschiedenen
Programmen ist es dem Benutzer natürlich auch möglich eine Vielzahl von verschiedenen Parametern zu wählen.
Um, auf Wunsch, Probleme ohne negative Zyklen, aber mit negativen Kosten zu
erzeugen, benutzen wir Knoten-Potentiale. Das Potential eines Knotens i ist dabei
eine ganze Zahl (integer) P(i) die zufällig aus dem gegebenen Intervall gewählt
wird. Wenn eines der Programme nun ein solches Beispiel erzeugt, werden die
Kosten cij eines Pfeils hi, ji folgendermaßen modifiziert: c0ij = cij + P (i) − P (j)
Um also ein Beispiel ohne negative Zyklen und mit negativen Kosten zu erzeugen muss also nun für die Pfeilkosten ein rein positives Intervall gewählt werden.
Für die Potentiale muss allerdings mindestens der Intervallanfang negativ sein.
Auf Wunsch ist es auch möglich, dass den Beispielen ein künstlicher Knoten hinzufügt wird. Von diesem führt zu jedem anderen Knoten des Graphen ein Pfeil
und ist somit eine künstliche Quelle. Ein Intervall, aus dem die Kosten dieser
Pfeile zufällig gewählt werden, kann ebenfalls angegeben werden. Die erzeugten
Knoten werden standardmäßig vom Programm mit natürlichen Zahlen, angefangen bei 1, durchnummeriert. Wenn wir im Folgenden von zufällig erzeugten
Pfeilen sprechen, gehen wir davon aus, dass sowohl Anfangs- als auch Endknoten
aus der Menge N = {1, ..., n} der Knoten zufällig gewählt wurden. Dabei werden Schlingen nicht erlaubt. Bei der Erklärung der einzelnen Programme wollen
wir die Ermittlung der Pfeilkosten vernachlässigen. Es sei hier erwähnt, dass die
Kosten aller Pfeile immer aus einem, vom Benutzer gewählten Intervall zufällig
ermittelt werden.
Die Beispielerzeuger Sprand, Spacyc und Spgrid basieren ganz wesentlich auf der
Forschungsarbeit ,,Shortest Paths Algorithms: Theory and experimental Evaluation“, Teile wurden auch aus C-Programmen der Bibliothek splib in VBA
übersetzt, siehe [CGR94].
26
KAPITEL 5. BEISPIELERZEUGUNG
5.1
27
Sprand
2
3
1
4
6
6
5
Abbildung 5.1: Beispielgraph Sprand mit Zyklenlänge 4
Sprand steht für ,,shortest path random“ (,,kürzeste Wege zufällig“). Dieses Programm erzeugt einen stark verbundenen Zufallsgraphen. Die starke Verbundenheit wird durch die Generierung eines oder mehrerer Zyklen erreicht. Pfeile, die
nicht zu den Zyklen gehören, werden mit zufälligen Anfangs- und Endknoten erstellt. Um die Zyklen erzeugen zu können, muss die Zahl der Knoten ausreichend
sein. Enthält das Problem z.B. genau einen Zyklus muss die Zahl der Pfeile mindestens denen der Knoten entsprechen. Die Quelle ist normalerweise der Knoten
1, außer der Graph enthält einen künstliche Knoten (s.o.). Die Zahl der Pfeile k
aus denen die Zyklen bestehen, kann vom Benutzer gewählt werden. Hierbei muss
natürlich 1 < k ≤ n sein. Vom Knoten 1 ausgehend verbindet Sprand der Reihenfolge nach die nächsten Knoten mit k-1 Pfeilen miteinander. Der letzte Pfeil k
verbindet dann den letzten Knoten wieder mit dem Anfangsknoten 1. Der nächste
Zyklus beginnt wieder beim Knoten 1, der mit dem nächsten, noch nicht verbundenen Knoten verknüpft wird. Sind nach einem oder mehreren Durchläufen nur
noch weniger als k nichtverbundene Knoten übrig, so wird ein Zyklus geringerer
Länge als k mit den restlichen Knoten erzeugt. Die restlichen Pfeile, werden nun
zufällig gesetzt. Der Benutzer kann sowohl für die zufälligen Pfeile, als auch für
die Pfeile der Zyklen, ein jeweils unterschiedliches Intervall für die Kostenbewertungen angeben. Bei ausschließlich negativen Bewertungen für die Zyklenpfeile
enthält das resultierende Beispiel negative Zyklen.
5.2
Spacyc
Spacyc steht für ,,shortest path acyclic“ (,,kürzeste Wege azyklisch“). Dieses Programm erzeugt einen azyklischen Zufallsgraphen. Hierbei kann vom Quellknoten 1
jeder andere Knoten des Graphen erreicht werden. Um dies sicherzustellen, generieren wir einen oder mehrere verbundene Wege. Die Länge k kann vom Benutzer
gewählt werden, wobei natürlich 1 ≤ k < n sein muss. Ausgehend vom Knoten
1 werden die Nachfolgenden k Knoten mittels k Pfeilen miteinander verbunden.
KAPITEL 5. BEISPIELERZEUGUNG
28
2
3
1
4
6
6
5
Abbildung 5.2: Beispielgraph Spacyc mit Weglänge 3
Nach dem Ende eines Weges wird der nächste Knoten immer vom Anfangsknoten
aus verbunden. Danach, wie oben beschrieben, die Nachfolger. Sind nach einem
oder mehreren Durchläufen nur noch weniger als k Knoten verblieben, so wird
nur noch ein Weg generiert, der bis zum letzten Knoten führt. Dort endet dieser Weg. Es wird also vom letzten Knoten kein weiterer Pfeil mehr erzeugt. Die
restlichen Pfeile werden dann zufällig generiert, wobei jedoch der Knoten mit der
niedrigeren Nummer immer den Anfangs- und der mit der höheren stets den Endknoten darstellt. So wird sichergestellt, dass kein Zyklus entsteht. Der Benutzer
kann sowohl für die Wegpfeile, also auch für die Zufallspfeile jeweils ein Intervall
für die Kostenbewertungen angeben. Der wichtigste Unterschied zu Sprand ist
zusammenfassend, dass keine Zyklen sondern nur Wege erzeugt werden.
5.3
Spgrid
1
4
1
4
2
5
2
3
6
3
1
4
5
2
5
6
3
6
7
7
7
Abbildung 5.3: Beispielgraph Spgrid
Spgrid steht für ,,shortest path grid“ (,,kürzeste Wege Gitter“). Dieses Programm
erzeugt einen Gittergraphen. Im einfachsten Fall bilden alle Pfeile die Gitterstruktur. Allerdings können auch kompliziertere Graphen durch zusätzliche Pfeile generiert werden. Hierbei werden nicht die Pfeil- und Knotenanzahl, sondern Breite
und Höhe des Gitters angegeben. Die Quelle bildet der Knoten x · y + 1, außer
ein künstlicher Startknoten wurde erzeugt. Pfeile von der Quelle gehen zu allen
KAPITEL 5. BEISPIELERZEUGUNG
29
Knoten der ersten vertikalen Linie (,,Ebene“). Es gibt drei Möglichkeiten, wie
die Ebenen mittels Pfeilen verbunden werden: durch einfache Zyklen, doppelt
verkettete Zyklen oder einfachen Wegen. Bei einfachen Zyklen werden y Pfeile
verwendet. Die einzelnen Knoten werden jeweils mit ihrem Nachfolger verbunden und der letzte Knoten wieder mit dem ersten. Also für die Knoten 1, ..., y
h1, 2i, h2, 3i, ..., hy, 1i. Bei den doppelt verketteten Zyklen werden 2 · y Pfeile verwendet. Hierbei werden die einzelnen Knoten mit dem Nachfolger und Vorgänger
verbunden. Also h1, 2i, h2, 1i, h2, 3i, h3, 2i, ..., hy, 1i, h1, yi. Bei den einfachen Wegen werden y − 1 Pfeile verwendet. Jeder Knoten wird jeweils mit seinem Nachfolger verbunden, außer der letzte Knoten y. Also h1, 2i, h2, 3i, ..., hy − 1, yi. Die
Kosten der einzelnen Pfeile werden dabei zufällig aus dem vorgegebenen Intervall
gewählt. Das Problem kann auf Wunsch auch noch weitere Pfeile besitzen. Diese
werden dann mit zufälligen Anfangs- und Endknoten in den Graphen eingefügt
und mit zufälligen Kosten aus dem entsprechenden Intervall versehen.
5.4
Spadja
Spadja steht für ,,shortest path adjacency(matrix)“ (,,kürzeste Wege Adjazenz(matrix)“). Dieses Programm erzeugt einen Zufallsgraphen aus einer Adjazenzmatrix. Durch diese Generierung wird sichergestellt, dass im Gegensatz zu den
obigen Beispielerzeugern, keine parallele Pfeile entstehen können. Der Graph ist
komplett zufällig erzeugt und unterliegt keinem Muster.
5.5
Spsym
Spsym steht für ,,shortest path symmetrical“ (,,kürzeste Wege symmetrisch“).
Dieses Programm erzeugt einen symmetrischen oder ungerichteten Graphen. Auch
dieser Algorithmus erzeugt den Graphen aus einer Adjazenzmatrix. Es können
also auch hier keine parallelen Pfeile entstehen. Bei diesem Erzeuger wird jedoch
zu jedem zufällig generierten Pfeil auch immer direkt der Pfeil in die entgegengesetzte Richtung erzeugt. Dabei kann vom Benutzer gewählt werden ob beide
Pfeile die gleichen Kosten haben sollen oder ob diese für jeden einzeln zufällig
ermittelt werden.
Kapitel 6
Vergleichende Analyse
Die Analysen in diesem Kapitel wurden auf Grund von etwa 60 (großen) Beispieldateien erstellt. Sie befinden sich auf der beigefügten DVD-Rom im Ordner ,,examples“. Alle Messergebnisse befinden sich im Anhang B und auf der DVD als
Excel-Datei im jeweiligen Ordner.
Nach den Erfahrungen mit den Zeitmessungen im Datenstrukturtest (Kapitel 3),
wurde diesmal jedes Beispiel dreimal mit dem Optimale-Wege-Assistenten gelöst
und analysiert, um Messfehler zu vermeiden. Die Zeitangaben in den Abbildungen sind das arithmetische Mittel über die drei Messungen in logarithmischer
Skala.
Die Lösung großer Beispiele sollte nach Vorgabe etwa 15 Minuten Rechenzeit
benötigen. Die Größe muss daher relativ zum Rechner beurteilt werden. Unsere
Testplatform war ein moderner Rechner der Gigahertz-Klasse1 . Er löst kleinere
Sprand-Beispiele (2.048 Knoten, 8.192 Knoten) mit dem Fifo-Verfahren innerhalb
einer Sekunde und sehr große (1.048.576 Knoten, 4.194.304 Pfeile) vor Ablauf einer Viertelstunde (siehe Kapitel 6.2). Ein Referenzrechner im universitätseigenen
IT-Pool benötigt für die Lösung des kleineren Beispiels gut 3 Sekunden, ebenfalls
mit dem Fifo-Verfahren. Er fällt auch bei größeren um den Faktor 3-4 zurück.
Es wurden Berechnungen, deren Lösung auf unserem Testsystem deutlich über
15min (bzw 1.000.000ms) dauerte, bis auf wenige Ausnahmen abgebrochen. Das
betraf in den meisten Fällen die klassische Implementation des Dijkstra-Algorithmus mit Liste, dessen geringe Performance schon im Vorfeld zu erwarten war.
Das PSP-Verfahren wurde nicht untersucht, da die von uns implementierte Version die beiden Listen als Queue ausführt, so dass das Verfahren im Grunde nur
eine Variante des Fifo-Algorithmus ist (siehe 4.1.4).
1
Pentium IV, 2,66 Ghz, 768 MB Arbeitsspeicher, Windows XP SP1, Office 2000 SP3
30
KAPITEL 6. VERGLEICHENDE ANALYSE
6.1
31
Implementationen des Fifo
Das Fifo-Verfahren (siehe Kapitel 4.1.1) nach Bellman überprüft nicht, ob ein
neu einzubringendes Element bereits in der Datenstruktur Q vorhanden ist. Dadurch muss ein Knoten unnötig oft auf Verkürzung untersucht werden. Wird Q
zur Suche nach diesem Element durchlaufen, so steigert sich die Komplexität des
Verfahrens auf O(n2 m). Diese Version verhindert dafür den Aufwand der mehrfachen Untersuchung.
Alternativ kann zusätzlich zur eigentlichen Schlange noch eine weitere Datenstruktur benutzt werden, um den Mehraufwand zu verhindern. Diese gibt dann
an, ob ein Element bereits vorhanden ist oder nicht. Das erhöht die ursprüngliche
Abschätzung von O(mn) nicht. Allerdings erhöht sich dadurch der Speicherbedarf um ein Feld der Größe n-Bit für n Wahrheitsvariablen.
Diese drei Versionen des Fifo-Verfahrens wurden in diesem Test anhand von zwei
geeigneten Beispielgraphen analysiert. Es handelt sich um unterschiedlich dichte
Graphen der Sprand-Familie jeweils mit 262.144 Pfeilen (zur Struktur siehe Kapitel 6.2 und 5.1).
Verfahren
Zeit (s) Durchläufe
Hinzufügen ohne Prüfung
51
1.172.546
Vollständiges Durchsuchen
2920
1.087.553
Weitere Datenstruktur
44
1.087.553
Tabelle 6.1: Ergebnisse bei einem spärlichen Graphen
Im spärlichen Graphen (Sprand-Sparse) mit 65.536 Knoten fällt die zweite
Version mit Durchsuchen katastrophal hinter den anderen zurück. Die hohe Anzahl der Knoten erfordert einen großen Aufwand. Da Knoten im Mittel wenige
Nachfolger haben, schlägt sich die Version nach Bellman gut. Die Anzahl der
zusätzlichen Durchläufe ist relativ gering (Zuwachs von circa 8%). Am schnellsten ist jedoch die Version mit weiterer Datenstruktur.
Verfahren
Zeit (s) Durchläufe
Hinzufügen ohne Prüfung
30
14.994
Vollständiges Durchsuchen
13
6.912
Weitere Datenstruktur
12
6.912
Tabelle 6.2: Ergebnisse bei einem dichten Graphen
Die Ergebnisse beim dichten Graphen (1.024 Knoten) liegen enger beieinander.
Schlusslicht ist dieses Mal der ursprüngliche Algorithmus nach Bellman. Grund
KAPITEL 6. VERGLEICHENDE ANALYSE
32
hierfür ist die gestiegene Anzahl der Nachfolger jedes Knotens. Das hat zur Folge, dass ein und der selbe Knoten sehr oft auf Verkürzung überprüft werden
muss (mehr als doppelt so viele Durchläufe wie die anderen Versionen). Das
vollständige Durchsuchen erfordert, wegen der geringen Knotenanzahl, einen geringen Aufwand. Wie zuvor hat die Version mit zusätzlicher Datenstruktur die
beste Laufzeit.
Diese Ergebnisse sprechen deutlich für eine Implementation mit zusätzlichen Datenstruktur. Das gleiche gilt für die beiden Fifo-Varianten Dequeue und 2-Queue.
In deb folgenden Kapiteln werden die anderen Versionen nicht berücksichtigt.
6.2
Zyklische Graphen
In diesem ersten Test wurden Graphen untersucht, die mit Sprand (siehe Kapitel
5.1) erzeugt wurden. In Anlehnung an [CGR94] haben wir Graphen generiert, deren Zykluslänge n beträgt und deren Pfeilkosten im Zyklus auf 1 gesetzt wurden.
Die Kosten der anderen Pfeile sind Zufallszahlen aus dem Intervall [0; 10.000].
Graphen die diesem Schema genügen liefern wesentlich interessantere Ergebnisse
als völlig zufällig erzeugte.
Es wurden drei Testserien durchgeführt. Eine Serie mit dichten (Abbildung 6.1)
und eine mit spärlich besetzten Graphen (Abbildung 6.2). Die letztgenannten
haben viermal so viele Pfeile wie Knoten (m = 4 · n), während die zuerst genannten deutlich mehr Pfeile aufweisen (m = 41 · n2 ). Es wurden in beiden Fällen die
Anzahl der Knoten sukzessive verdoppelt, bis zu einer Obergrenze von etwa 4
Millionen Pfeilen.
Im Anhang B finden sich nicht nur die beiden eben benannten Testserien (Tabellen B.1, B.2), sondern noch Ergebnisse zu einer weiteren Testserie aus der
Sprand-Familie (Tabelle B.3). Diese Testserie unterscheidet sich von den anderen
beiden nur durch ihre Dichte (m = 16 · n) und ergibt keine neuen Erkenntnisse
im Vergleich mit den anderen beiden. Sie verhält sich wie der spärlich besetzte
Graph. Erst im Vergleich mit den azyklischen Graphen des nächsten Kapitels,
die die gleiche Dichte aufweisen, lassen sich interessante Vergleiche anstellen.
Aus der Testserie der dichten Graphen (Sprand-Dense) gehen die auf dem
Dijkstra-Algorithmus basierenden Implementationen als klarer Sieger hervor, siehe dazu Abbildung 6.1 auf Seite 33. Es kann keiner von ihnen besonders hervorgehoben werden. Selbst beim größten Graphen (4.194.304 Pfeile) unterscheidet
sich die schnellste Implementierung (Dial) von der langsamsten (Dijkstra) nur
um 2 Sekunden, bei einer Laufzeit von etwa einer halben Minute.
Deutlich abgeschlagen sind die Verfahren Fifo, Dequeue und 2-Queue. Sie sind im
KAPITEL 6. VERGLEICHENDE ANALYSE
33
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra
1.000
Dijkstra (Heap)
Dial
100
10
256
(16384)
512
(65536)
1024
(262144)
2048
(1048576)
4096
(4194304)
Knoten (Pfeile)
Abbildung 6.1: Laufzeiten der Sprand-Dense Beispiele
schlimmsten Fall um den Faktor 10 und im besten um den Faktor 5 langsamer
als die Dijkstra-Familie. Es zeigt sich, dass jeweils der Fifo eine etwas bessere
Performance bietet, obwohl alle seine Varianten relativ eng beieinander liegen.
Sie unterscheiden sich aber deutlicher als etwa die Dijkstra-Familie. Die maximale Abweichung beträgt hier 16 Sekunden bei einer Gesamtlaufzeit von etwa 4
Minuten.
Der langsamste Algorithmus im Testfeld ist der Schwellenalgorithmus. Dieses Verhalten ließ sich auch nicht durch eine geschickte Wahl des Parameters verbessern.
Er liegt bei allen Graphen noch deutlich hinter jeder Fifo-Variante zurück. Beim
größten Graphen hat er die 13-fache Laufzeit des Verfahrens nach Dial und die
anderthalbfache Laufzeit des Fifo-Verfahrens.
Im spärlichen Graphen (Sprand-Sparse) ändern sich die Ergebnisse ein wenig,
siehe Abbildung 6.2 auf Seite 34. Es dominiert zwar noch immer die DijkstraFamilie, aber ihre klassische Implementation wird bei größeren Graphen (ab
32.768 Pfeile) katastrophal langsam. Die Messungen für Graphen über 524.288
Pfeile mussten sogar abgebrochen werden, da diese Implementation zuvor schon
fast eine Dreiviertelstunde zur Lösung benötigte. Die Variante nach Dial brauchte
für den selben Graphen keine 7 Sekunden. Das gilt nicht nur hier, sondern bei
jedem untersuchten Sparse-Graphen ist das Verfahren nach Dial das schnellste
im Test. Die zweitplatzierte Implementation mit Heap ist etwa um den Faktor 2
langsamer.
KAPITEL 6. VERGLEICHENDE ANALYSE
34
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra
1.000
Dijkstra (Heap)
Dial
100
10
1024
(4096)
4096
(16384)
16384
(65536)
65536
(262144)
262144
(1048576)
1048576
(4194304)
Knoten (Pfeile)
Abbildung 6.2: Laufzeiten der Sprand-Sparse Beispiele
Die Fifo-Varianten liegen diesmal weniger eng zusammen. Der Fifo löst jetzt jeden
Graphen spürbar schneller als die verwandten Verfahren Dequeue und 2-Queue.
Die Überlegenheit des Fifo zeigt sich auch in der Anzahl der Schleifendurchläufe.
Er durchläuft seine Schleife um ein drittel weniger als beispielsweise Dequeue. Das
Verfahren 2-Queue hat wiederum immer weniger Durchläufe als Dequeue, obwohl
es teilweise eine längere Gesamtlaufzeit hat. Der Grund hierfür muss der größere
Overhead sein, der notwendig ist, um zwei Schlangen zu verwalten. Die Datenstruktur selbst kann nicht der Grund für den Geschwindigkeitsnachteil sein. Wie
im Kapitel 3 zu den Datenstrukturen dargelegt, basieren sowohl Queue und Dequeue bzw. Stack auf dem gleichen Collection-Objekt. Schenkt man dem dünnen
Vorsprung der Queue auf den Stack im Datenstrukturtest glauben, so müsste
das Verhältnis bei nur einer Datenstruktur sogar umgekehrt sein (siehe Tabellen
A.5 und A.2). Trotz der angesprochenen Unterschiede, ist das Gesamtverhalten
von Dequeue und 2-Queue recht ähnlich, so dass sie in Abbildung 6.2 kaum zu
unterscheiden sind.
Völlig anders als bei der vorherigen Testserie präsentiert sich der Schwellenalgorithmus: Er setzt sich deutlich von den anderen SPT-Verfahren ab und deklassiert
so den Fifo mit fast doppelter Geschwindigkeit. Erst beim letzten Testgraphen
mit 4194304 Pfeile bricht der Schwellenalgorithmus ein. Der Grund für dieses
merkwürdige Verhalten ist unklar, hier könnte ein Test mit verschiedenen Parametern Klarheit bringen.
In Abbildung 6.2 sind einige Schwankungen zu erkennen. Sie sind besonders deut-
KAPITEL 6. VERGLEICHENDE ANALYSE
35
lich bei den SPT-Verfahren, da sie aber bei allen Verfahren –wenn auch weniger
ausgeprägt– auftreten, dürfte ein von den anderen abweichender Graph bei der
Zufallserzeugung entstanden sein.
Insgesamt zeigt sich, dass die Algorithmen mit dem dichten Graphen – bei gleicher
Pfeilanzahl – natürlich wesentlich besser arbeiten können, als mit der spärlichen
Variante. Der dichte Graph ist im Vergleich erheblich kleiner. Diese triviale Beobachtung wird auch dadurch gestützt, dass im spärlichen Graphen wesentlich
mehr Durchläufe zur Lösung erforderlich sind (siehe Tabelle B.2). Obwohl hier
ein Durchlauf kürzer dauert, da ein Knoten im Mittel deutlich weniger Nachfolger hat, ist dennoch die Gesamtdauer, durch die höhere Anzahl der Durchläufe,
länger.
6.3
Azyklische Graphen
Zur Untersuchung der azyklischen Graphen wurden zwei Testserien durchgeführt
(Tabellen B.4 und B.5). Die dafür erzeugten Beispiele wurden mit Spacyc generiert (siehe Kapitel 5.2). Ähnliche Tests finden sich auch in der Arbeit von
Cherkassky et al. wieder [CGR94].
Die erste Serie enthält ausschließlich Graphen mit nicht-negativ bewerteten Pfeilen, die zweite nur Pfeile mit nicht-positiven Kostenbewertungen. Die zweite Testserie ist nur möglich, weil nach Konstruktion keine negativen Zyklen auftreten.
Trotz der sichergestellten Lösbarkeit, können nicht alle Verfahren angewandt werden, sowohl der Schwellenalgorithmus als auch die Dijkstra-Familie lösen diesen
Aufgabentyp nicht sinnvoll. Nähere Informationen dazu finden sich im Kapitel 4
über die Verfahren.
Ähnlich wie im Test der zyklischen Graphen (Kapitel 6.2) wird eine geeignete
Struktur gewählt: Die Graphen haben eine feste Dichte von 16 (m = 16 · n), die
Anzahl der Knoten und Pfeile wird im Test sukzessive verdoppelt. Die Weglänge
beträgt jeweils n − 1, so dass alle Knoten durch einen einzigen Weg verbunden
sind. Die Pfeilbewertungen auf diesem Weg sind im Fall des ersten Tests jeweils 1
und im zweiten -1. Die restlichen Pfeile wurde mit Zufallszahlen aus dem Intervall
[0; 10.000] bzw. [-10.000;0] bewertet.
Azyklische Graphen können mit speziellen Algorithmen mit linearem Aufwand
gelöst werden, siehe [NM04][Algorithmus 2.4.6]. Das wurden im Rahmen dieses
Projektseminars nicht weiter untersucht.
Aus der Testserie der positiven azyklischen Graphen (Acyc-Pos), an der noch
alle Algorithmen teilnahmen, gehen wieder die Dijkstra-Varianten Dial und Heap
als Sieger hervor (Abbildung 6.3 auf Seite 36). Die Implementation von Dial geht
dabei jedes Mal deutlich schneller zu Werke als der Heap. Beim größten Graphen
(2.097.152 Pfeilen) ist Dial bereits nach 67% der Zeit fertig. Beide Verfahren
KAPITEL 6. VERGLEICHENDE ANALYSE
36
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra
1.000
Dijkstra (Heap)
Dial
100
10
2048
(32768)
4096
(65536)
8192
(131072)
16384
(262144)
32768
(524288)
65536
(1048576)
131072
(2097152)
Knoten (Pfeile)
Abbildung 6.3: Laufzeiten der Acyc-Pos Beispiele
benötigen in diesem Test etwa die gleiche Zeit wie schon zur Lösung gleichgroßer
Sprand-Probleme (Tabelle B.3). Der klassische Dijkstra mit Liste fällt ebenfalls
wie zuvor bei großen Graphen (hier ab 262.144 Pfeilen) deutlichen zurück.
Die schnellste Fifo-Variante bei allen Testgraphen in dieser Serie ist die klassische
Implementation nach Bellman und Ford. Sie kann sich erkennbar von Dequeue
und 2-Queue absetzen. Die beiden letztgenannten sind sich sehr ähnlich, auch
wenn Dequeue stets mehr Durchläufe hat und somit langsamer als 2-Queue ist.
Beim Vergleich mit dem entsprechenden Sprand-Beispiel (Tabelle B.3) fällt auf,
dass alle SPT-Verfahren im zyklenfreien Graphen erheblich weniger Schleifendurchläufe benötigen und somit auch schneller sind. Vor allem Dequeue und 2Queue profitieren durch diese spezielle Struktur. Auf diese Weise fällt der Abstand
auf die Dijkstra-Familie etwas geringer aus, weil diese – wie bereits festgestelltnahezu identische Zeiten liefern. Der Schwellenalgorithmus liegt wiederum vor den
anderen SPT-Verfahren und profitiert auch durch die Zyklenfreiheit. Im größten
Graphen ist er doppelt so schnell wie der Fifo-Algorithmus.
Das Testfeld ist bei den negativen azyklischen Graphen (Acyc-Neg) auf die
klassischen SPT-Verfahren zusammengeschrumpft. Darüberhinaus tun sich die
drei verbliebenen Verfahren mit dieser Problemstruktur so schwer, dass viele
Testdurchläufe auf Grund ihrer hohen Laufzeit abgebrochen wurden (Abbildung
6.4 auf Seite 37). Schon die Lösung des einfachsten Beispiels (32.768 Pfeile) mit
dem Fifo-Algorithmus dauert statt gut 2 Sekunden zuvor jetzt geschlagene 48
KAPITEL 6. VERGLEICHENDE ANALYSE
37
1.000.000
Millisekunden
100.000
10.000
Fifo
Dequeue
2-Queue
1.000
100
10
2048
(32768)
4096
(65536)
8192
(131072)
16384
(262144)
Knoten (Pfeile)
Abbildung 6.4: Laufzeiten der Acyc-Neg Beispiele
Sekunden. Da aber die anderen Verfahren noch drastischer einbrechen, ist der
Fifo noch die beste Wahl. Sein Vorteil besteht wieder einmal in der erheblich
geringeren Anzahl der Schleifendurchläufe. Beim bereits angesprochenen kleinsten Graphen hat der Fifo nur ein achtel der Iterationen des Dequeue-Verfahrens.
Gerade bei Dequeue wird auch die riesige Diskrepanz zur vorherigen Testserie
deutlich: Diese Variante löste den Graphen mit 65.536 Pfeilen und positiven Kosten noch in 5 Sekunden, bei negativen Kosten dauert die Lösung aber bereits
eine Viertelstunde! Die Bearbeitung des nächstgrößeren Graphen wurde nach einer Stunde abgebrochen.
6.4
Gittergraphen
Gittergraphen unterscheiden sich von den zuvor betrachteten Typen, weil ihre
Struktur viel starrer und weniger vom Zufall beeinflussbar ist. Es wurde auch
darauf verzichtet die Graphen durch zusätzliche Pfeile aufzulockern. Eine relativ
hohe Pfeilanzahl wird jedoch dadurch sichergestellt, dass die einzelnen Ebenen
durch doppelt verkettete Zyklen verbunden sind.
Es werden drei Ausprägungen untersucht, zum einem quadratische (Grid-Square,
Tabelle B.6), breite (Grid-Wide, Tabelle B.7) und lange Gittergraphen (GridLong, Tabelle B.8). In der Grid-Square Serie wurden Höhe (Y ) und Breite (X)
des Graphen identisch gewählt (X = Y ) und jeweils nach jedem Test verdoppelt,
KAPITEL 6. VERGLEICHENDE ANALYSE
38
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra
1.000
Dijkstra (Heap)
Dial
100
10
4097
(12288)
16385
(49152)
65537
(196608)
262145
(786432)
1048577
(3145728)
Knoten (Pfeile)
Abbildung 6.5: Laufzeiten der Grid-Square Beispiele
so dass sich die Gesamtanzahl der Knoten vervierfacht (siehe dazu Kapitel 5.3).
Die Graphen der zweiten Serie haben eine konstante Breite von X = 16 Knoten,
aber eine variable Höhe Y , die schrittweise verdoppelt wird. Die dritte Serie entspricht der vorangegangenen mit umgekehrten Seitenverhältnis: Es wird also die
Höhe auf Y = 16 Knoten festgesetzt und die Breite X schrittweise verdoppelt.
Diese Konstruktionen führen zu einer Verdopplung der Gesamtknotenanzahl nach
jedem Testschritt. Die zufällige Bewertung der Pfeile ist in allen drei Fällen aus
dem Intervall [0;10.000]. Die drei Tests, sowie weitergehende Erläuterungen finden sich in dem bereits mehrfach zitierten Papier von Cherkassky wieder [CGR94].
Im Test der quadratischen Gittergraphen (Grid-Square) fallen zwei Algorithmen deutlich gegenüber dem Hauptfeld zurück: zum wiederholten Male ist die
klassische Implementation des Dijkstra-Verfahrens das langsamste Verfahren im
Testfeld (Abbildung 6.5). Anders als bei den vorangegangenen Test ist es diesmal
auch bei den kleinen Graphen völlig abgeschlagen. Beim kleinsten Graphen mit
12.288 Pfeile ist es um den Faktor 20 langsamer als das schnellste Verfahren, beim
mittleren Graphen (196.608 Pfeile) bereits um den Faktor 260. Die größeren Beispiele mit dem Dijkstra-Verfahren zu untersuchen scheiterte am Zeitlimit. Der
Fifo überschreitet in diesem Test ebenfalls das Zeitlimit, allerdings erst beim
größten Graphen mit 3.145.728 Pfeilen. Es dauert über eine halbe Stunde bis
zur Lösung dieses Problems, während das Verfahren nach Dial bereits nach einer halben Minute fertig ist. Dieser Vergleich deutet schon an, dass auch bei
KAPITEL 6. VERGLEICHENDE ANALYSE
39
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra
1.000
Dijkstra (Heap)
Dial
100
10
8193
(24576)
16385
(49152)
32769
(98304)
65537
(196608)
131073
(393216)
262145
(786432)
524289
(1572864)
Knoten (Pfeile)
Abbildung 6.6: Laufzeiten der Grid-Wide Beispiele
den Gittergraphen Dial’s Implementation zu den schnellsten Verfahren gehört.
Es liegt aber nicht immer an der Spitze. Bei kleinen Graphen (bis 196.608 Pfeile)
schlägt sich Dequeue besser. Allerdings liegt hier das restliche Testfeld so eng
beieinander, dass die Unterschiede in den Rangfolge unbedeutend sind. Es fällt
auf, dass die Heap-Variante des Dijkstra-Verfahrens hinter Dequeue, 2-Queue und
dem Schwellenalgorithmus zurückfällt. In allen Tests zuvor lag sie nur knapp hinter der Variante von Dial zurück. Dequeue und 2-Queue durchlaufen exakt die
gleiche Anzahl der Schleifen. Weitere Untersuchungen mit aktivierter Protokollierung an kleinen Gittergraphen durch den Optimale-Wege-Assistenten haben
gezeigt, dass beide Verfahren bei Gittergraphen gleich vorgehen. Das Verhalten
lässt sich daher auch bei den Ausprägungen Wide und Long beobachten. Wie
sich schon bei den spärlichen Graphen mit Zyklen (Kapitel 6.2) zeigte, ist der
Verwaltungsoverhead für die 2 Schlangen bei 2-Queue offensichtlich größer als
bei Dequeue und verlangsamt auf diese Weise das Verfahren.
Der Schwellenalgorithmus liegt in der Messung nur im Mittelfeld und verdient so
nur wenig Beachtung, aber die geringe Anzahl der Schleifendurchläufe fällt auf.
In den vorherigen Testserien lag die Anzahl meist leicht unter denen der anderen
SPT-Verfahren und nur selten wenig darüber. Im Falle des größten Gittergraphen
(3.145.728 Pfeile) stehen 3 Schleifendurchläufe des Schwellenalgorithmus den 62
Millionen Durchläufen des Fifo gegenüber. Das ist allerdings wegen der anderen
Architektur des Verfahrens kein sinnvolles Vergleichskriterium.
KAPITEL 6. VERGLEICHENDE ANALYSE
40
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra
1.000
Dijkstra (Heap)
Dial
100
10
8193
(24576)
16385
(49152)
32769
(98304)
65537
(196608)
131073
(393216)
262145
(786432)
524289
(1572864)
Knoten (Pfeile)
Abbildung 6.7: Laufzeiten der Grid-Long Beispiele
Die breiten Gittergraphen (Grid-Wide) stellen für die getesteten Verfahren
keine große Schwierigkeit dar (Abbildung 6.6 auf Seite 39). Fast alle Verfahren lösen dieses Problem in passabler Zeit und liegen dicht beieinander. Die
klassische Variante des Verfahrens nach Dijkstra ist nicht konkurrenzfähig und
überschreitet bei Graphen mit mehr als 196.608 Pfeilen das Zeitlimit. Die Variante
Heap und der Fifo-Algorithmus sind die langsamsten Verfahren des Hauptfeldes.
Bei größeren Graphen (ab 393.216 Pfeilen) fällt der Heap immer noch weiter
hinter den Fifo zurück. Obwohl der Fifo zu den schlechteren Verfahren in dieser
Testserie gehört, schlägt er sich deutlich besser als in der Serie davor. Er bleibt
konstant um den Faktor 2 langsamer als das schnellste Verfahren, einmal mehr
die Version von Dial. Diesem verhilft die Bucket-Implementation zu erheblich
besseren Gesamtlaufzeiten als dem verwandten Verfahren Heap. Daher ist Dial’s
Variante bei jedem breiten Graphen das schnellste Verfahren im Test, dicht gefolgt von Dequeue. 2-Queue und der Schwellenalgorithmus sind etwas langsamer.
Die langen Gittergraphen (Grid-Long) sind für die Algorithmen eine größere
Herausforderung als die breiten der vorherigen Testserie (Abbildung 6.7). Wie
schon bei quadratischen Gittergraphen vermutet, stellt sich der Fifo als das untauglichste Verfahren auf Gittergraphen heraus, gefolgt vom klassischen Dijkstra.
Beide absolvierten auf Grund des gesetzten Zeitlimits nicht alle Tests. Die letzten
beiden Durchläufe wurden nach einer Stunde abgebrochen.
Die Sieger dieser Testserie sind die Label-Correcting-Verfahren Dequeue und 2-
KAPITEL 6. VERGLEICHENDE ANALYSE
41
1.000.000
100.000
Millisekunden
Fifo
Dequeue
10.000
2-Queue
Thresh
Dijkstra (Heap)
1.000
Dial
100
10
[0;10]
[0;100]
[0;1000]
[0;10000]
[0;100000]
[0;1000000]
[0;10000000]
Knoten (Pfeile)
Abbildung 6.8: Laufzeiten der Grid-Cost Beispiele
Queue. Beide weisen weiterhin gleiche Iterationen auf und 2-Queue liegt nur geringfügig zurück. Nur wenig dahinter befindet sich die Dijkstra-Implementation
mit Heap. Sie ist um den Faktor 1,2 langsamer als Dequeue. Überraschend schlecht
schneidet die Version nach Dial ab, die sonst immer zu den schnellsten Verfahren
zählte. Sie braucht zur Lösung des Problems doppelt so lange wie die Variante
mit Heap. Selbst der Schwellenalgorithmus ist bis zum vorletzten Graphen weit
schneller als der Dial. Erst bei den letzten beiden Graphen bricht er ein. Ein
Verhalten, das schon beim letzten Graphen der Sprand-Sparse Serie beobachtet
werden konnte.
Bei diese drei Testserien bleibt vor allem das schlechte Abschneiden des FifoVerfahrens festzuhalten. Sobald der Graph eine hohe Tiefe aufweist schlägt sich
das massiv auf die Laufzeit nieder. Es liegt offensichtlich an der großen Anzahl
von Wegen zu jedem Knoten, die stur abgearbeitet werden.
6.5
Steigende Kosten
In diesem Test wurden bis auf die Kosten identische Graphen verglichen. Als
Graphentyp wurde der breite Gittergraph (mit 196.608 Pfeilen) aus der Testserie
zuvor gewählt, weil er eine nicht vom Zufall abhängige Struktur hat und ihn alle
Verfahren etwa gleich gut bearbeiten konnten.
KAPITEL 6. VERGLEICHENDE ANALYSE
42
Die Verfahren reagieren unbeeindruckt auf die Veränderungen der Pfeilkosten
wie Abbildung 6.8 auf Seite 41 veranschaulicht. Ausnahme ist die Implementation nach Dial, die erwartungsgemäß bei sehr großen Kosten Probleme zeigt. Das
kritische Verhalten beginnt bei Kostenintervallen der Größenordnung von einer
Million. Der Grund hierfür ist bereits in Kapitel 4.2.2 angesprochen worden: der
Speicherbedarf wächst in Abhängigkeit der maximalen Kosten (C).
6.6
Unlösbare Graphen
Die einzige Möglichkeit ein unlösbares Problem zu erzeugen besteht darin einen
Graphen mit mindestens einem negativen Zyklus zu generieren. Alle anderen Probleme sind lösbar. Daher sind negative Kostenbewertungen erforderlich, was die
Dijkstra-Familie und den Schwellenalgorithmus ausnimmt.
Ein solcher Graph kann leicht erzwungen werden, in dem etwa Sprand oder Spgrid
(mit Option Zyklen) benutzt werden. Spacyc kann keine unlösbaren Graphen erzeugen, da dort niemals Zyklen auftreten können. Voraussetzung ist in jedem Fall
die Wahl eines nicht-positiven Kostenintervalls. (Auf der DVD befinden sich im
Ordner ,,insoluble“ zwei unlösbare Beispielgraphen)
Kapitel 7
Zusammenfassung
Die Ergebnisse des vorherigen Kapitels zeigen, dass es in unserem Testfeld keinen
optimalen Algorithmus für alle Probleme gibt. Wir wollen deswegen die Ergebnisse zu jedem Verfahren kurz zusammenfassen, um anschließend eine Empfehlungen
auszusprechen.
Das Fifo-Verfahren nach Bellman und Ford ist bei zyklischen, sowie azyklischen
Graphen die schnellste Fifo-Variante und lässt Dequeue und 2-Queue knapp hinter sich. Eine Ausnahme sind negativ bewertete Graphen ohne Zyklen, hier hebt
sich der Fifo deutlich ab.
Die große Schwäche des Fifo offenbart sich bei Gittergraphen. Während er sich
bei geringer Tiefe noch passabel schlägt, fällt er mit zunehmender Tiefe (etwa bei
langen Gittergraphen) katastrophal zurück. Kein anderes Verfahren schneidet bei
dieser Struktur schlechter ab.
Die Verfahren Dequeue und 2-Queue nach Gallo und Pallottino verhalten sich
bei allen Testserien ähnlich. Bei Gittergraphen ist ihr Vorgehen sogar identisch.
Das ist in sofern erstaunlich, weil die theoretische Laufzeit von Dequeue exponentiell ist (siehe Kapitel 4.1.2). Dequeue und 2-Queue fallen nie weit hinter den
Fifo zurück, mit Ausnahme der Acyc-Neg-Serie, in der schon der Fifo keine gute
Performance hat. Bei den langen Gittergraphen sind die beiden Verfahren sogar
die schnellsten im Test, knapp vor Heap und deutlich vor dem Rest des Feldes.
Der Schwellenalgorithmus zeigt ein durchwachsenes Ergebnis. In vielen Tests
löst das Verfahren die Beispiele in guter Zeit und ist mehrfach das schnellste
SPT-Verfahren. Er ist aber immer langsamer als eine der Dijkstra-Varianten.
Kritisch ist das Verhalten bei großen Beispielen der Sprand-Sparse- und GridLong-Serie. Die Geschwindigkeit bricht regelrecht ein. Eventuell könnte durch
geschickte Wahl des Parameters P gegengesteuert werden.
43
KAPITEL 7. ZUSAMMENFASSUNG
44
Der klassische Dijkstra-Algorithmus ist bei dichten Graphen seinen verwandten Verfahren Dial und Heap ebenbürtig. Die Laufzeitabschätzung von O(n2 ) ist
scharf, so dass kein Verfahren schneller sein kann. Bei jedem anderen Graphen
ist er katastrophal langsam. Die meisten Testserien wurden wegen des Zeitlimits
abgebrochen.
Die Implementation mit Heap löst Graphen meist etwas langsamer als die Version nach Dial. Einzige Ausnahme bildet die Testserie mit langen Gittergraphen.
Hier ist Heap schneller als Dial, aber liegt knapp hinter Dequeue und 2-Queue.
Die Version nach Dial ist in allen Testserien das schnellste Verfahren, außer im
Fall von langen Gittergraphen. Allerdings fällt sie auch hier nicht deutlich hinter
den schnellsten Verfahren zurück.
Im Test der steigenden Kosten zeigte sich –wie zu erwarten–, dass das Bucketarrangement empfindlich auf extrem große Kosten reagiert.
Fazit: Bei positiven Graphen ist das Verfahren nach Dial die erste Wahl. Im Fall
von hohen Kosten (eine Million und höher), sollte jedoch auf die Variante Heap
ausgewichen werden.
Graphen mit negativen Kosten lassen sich schwerer einschätzen. Handelt es sich
nicht gerade um einen negativen Graphen ohne Zyklen, so sollte dem Verfahren
Dequeue vor 2-Queue und Fifo der Vorzug gegebenen werden.
Anhang A
Tabellen: Datenstrukturen
10
100
1000
10000
100000
1000000
Init
34
32
32
33
33
33
Enqueue
1
1
4
36
363
3711
Dequeue
0
0
5
42
422
4253
Gesamt
35
33
41
111
818
7997
Tabelle A.1: Queue - Statisches Array
10
100
1000
10000
100000
1000000
Init
0
0
0
0
0
0
Enqueue
0
1
4
38
401
4072
Dequeue
0
0
6
52
524
5298
Tabelle A.2: Queue - Collection
45
Gesamt
0
1
10
90
925
9370
ANHANG A. TABELLEN: DATENSTRUKTUREN
10
100
1000
10000
100000
1000000
Init
0
0
0
0
0
0
Enqueue
0
1
11
120
1245
12308
Dequeue
1
1
8
72
726
7401
46
Gesamt
1
2
19
192
1971
19709
Tabelle A.3: Queue - Verkettete Liste
10
100
1000
10000
100000
1000000
Init
0
0
0
0
0
0
Push
0
0
4
32
1203
99497
Pop
0
0
4
41
431
3930
Gesamt
0
0
8
73
1634
103427
Tabelle A.4: Stack - Dynamisches Array
10
100
1000
10000
100000
1000000
Init
0
1
0
0
0
0
Push
0
0
3
44
560
4630
Pop
0
0
6
56
573
5747
Gesamt
0
1
9
100
1133
10377
Tabelle A.5: Stack - Collection
10
100
1000
10000
100000
1000000
Init
0
0
0
0
0
0
Push
0
1
11
111
1242
11705
Pop
0
0
7
70
729
7203
Tabelle A.6: Stack - Verkette Liste
Gesamt
0
1
18
181
1971
18908
ANHANG A. TABELLEN: DATENSTRUKTUREN
10
100
1000
10000
100000
Init
0
0
0
1
-
Add
0
1
4
45
-
Search
0
2
809
700515
-
Remove
0
0
13
514
-
47
Gesamt
0
3
826
701075
-
Tabelle A.7: List - Collection
10
100
1000
10000
100000
Init
0
0
0
0
0
Add
0
0
15
125
1281
Search
0
16
141
13814
1372273
Remove
0
0
0
233
3196
Gesamt
0
3
156
14172
1376750
Tabelle A.8: List - Doppelt verkettete Liste
10
100
1000
10000
100000
1000000
Init
0
0
0
0
2
18
Insert
0
1
16
137
1371
14099
Find
1
1
7
57
654
6707
Delete
0
3
39
626
7661
93240
Gesamt
1
5
62
820
9688
114064
Tabelle A.9: Heap - Binary Heap
10
100
1000
10000
100000
1000000
Init
0
0
0
0
1
20
Insert
0
1
15
121
1253
12837
Find
1
0
5
53
754
6747
Delete
0
3
36
496
6238
76941
Tabelle A.10: Heap - 3-Heap
Gesamt
1
4
56
670
8246
96545
Anhang B
Tabellen: Vergleichende Analyse
Knoten
(Pfeile)
256
(16384)
512
(65536)
1024
(262144)
2048
(1048576)
4096
(4194304)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
1.749
781
781
782
781
3.418
3.031
3.031
3.031
3.031
6.912
11.910
12.094
11.891
11.965
13.082
46.157
46.531
46.140
46.276
20.310
248.609
245.281
246.954
246.948
2.058
906
922
906
911
4.889
4.406
4.375
4.391
4.391
9.357
16.359
16.484
16.453
16.432
18.998
67.687
67.531
67.282
67.500
37.478
263.250
264.031
265.155
264.145
1.980
890
891
906
896
4.750
4.266
4.219
4.219
4.235
9.019
15.938
15.750
15.890
15.859
18.195
64.219
65.125
64.500
64.615
37.036
265.359
261.812
264.278
263.816
1.733
1.000
984
985
990
3.679
4.516
4.484
4.531
4.510
7.239
20.156
20.094
20.203
20.151
15.595
94.546
95.016
95.922
95.161
29.287
378.516
377.281
376.497
377.431
256
125
125
109
120
512
469
468
469
469
1.024
1.937
1.875
1.844
1.885
2.048
7.391
7.703
7.703
7.599
4.096
30.797
31.062
31.052
30.970
256
110
125
125
120
512
484
485
484
484
1.024
1.891
1.875
1.844
1.870
2.048
7.281
7.406
7.406
7.364
4.096
29.156
29.547
29.851
29.518
256
141
140
141
141
512
469
468
485
474
1.024
1.828
1.781
1.875
1.828
2.048
7.766
7.219
7.281
7.422
4.096
29.297
28.610
28.951
28.953
Tabelle B.1: Laufzeiten der Sprand-Dense Beispiele
48
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
1024
(4096)
2048
(8192)
4096
(16384)
8192
(32768)
16384
(65536)
32768
(131072)
65536
(262144)
131072
(524288)
262144
(1048576)
524288
(2097152)
1048576
(4194304)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
49
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
10.403
394
397
393
395
28.553
1.084
1.087
1.080
1.084
50.925
1.989
1.985
1.989
1.988
111.840
4.358
4.392
4.399
4.383
271.829
10.794
10.819
10.930
10.848
553.394
21.871
22.098
22.404
22.124
1.087.553
44.026
43.562
43.985
43.858
2.454.145
97.764
97.665
97.297
97.575
5.177.648
206.713
205.984
204.750
205.816
9.756.273
387.234
386.172
386.766
386.724
20.942.634
874.422
870.200
860.407
868.343
13.135
499
500
503
501
31.889
1.250
1.254
1.257
1.254
61.646
2.411
2.417
2.416
2.415
113.613
4.426
4.464
4.482
4.457
314.077
12.384
12.514
12.462
12.453
875.419
34.486
34.775
34.914
34.725
1.343.603
52.313
52.688
52.719
52.573
3.413.871
134.343
135.078
136.485
135.302
6.780.313
271.063
267.532
269.563
269.386
12.964.605
517.562
518.672
521.578
519.271
27.134.920
1.078.469
1.145.422
1.130.266
1.118.052
12.195
483
483
484
483
29.433
1.205
1.162
1.163
1.177
58.937
2.340
2.388
2.360
2.363
113.129
4.619
4.621
4.601
4.614
305.169
12.566
12.609
12.628
12.601
767.639
31.524
31.679
31.819
31.674
1.329.118
54.344
54.578
54.375
54.432
3.314.685
137.141
137.110
135.875
136.709
6.635.745
275.094
275.312
274.641
275.016
12.845.159
529.281
531.188
530.656
530.375
27.014.497
1.136.016
1.202.562
1.217.391
1.185.323
2.213
335
295
293
308
4.587
768
766
756
763
9.927
1.384
1.359
1.373
1.372
18.897
2.907
2.918
2.963
2.929
39.411
6.363
6.395
6.319
6.359
78.761
13.017
12.972
13.556
13.182
154.457
26.836
26.894
27.309
27.013
318.447
53.715
55.507
54.937
54.720
634.129
115.531
113.782
114.079
114.464
1.245.223
212.813
213.843
213.813
213.490
2.497.455
1.561.625
1.727.000
1.683.656
1.657.427
1.024
166
166
166
166
2.048
597
599
595
597
4.096
2.282
2.271
2.269
2.274
8.192
8.747
8.861
8.850
8.819
16.384
34.442
34.520
34.592
34.518
32.768
137.264
159.723
159.604
152.197
65.536
564.547
570.734
569.656
568.312
131.072
2.577.767
2.584.071
2.580.432
2.580.757
-
1.024
81
80
82
81
2.048
170
170
167
169
4.096
346
347
347
347
8.192
721
724
718
721
16.384
1.513
1.551
1.555
1.540
32.768
3.601
3.443
3.341
3.462
65.536
6.768
6.780
6.882
6.810
131.072
14.010
13.985
14.126
14.040
262.144
29.416
28.853
28.964
29.078
524.288
60.594
60.422
60.192
60.403
1.048.576
184.422
208.360
198.266
130.927
1.024
65
65
65
65
2.048
117
124
122
121
4.096
215
223
236
225
8.192
420
420
447
429
16.384
836
849
916
867
32.768
1.800
1.958
1.954
1.904
65.536
3.296
3.235
3.402
3.311
131.072
6.666
6.601
6.613
6.627
262.144
13.510
13.452
13.443
13.468
524.288
27.781
27.827
27.851
27.820
1.048.576
70.518
65.484
62.125
68.001
Tabelle B.2: Laufzeiten der Sprand-Sparse Beispiele
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
2048
(32768)
4096
(65536)
8192
(131072)
16384
(262144)
32768
(524288)
65536
(1048576)
131072
(2097152)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
50
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
22.365
2.750
2.719
2.765
2.745
40.980
5.016
5.000
5.093
5.036
97.230
11.812
11.891
11.907
11.870
219.076
26.937
26.984
27.250
27.057
480.627
60.500
59.172
60.172
59.948
982.051
124.906
125.969
123.672
124.849
2.141.147
265.141
265.375
264.015
264.844
41.564
5.156
5.094
5.141
5.130
52.873
6.562
6.562
6.563
6.562
158.963
19.407
19.562
19.562
19.510
310.007
38.281
38.578
38.828
38.562
817.118
103.500
101.468
102.469
102.479
1.493.280
190.422
190.344
188.235
189.667
3.326.888
412.313
411.797
411.594
411.901
37.012
4.625
4.531
4.641
4.599
53.031
6.610
6.625
6.734
6.656
148.305
18.312
18.485
18.516
18.438
301.919
37.938
37.844
38.313
38.032
751.362
96.391
94.797
95.437
95.542
1.467.904
188.625
187.359
186.390
187.458
3.241.850
408.125
407.109
407.328
407.521
9.641
2.313
2.297
2.343
2.318
18.695
4.375
4.375
4.375
4.375
38.181
9.969
10.125
10.109
10.068
78.687
21.172
21.172
21.219
21.188
159.513
44.828
44.235
44.407
44.490
323.215
89.141
88.484
88.360
88.662
661.827
184.359
183.906
183.484
183.916
2.048
937
875
843
885
4.096
2.906
2.922
2.906
2.911
8.192
12.281
12.297
12.328
12.302
16.384
41.484
41.125
41.235
41.281
32.768
161.969
168.110
161.141
163.740
65.536
643.438
648.000
646.625
646.021
131.072
2.498.891
2.497.375
2.499.413
2.498.560
2.048
390
344
360
365
4.096
781
719
766
755
8.192
1500
1.500
1.516
1.505
16.384
3.031
3.062
3.112
3.068
32.768
6.422
6.297
6.797
6.505
65.536
13.984
13.985
13.812
13.927
131.072
27.469
26.328
27.985
27.261
2.048
313
312
297
307
4.096
609
625
656
630
8.192
1.203
1.218
1.281
1.234
16.384
2.500
2.547
2.578
2.542
32.768
4.844
4.796
4.671
4.770
65.536
9.719
9.719
9.531
9.656
131.072
19.234
19.079
19.156
19.156
Tabelle B.3: Laufzeiten der Sprand-Average Beispiele
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
2048
(32768)
4096
(65536)
8192
(131072)
16384
(262144)
32768
(524288)
65536
(1048576)
131072
(2097152)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
51
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
18.635
2.156
2.141
2.157
2.151
39.038
4.656
4.671
4.687
4.671
80.116
9.328
9.375
9.360
9.354
198.484
23.282
23.359
23.266
23.302
385.047
45.281
45.344
45.532
45.386
761.697
92.547
92.875
93.016
92.813
1.712.144
208.830
209.593
207.093
208.505
27.546
2.781
2.766
2.750
2.766
46.136
5.109
5.110
5.125
5.115
101.297
11.234
11.250
11.187
11.224
247.951
28.484
28.516
28.250
28.417
476.031
54.266
54.485
54.625
54.459
927.142
109.719
109.968
110.016
109.901
2.071.786
251.232
251.000
249.485
250.572
25.981
2.719
2.656
2.687
2.687
44.733
5.031
5.047
5.032
5.037
100.390
11.328
11.297
11.266
11.297
235.272
27.266
27.234
27.109
27.203
456.518
52.687
52.671
52.734
52.697
909.379
109.062
108.719
109.015
108.932
1.998.046
246.091
247.078
245.359
246.176
7.447
1.859
1.844
1.844
1.849
15.815
3.407
3.422
3.468
3.432
27.869
6.516
6.609
6.562
6.562
55.585
15.140
15.172
15.047
15.120
102.131
25.938
26.125
26.453
26.172
201.241
50.329
50.688
50.582
50.533
405.257
106.160
106.813
105.391
106.121
2.048
937
828
828
864
4.096
2.922
2.953
2.922
2.932
8.192
12.328
12.313
12.328
12.323
16.384
48.235
47.469
47.375
47.693
32.768
183.968
183.094
183.756
183.606
65.536
747.609
747.562
747.571
747.581
-
2.048
344
359
375
359
4.096
718
703
711
711
8.192
1.609
1.469
1.500
1.526
16.384
3.000
3.015
2.984
3.000
32.768
6.609
6.125
6.141
6.292
65.536
12.453
13.012
12.968
12.811
131.072
27.219
27.422
27.047
27.229
2.048
297
297
297
297
4.096
594
625
605
608
8.192
1.219
1.265
1.188
1.224
16.384
2.375
2.438
2.516
2.443
32.768
4.516
4.516
4.625
4.552
65.536
9.156
9.219
9.297
9.224
131.072
18.313
18.375
18.000
18.229
Tabelle B.4: Laufzeiten der Acyc-Pos Beispiele
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
Durchläufe
1. Messung
2048
2. Messung
(32768)
3. Messung
Durchschnitt
Durchläufe
1. Messung
4096
2. Messung
(65536)
3. Messung
Durchschnitt
Durchläufe
1. Messung
8192
2. Messung
(131072)
3. Messung
Durchschnitt
Durchläufe
1. Messung
16384
2. Messung
(262144)
3. Messung
Durchschnitt
Durchläufe
1. Messung
32768
2. Messung
(524288)
3. Messung
Durchschnitt
Durchläufe
1. Messung
65536
2. Messung
(1048576)
3. Messung
Durchschnitt
Durchläufe
1. Messung
131072
2. Messung
(2097152)
3. Messung
Durchschnitt
Fifo
Dequeue
2-Queue
598.804
48.500
48.641
47.984
48.375
1.992.840
165.422
165.937
163.218
164.859
7.573.389
619.954
620.484
623.597
621.345
30.177.943
2.411.922
2.410.344
2.415.879
2.412.715
-
4.856.406
370.125
372.015
369.922
370.687
13.755.409
1.060.578
1.069.516
1.062.931
1.064.342
-
2.602.157
191.110
191.782
191.156
191.349
10.451.842
813.734
816.688
815.765
815.396
41.646.112
3.000.875
3.001.058
3.000.978
3.000.970
-
Tabelle B.5: Laufzeiten der Acyc-Neg Beispiele
52
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
4097
(12288)
16385
(49152)
65537
(196608)
262145
(786432)
1048577
(3145728)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
53
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
23.198
699
700
687
695
180.121
5.500
5.532
5.515
5.516
1.267.125
39.032
38.625
38.953
38.870
11.120.409
344.813
346.484
340.844
344.047
62.378.411
1.932.484
1.937.500
1.936.894
1.935.626
5.135
156
157
156
156
20.999
640
671
641
651
82.625
2.546
2.500
2.563
2.536
331.207
10.219
10.344
10.078
10.214
1.355.454
41.594
41.531
42.562
41.896
5.135
165
164
172
167
20.999
689
672
672
678
82.625
2.641
2.688
2.687
2.672
331.207
10.890
11.000
10.781
10.890
1.355.454
44.172
44.329
45.140
44.547
17
174
187
172
178
3
693
688
687
689
11
3.063
2.984
2.969
3.005
3
14.297
14.266
14.172
14.245
3
69.406
69.375
69.438
69.406
4.097
2.965
2.980
2.953
2.966
16.385
45.922
46.015
46.110
46.016
65.537
648.453
637.063
692.758
659.425
-
4.097
229
248
219
232
16.385
907
922
969
933
65.537
3.953
3.953
3.984
3.963
262.145
17.563
17.672
17.407
17.547
1.048.577
71.609
74.578
73.172
73.120
4.097
216
215
203
211
16.385
750
750
703
734
65.537
2.515
2.500
2.516
2.510
262.145
9.500
9.781
9.500
9.594
1.048.577
36.953
36.734
36.609
36.765
Tabelle B.6: Laufzeiten der Grid-Square Beispiele
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
8193
(24576)
16385
(49152)
32769
(98304)
65537
(196608)
131073
(393216)
262145
(786432)
524289
(1572864)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
54
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
17.198
515
531
548
531
35.617
1.078
1.094
1.076
1.083
66.186
2.063
2.078
2.116
2.086
138.427
4.328
4.250
4.312
4.297
274.079
8.484
8.844
8.578
8.635
557.023
17.281
19.016
17.343
17.880
1.107.578
34.547
34.125
34.532
34.401
10.043
297
328
297
307
20.384
609
656
610
625
41.005
1.281
1.219
1.266
1.255
81.837
2.484
2.516
2.500
2.500
163.851
5.000
5.000
5.047
5.016
327.809
9.906
9.938
9.969
9.938
653.202
20.046
19.875
19.953
19.958
10.043
328
328
328
328
20.384
656
656
656
656
41.005
1.297
1.359
1.313
1.323
81.837
2.657
2.656
2.688
2.667
163.851
5.344
5.328
5.375
5.349
327.809
10.641
10.718
10.687
10.682
653.202
21.344
21.188
21.328
21.287
11
344
344
360
349
5
750
703
703
719
37
1.453
1.406
1.453
1.437
3
2.921
2.907
2.891
2.906
15
5.875
5.828
5.812
5.838
3
11.765
11.750
11.735
11.750
7
23.641
23.703
23.578
23.641
8.193
10.047
10.031
10.047
10.042
16.385
39.610
39.532
41.750
40.297
32.769
183.687
183.906
185.766
184.453
65.537
646.485
637.172
658.327
647.328
-
8.193
500
500
500
500
16.385
1.079
1.125
1.109
1.104
32.769
2.250
2.297
2.265
2.271
65.537
4.844
4.968
4.906
4.906
131.073
10.594
10.578
10.828
10.667
262.145
22.453
21.844
21.750
22.016
524.289
47.734
45.687
46.156
46.526
8.193
328
328
328
328
16.385
593
625
594
604
32.769
1.188
1.235
1.250
1.224
65.537
2.281
2.297
2.422
2.333
131.073
4.578
4.625
4.594
4.599
262.145
9.000
9.172
9.140
9.104
524.289
18.250
18.110
18.172
18.177
Tabelle B.7: Laufzeiten der Grid-Wide Beispiele
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Knoten
(Pfeile)
8193
(24576)
16385
(49152)
32769
(98304)
65537
(196608)
131073
(393216)
262145
(786432)
524289
(1572864)
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
3. Messung
Durchschnitt
55
Fifo
Dequeue
2-Queue
Thresh
Dijkstra
Heap
Dial
375.680
12.016
11.547
11.592
11.718
1.600.451
49.950
48.797
48.843
49.197
5.732.346
176.719
177.375
175.766
176.620
25.943.212
793.125
794.375
794.287
793.929
114.733.166
3.452.313
3.463.672
3.454.356
3.456.780
-
10.283
328
312
361
334
20.548
637
610
657
635
40.943
1.281
1.313
1.281
1.292
81.842
2.500
2.484
2.609
2.531
164.415
4.937
4.922
5.041
4.967
329.289
10.203
10.281
10.297
10.260
656.807
20.266
20.297
20.328
20.297
10.283
343
375
331
350
20.548
661
703
656
673
40.943
1.313
1.359
1.297
1.323
81.842
2.657
2.625
2.641
2.641
164.415
5.250
5.250
5.234
5.245
329.289
10.453
10.422
10.578
10.484
656.807
20.906
21.016
20.813
20.912
3
391
360
367
373
3
741
734
734
736
7
1.593
1.625
1.609
1.609
3
3.437
3.547
3.515
3.500
5
9.235
9.250
9.204
9.230
3
48.516
48.734
49.157
48.802
3
175.719
176.843
175.672
176.078
8.193
10.047
10.062
10.520
10.210
16.385
40.000
39.594
39.656
39.750
32.769
159.594
160.047
159.594
159.745
65.537
740.360
742.172
743.875
742.136
131.073
2.924.078
2.926.547
2.925.063
2.925.229
-
8.193
422
390
387
400
16.385
799
750
750
766
32.769
1.516
1.531
1.500
1.516
65.537
3.125
3.047
3.110
3.094
131.073
6.000
6.000
6.015
6.005
262.145
12.031
11.969
11.984
11.995
524.289
24.047
24.329
24.172
24.183
8.193
812
813
812
812
16.385
1.595
1.609
1.610
1.605
32.769
3.250
3.297
3.297
3.281
65.537
6.406
6.656
6.484
6.515
131.073
12.469
13.172
12.313
12.651
262.145
25.672
24.672
25.406
25.250
524.289
51.390
49.718
51.156
50.755
Tabelle B.8: Laufzeiten der Grid-Long Beispiele
ANHANG B. TABELLEN: VERGLEICHENDE ANALYSE
Kosten
Durchläufe
1. Messung
2. Messung
[0;10]
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
[0;100]
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
[0;1000]
3. Messung
Durchschnitt
Durchläufe
1. Messung
2. Messung
[0;10000]
3. Messung
Durchschnitt
Durchläufe
1. Messung
[0;100000] 2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
[0;1000000] 2. Messung
3. Messung
Durchschnitt
Durchläufe
1. Messung
[0;10000000] 2. Messung
3. Messung
Durchschnitt
Fifo
135.698
4.141
4.169
4.255
4.188
137.611
4.333
4.208
4.331
4.291
134.224
4.193
4.395
4.172
4.253
137.668
4.250
4.323
4.215
4.263
135.451
4.393
4.218
4.196
4.269
136.737
4.180
4.241
4.263
4.228
134.395
4.281
4.230
4.346
4.286
Dequeue
81.428
2.452
2.496
2.490
2.479
81.982
2.501
2.475
2.478
2.485
81.873
2.520
2.490
2.512
2.507
82.131
2.527
2.484
2.462
2.491
81.846
2.511
2.457
2.481
2.483
81.820
2.468
2.476
2.582
2.509
82.036
2.482
2.537
2.520
2.513
2-Queue
81.428
2.623
2.655
2.629
2.636
81.982
2.674
2.632
2.661
2.656
81.873
2.667
2.665
2.814
2.715
82.131
2.684
2.649
2.642
2.658
81.846
2.687
2.622
2.654
2.654
81.820
2.634
2.643
2.686
2.654
82.036
2.644
2.703
2.681
2.676
Thresh
7
2.760
2.826
2.773
2.786
5
2.861
2.865
2.864
2.863
11
2.894
2.886
2.946
2.909
5
2.901
2.865
2.867
2.878
10
2.894
2.957
2.905
2.919
7
2.875
2.868
2.883
2.875
7
2.858
2.875
2.867
2.867
Tabelle B.9: Laufzeiten der Grid-Cost Beispiele
56
Heap
65.537
4.763
5.219
4.923
4.968
65.537
4.919
4.938
4.929
4.929
65.537
4.943
4.884
4.977
4.935
65.537
5.134
4.890
4.860
4.961
65.537
4.985
5.013
5.063
5.020
65.537
4.904
4.913
5.004
4.940
65.537
4.859
4.923
4.928
4.903
Dial
65.537
3.583
3.365
3.368
3.439
65.537
2.366
2.404
2.526
2.432
65.537
2.268
2.269
2.283
2.273
65.537
2.336
2.444
2.428
2.403
65.537
2.921
2.844
2.772
2.846
65.537
6.692
6.615
6.582
6.630
65.537
2.995.570
2.929.318
2.928.953
2.951.280
Literaturverzeichnis
[AMO93] Ahuja, Ravindra K., Thomas L. Magnanti, and James B. Orlin: Network Flows: Theory, Algorithms and Applications. PrenticeHall, New Jersey, 1993.
[Bei03]
Beisel, Prof. Dr. Ernst-Peter: Operations Research II - Diskrete
Optimierung. Vorlesung, Bergische Universität, Wuppertal, Oktober
2003. http://www.math.uni-wuppertal.de/org/OR/.
[CGR94] Cherkassky, Boris V., Andrew V. Goldberg, and Tomasz
Radzik: Shortest paths algorithms: Theory and experimental evaluation. In SODA: ACM-SIAM Symposium on Discrete Algorithms (A
Conference on Theoretical and Experimental Analysis of Discrete Algorithms), 1994.
[DIM90] DIMACS: The first dimacs international algorithm implementation
challenge: Problem definitions and specifications. Technical report,
State University, New Jersey, 1990. http://dimacs.rutgers.edu/
Challenges/.
[GG01]
Getz, Ken and Mike Gilbert: VBA Developer’s Handbook. Sybex
Books, Alameda, 2nd edition, 2001.
[McK97] McKinney, Bruce: Hardcore Visual Basic. Microsoft Press, Redmond, 2nd edition, 1997. http://www.mvps.org/vb/hardcore/.
[NM04]
Neumann, Klaus und Martin Morlock: Operations Research.
Hanser, München, Wien, Zweite Auflage, 2004.
57
Herunterladen