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