Daten, Daten, Daten. Und immer an den Zugriff denken. Zusammenfassung In dieser Ausarbeitung wird ein externer Algorithmus beschrieben, welcher die Breitensuche auf ungerichteten Graphen mit wenig Kanten mit einem sublinearem Aufwand für die Anzahl der IO-Operationen in Bezug auf die Anzahl der Knoten durchführt. Schriftliche Ausarbeitung von Michael Hußmann im Rahmen des Seminars Perlen der Theoretischen Informatik. Wintersemester 2002/2003 - Universität Paderborn Inhaltsverzeichnis 1 Einleitung 2 1.1 Externe Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2 Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2 Das Parallel Disk Model (PDM) 3 2.1 Merkmale des PDM-Modells . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.2 Parameter des PDM-Modells . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.3 Fundamentale IO-Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 3 Breitensuche 5 3.1 Interner Algorithmus für die Breitensuche . . . . . . . . . . . . . . . . . . . . . . . 5 3.2 Probleme des internen Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 4 Externe Breitensuche nach Munagala und Ranade 7 4.1 Idee und Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 4.2 Abschätzung der IO-Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 5 Externe Breitensuche nach Mehlhorn und Meyer 9 5.1 Idee des Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 5.2 Partitionierungsphase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 5.2.1 Zieldarstellung und Verfahren . . . . . . . . . . . . . . . . . . . . . . . . . . 9 5.2.2 Randomisierte Partitionierung . . . . . . . . . . . . . . . . . . . . . . . . . 10 5.2.3 Aufbau der Datenstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 BFS-Phase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 5.3.1 Vorgehensweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 5.3.2 Abschätzung der IO-Operationen . . . . . . . . . . . . . . . . . . . . . . . . 12 5.3 6 Zusammenfassung 13 1 1 Einleitung Der Zusatz Und immer an den Zugriff denken“ auf der Titelseite gibt bereits einen wichtigen ” Hinweis auf den Inhalt meiner Seminararbeit: Um die Laufzeit von Algorithmen abschätzen zu können, wurden in der theoretischen Informatik verschiedene Rechnermodelle und Effizienzmaße entwickelt. Ein wichtiges Beispiel für ein Rechnermodell ist z.B. das der RAM (Random Access Machine), welches abstrakt die Arbeitsweise aller heutigen Rechner mit von-Neumann-Architektur beschreibt. Auf das RAM -Modell soll hier jedoch nicht genauer eingegangen werden, ebenso wenig auf die verschiedenen Effizienzmaße, die bereits bekannt sein sollten. Stattdessen soll der Blick des Lesers auf die Art und Weise gerichtet werden, wie mittels des RAM -Modells Laufzeiten von Algorithmen abgeschätzt werden: Ein wesentliches Merkmal ist, daß alle Speicherzugriffe als gleich teuer angesehen werden. Diese Abschätzung mag tatsächlich in vielen Fällen richtig sein bzw. als grobe Abschätzung genügen. Sie trifft jedoch nicht mehr zu, wenn ein Programm im Laufe seiner Ausführung auf unterschiedlich schnelle Speicher zugreift. 1.1 Externe Algorithmen Unterschiedlich schnelle Speicher liegen aber bei fast allen existierenden Rechnern vor: Damit die entsprechenden Systeme nicht zu teuer in der Anschaffung sind, ist der Speicher hierarchisch angeordnet. Auf der höchsten Ebene finden sich die Register der CPU, auf die sehr schnell zugegriffen werden kann. Dieser Speicher ist daher auch der teuerste. Auf der zweiten Ebene der Hierarchie befindet sich häufig ein sogenannter Cache-Speicher als Bindeglied zwischen der CPU und dem Hauptspeicher auf der nächsten Stufe. Im Cache-Speicher werden die als nächstes benötigten oder häufig benutzten Daten gespeichert, um die Zugriffe der CPU auf den langsamen Hauptspeicher zu beschleunigen, welcher erheblich billiger als der schnelle Cache-Speicher ist. Daher dauern Zugriffe auf den Hauptspeicher bei Verwendung von Cache-Speicher unterschiedlich lang, weil sich ein angefordertes Datum entweder im schnellen Cache-Speicher oder im langsamen Hauptspeicher befinden kann. Abbildung 1: Vereinfachte Speicher-Hierarchie in modernen Rechner-Architekturen. Auf der untersten Ebene dieser Hierarchie befindet sich der Sekundärspeicher, zu dem z.B. Festplatten, CD-ROM-Laufwerke etc. gehören, die sehr große Datenmengen speichern können, aber auch sehr langsam im Vergleich zu den anderen vorgestellten Speichern sind. Es lassen sich daher in Bezug auf Hauptspeicher und Sekundärspeicher dieselben Unterschiede in den Zugriffszeiten 2 beobachten, die vorhin beschrieben wurden. Diese Speicherhierachie macht somit in vielen Fällen eine genaue Laufzeitabschätzung eines Algorithmus unmöglich. Trotzdem sind Modelle wie das der RAM nicht unbrauchbar: Ist der Hauptspeicher eines Rechners groß genug für die Daten, die ein Programm benötigt, fallen zumindest die langsamen Zugriffe auf den Sekundärspeicher weg, welche mit Abstand am zeitaufwendigsten gegenüber Zugriffen auf andere Speicher sind: Typischerweise dauert ein Zugriff auf den Hauptspeicher (DRAM) 60-70 ns bzw. auf den Cache-Speicher (SRAM) 12-15 ns, während der Zugriff auf eine Festplatte (IDE) ca. 10 ms benötigt. Die Laufzeitabschätzung eines Algorithmus mit Hilfe des RAM -Modells ist daher in den meisten Fällen als ausreichend anzusehen, sofern keine Zugriffe auf Sekundärspeicher auftreten. Bei Problemen mit sehr großen Datenmengen wie z.B. umfangreichen Matrizen oder Graphen reicht der Hauptspeicher eines Rechners jedoch nicht mehr aus, d.h. Zugriffe auf den Sekundärspeicher sind nicht mehr zu vermeiden. Daher müssen sogenannte externe Algorithmen“ ” entwickelt werden, die die Zugriffe auf den Sekundärspeicher minimieren. Weiter werden neue Modelle benötigt, um die Laufzeiten dieser externen Algorithmen unter Berücksichtigung der verschieden schnellen Speicher abschätzen zu können. 1.2 Breitensuche Ein gut studiertes Verfahren in der theoretischen Informatik ist die Breitensuche, welche einen Graphen ebenenweise durchläuft. Neben der Graphtraversierung hat die Breitensuche viele andere Anwendungen wie z.B. die Partitionierung eines Graphens etc. Falls ein Graph komplett im Hauptspeicher eines Rechners abgebildet werden kann, sind optimale Algorithmen bekannt, die die Breitensuche in Zeit O(|V | + |E|) durchführen können. Benutzt nun aber ein Algorithmus die Breitensuche zur Lösung eines Problems, dessen Platzbedarf größer als der zur Verfügung stehende Hauptspeicher ist, kann die Breitensuche mit den bekannten Algorithmen nicht mehr effizient durchgeführt werden, weil viele langsame Zugriffe auf den Sekundärspeicher auftreten. In dieser Ausarbeitung wird ein externer Algorithmus mitsamt verwendeter Datenstruktur beschrieben, welcher die Breitensuche auf ungerichteten Graphen mit wenig Kanten so effizient löst, daß der Aufwand für die IO-Operationen sublinear in Bezug auf die Anzahl der Knoten ist. Diese Lösung wurde erst in diesem Jahr (2002) von Mehlhorn und Meyer [2] entwickelt und baut auf einer Idee von Munagala und Ranade [3] auf. In [1] findet sich eine weitere Beschreibung der Lösung, die weniger technisch aber dafür anschaulicher geworden ist. Der Rest dieser Ausarbeitung ist wie folgt gegliedert: In Kapitel 2 wird ein Modell vorgestellt, mit dem externe Algorithmen bewertet werden können. Kapitel 3 wiederholt das Verfahren der Breitensuche und begründet, warum die bekannten Algorithmen versagen, wenn die Daten im Sekundärspeicher abgelegt sind. Danach folgt in Kapitel 4 der Algorithmus von Munagala und Ranade, der in der vorzustellenden Lösung verwendet wird. Kapitel 5 beschäftigt sich ausführlich mit dem Verfahren von Mehlhorn und Meyer. In einer abschließenden Zusammenfassung werden kurz die wesentlichen Ergebnisse wiederholt. 2 Das Parallel Disk Model (PDM) Das Parallel Disk Model wird verwendet, um die Anzahl der IO-Operationen eines externen Algorithmus abschätzen zu können. Der Nutzen dieses Modells wurde bereits in der Einleitung motiviert. In diesem Kapitel werden in aller Kürze die wesentlichen Aspekte dieses Modells beschrieben. Für weitere Fragen sei auf den schönen Aufsatz von Vitter [4] verwiesen. 3 2.1 Merkmale des PDM-Modells Das PDM-Modell ist so konstruiert, daß drei wesentliche Kenngrößen zur Bewertung von externen Algorithmen ermittelt werden können: 1. Anzahl der IO-Operationen 2. Platzbedarf auf der Platte 3. CPU-Zeit Um die Bewertung der Algorithmen nicht unnötig kompliziert zu gestalten, konzentriert sich diese Ausarbeitung auf die Anzahl der IO-Operationen und geht auf den Platzbedarf auf der Platte nur am Rande ein. 2.2 Parameter des PDM-Modells Definition 2.1 (Problem-Parameter des PDM-Modells) Sei N die Größe des Problems, M die Größe des internen Speichers, B die Größe eines Blocks für einen Plattenzugriff, D die Anzahl der unabhängigen Platten und P die Anzahl der Prozessoren. Die Werte der Parameter N , M , B beziehen sich jeweils auf eine Anzahl an Elementen. Es gelte weiter: M < N und 1 ≤ D · B ≤ M/2. D bezeichnet die Anzahl der Platten, die unabhängig voneinander angesprochen werden können. Dieser Parameter hat hohen Bezug zur Praxis, weil dort häufig sogenannte RAID-Systeme verwendet werden, die mehrere Platten ansteuern, um den Zugriff erstens zu beschleunigen und zweitens durch Redundanz die Ausfallsicherheit zu erhöhen. Die Bedingung M < N ist sinnvoll, weil sonst die Daten eines Problems vollständig in den Hauptspeicher passen würden. Folglich müßte ein interner Algorithmus betrachtet werden, so daß das PDM-Modell zur Auswertung ungeeignet wäre. Die Bedingung 1 ≤ D · B ≤ M/2 hat folgende Hintergründe: Ein wichtiges Verfahren, von dem viele externe Algorithmen Gebrauch machen, ist Disk Striping. Die Idee ist, daß die Daten plattenübergreifend in mehreren Streifen (stripes) abgelegt sind. Dies ermöglicht den Zugriff auf B · D aufeinanderfolgende Elemente mit einem Plattenzugriff. Eine Folge von N Elementen kann mit diesem Verfahren also mittels O(N/(D · B)) IO-Operationen gelesen/geschrieben werden. Es ist anzumerken, daß bei Schreibvorgängen ein entsprechend großer Puffer zu verwenden ist. Abbildung 2: Visualisierung von Disk Striping für D = 5 und B = 2. 4 Sollen nun Daten in irgendeiner Form miteinander verknüpft oder verglichen werden, so sollte die Größe des internen Speichers mind. 2 · B · D sein, um die Daten von zwei Zugriffen aufnehmen zu können. Wichtig ist, daß beim Disk Striping bei einem Plattenzugriff nur auf solche Blöcke der einzelnen Platten zugegriffen werden darf, die in einem einzigen Streifen liegen. Im Prinzip verhalten sich also die D Platten wie eine einzige logische Platte mit entsprechend größerer Blockgröße. Die Anzahl der Prozessoren wird in dieser Ausarbeitung mit P = 1 angenommen, denn uns interessiert nur der Flaschenhals“ zwischen internem Speicher und den Platten. Außerdem ist der ” Fall P 6= 1 wesentlich komplexer und würde von der eigentlichen Aufgabe, nämlich der Minimierung der IO-Operationen, nur ablenken. 2.3 Fundamentale IO-Operationen Bei der Auswertung von externen Algorithmen treten aufgrund der relativ großen Anzahl an zu berücksichtigenden Parametern des PDM-Modells komplizierte Terme für die Abschätzung der IO-Operationen auf. Zur Vereinfachung bezieht man sich daher oft auf die Abschätzungen von fundamentalen Operationen. Zwei dieser Operationen behandelt der folgende Satz; für weitere Details sei auf [4] verwiesen: Satz 2.1 (Fundamentale IO-Operationen) Sei F eine Datei im Sekundärspeicher, die aus x Elementen besteht. Dann gilt unter den oben genannten Bedingungen: • F kann mittels scan(x) := O(x/(D · B)) IO-Operationen sequentiell gelesen/geschrieben werden. • F kann mittels sort(x) := O(x/(D · B) · logM/B (x/B)) IO-Operationen sortiert werden. 2 Für einen Beweis des Satzes siehe [4]. Alle in diesem Kapitel eingeführten Bezeichnungen werde ich später ohne weitere Hinweise oder Erklärungen konsequent verwenden, um das Verständnis zu erleichtern. 3 Breitensuche Die Breitensuche wurde bereits in der Einleitung motiviert. In diesem Kapitel soll anhand eines internen Algorithmus verdeutlicht werden, warum auch i.a. interne Algorithmen für die Breitensuche versagen, wenn die Daten im Sekundärspeicher abgelegt sind. 3.1 Interner Algorithmus für die Breitensuche Es gibt viele verschiedene interne Algorithmen für die Breitensuche, die aber im Prinzip alle ähnlich vorgehen: Der erste zu besuchende Knoten eines Graphens wird einer FIFO-Queue hinzugefügt, die zu Beginn noch leer ist. Danach wird in einer Schleife jeweils ein Knoten aus der Queue entnommen und seine Nachfolger werden zur Queue hinzugefügt, sofern diese noch nicht besucht wurden. Die Schleife wird beendet, wenn die Queue leer ist. An dieser Stelle wird ein interner Algorithmus (entnommen aus [5]) vorgestellt, um dem Leser die Laufzeitabschätzung sowie die auftretenden Probleme bei der Verwendung von Sekundärspeicher besser erläutern zu können. 5 IM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 BF S(G, s) for each vertex u V [G]-{s} do color[u] := white d[u] := ∞ π[u] := nil color[s] := gray d[s] := 0 π[s] := nil Q := {s} while Q 6= ∅ do u := head[Q] for each v Adj[u] do if color[v] = white then color[v] := gray d[v] := d[u] + 1 π[v] := u enqueue(Q,v) dequeue(Q) color[u] := black Dieser Algorithmus benötigt Zeit O(|V | + |E|), weil alle Adjazenzlisten durchlaufen werden (O(|E|)) und jeder Knoten einmal zur Queue hinzugefügt und wieder entfernt wird (O(|V |)). 3.2 Probleme des internen Algorithmus Ist der Hauptspeicher eines Rechners groß genug, um den Graphen vollständig aufzunehmen, so ist der vorgestellte Algorithmus für die Breitensuche optimal. Ganz anders sieht es aber aus, wenn der Graph im langsamen Sekundärspeicher liegt: Es müssen im schlimmsten Fall Θ(|V | + |E|) IO-Operationen durchgeführt werden, die in der Praxis die Breitensuche und damit evtl. auch ein Programm, welches diese benutzt, stark verlangsamen. Dieses Problem betrifft zunächst einmal alle Verfahren zur Graph-Traversierung, also auch die bekannte Tiefensuche und das Single-Source-Shortest-Paths-Problem. Bei allen den Verfahren zugrunde liegenden Algorithmen lassen sich zwei wesentliche Probleme ausmachen: 1. Unstrukturierte Zugriffe auf die Adjazenzlisten 2. Unstrukturierte Abfragen, ob ein Knoten bereits besucht wurde Vor allem das erste Problem sieht wirklich dramatisch aus: Enthält eine Adjazenzliste k Kanten, so werden Θ(1 + k/B) IO-Operationen benötigt, um alle Kanten zu ermitteln. Falls k = Ω(B), lohnen sich die Zugriffe auf den Sekundärspeicher. Gilt dagegen k = O(1), so wird ein teurer Zugriff auf den externen Speicher für nur wenige Kanten durchgeführt. Ist der Graph dünn, d.h. die Adjazenzlisten i.a. eher kurz, so treten viele IO-Zugriffe auf, die bei weitem nicht die Blockgröße B für den Plattenzugriff ausnutzen. Insgesamt läßt sich festhalten: Es werden Θ(|V |) viele IOOperationen benötigt, um die Adjazenzlisten der einzelnen Knoten einzulesen. Das zweite Problem ist weniger schlimm und wird bereits von dem Algorithmus von Munagala und Ranade [3], der im nächsten Kapitel vorgestellt wird, gelöst: Es müssen Θ(|E|) viele Abfragen durchgeführt werden, ob ein Knoten bereits besucht wurde. 6 4 4.1 Externe Breitensuche nach Munagala und Ranade Idee und Algorithmus Im vorherigen Kapitel wurde darauf hingewiesen, daß der Algorithmus von Munagala und Ranade [3] bereits eine der zwei Ursachen für die hohe Anzahl an IO-Operationen verhindert, nämlich die unstrukturierten Abfragen, ob ein Knoten bereits besucht wurde. Die wesentliche Idee dieser Lösung ist es, nicht sofort den BFS-Baum zu berechnen, sondern zunächst nur die BFS-Level der einzelnen Knoten, d.h. die kürzesten Abstände vom Startknoten der Breitensuche zu den jeweiligen Knoten. Der BFS-Baum kann im Anschluß einfach aus den BFS-Leveln konstruiert werden. Dazu definiert man L(t) als Menge der Knoten auf Level t, d.h. mit BFS-Level t. Weiter sei A(t) die Multimenge der Nachbarknoten der Knoten aus L(t − 1): A(t) := N (L(t − 1)), wobei N (u) die Menge der Nachbarn eines Knotens u ist. Der Algorithmus berechnet nun sukzessiv eine Menge L(t) aus L(t − 1), indem zunächst A(t) wie oben beschrieben berechnet wird. Anschließend werden die Duplikate aus A(t) entfernt. L(t) entsteht aus A(t) dadurch, daß alle Elemente entfernt werden, die bereits in L(t − 1) und L(t − 2) enthalten sind. Abbildung 3: Eine Phase im Algorithmus von Munagala und Ranade [3]. Der folgende Algorithmus (entnommen aus [3]) berechnet für jeden Knoten sein BFS-Level: Ist ein Knoten in der Menge L(t) enthalten, so ist sein BFS-Level t. M R BF S(G) 1 for each vertex u V [G] do C[u] := 0 2 L(−1) := L(−2) := ∅ 3 t := c := 0 4 while L(t − 1) 6= ∅ or unvisited vertices exist do 5 if L(t − 1) = ∅ then 6 L(t) := next unvisited vertex 7 c := c + 1 8 else 9 A(t) := N (L(t − 1)) 10 Remove duplicates from A(t) 11 L(t) := A(t) \ (L(t − 1) ∪ L(t − 2)) 12 for each vertex u L(t) do C[u] := c 13 t := t + 1 Aus den BFS-Leveln der Knoten lassen sich die BFS-Nummern berechnen, welche eine Ordnung auf den Knoten eines bestimmten Levels definieren. Mit Hilfe dieser BFS-Nummern kann anschlie7 ßend der BFS-Baum konstruiert werden. Dieser zusätzliche Aufwand kostet O(sort(|V | + |E|)) IO-Operationen. Für einen Beweis und weitere Details sei auf [6] verwiesen. Der hier vorgestellte Algorithmus funktioniert allerdings nur auf ungerichteten Graphen. Das Gleiche gilt für die Lösung von Mehlhorn und Meyer [2], welche im nächsten Kapitel behandelt wird, weil diese den Algorithmus von Munagala und Ranade verwendet. Ein Korrektheitsbeweis des Algorithmus für ungerichtete Graphen findet sich in [2]. Abbildung 4: Gerichteter Graph, bei dem die Idee von Munagala und Ranade [3] versagt. Abbildung 4 liefert ein Beispiel für einen gerichteten Graphen, auf dem die Breitensuche von Munagala und Ranade versagt. Der Grund ist, daß der Startknoten s in Runde 4 als einziger Knoten des Graphens in die Menge A(4) aufgenommen wird. Da s zu diesem Zeitpunkt nur in L(0) enthalten ist, wird es auch in die Menge L(4) aufgenommen. Damit ist s sowohl in L(0) als auch in L(4) enthalten. Im nächsten Durchlauf wird der Nachfolger von s in die Menge L(5) aufgenommen, obwohl sich dieser bereits in L(1) befindet usw. Der Algorithmus von Munagala und Ranada bricht also auf diesem Graphen niemals ab und findet stattdessen in jeder Runde scheinbar neue Ebenen mit unbesuchten Knoten. 4.2 Abschätzung der IO-Operationen Der folgende Satz liefert eine Abschätzung der IO-Operationen für die Breitensuche mit dem Algorithmus von Munagala und Ranade und der anschließenden Konstruktion des BFS-Baumes: Satz 4.1 (Breitensuche nach Munagala und Ranade) Die Breitensuche kann auf ungerichteten Graphen mit O(|V | + sort(|V | + |E|)) IO-Operationen durchgeführt werden. Beweis: Im Algorithmus von Munagala und Ranade treten die Zugriffe auf den Sekundärspeicher P bei der Berechnung von PL(t) aus L(t − 1) auf. Man kann sich leicht klarmachen, daß gilt: |N (L(t))| = O(|E|) und t t |(L(t)| = O(|V |). In Zeile 9 werden |L(t − 1)| Zugriffe auf die Adjazenzlisten durchgeführt, um die Nachbarn der Knoten aus L(t − 1) zu bestimmen. Der Gesamtaufwand für Zeile 9 im Algorithmus beträgt also O(|V | + scan(|E|)) IO-Operationen. Das Entfernen der Duplikate in Zeile 10 kann wie folgt realisiert werden: Zuerst werden die Multimengen sortiert. Anschließend werden die Mengen gemäß der Sortierung durchlaufen und die Duplikate entfernt. Pro Durchlauf werden somit O(sort(|A(t)|) IO-Operationen durchgeführt. Der Gesamtaufwand im Algorithmus beträgt O(sort(|E|)) Zugriffe auf den Sekundärspeicher. 8 In Zeile 11 werden die Elemente aus A(t) entfernt, die bereits in L(t − 1) und L(t − 2) enthalten sind. Da alle diese Mengen sortiert sind, genügt ein Durchlauf durch A(t) mit gleichzeitigem Scannen von L(t − 1) und L(t − 2) für diese Aufgabe. Der Aufwand pro Durchlauf beträgt O(scan(|A(t)| + |L(t − 1)| + |L(t − 2)|)) IO-Operationen. Folglich benötigt der Algorithmus von Munagala und Ranade O(|V | + sort(|E|)) Zugriffe auf den externen Speicher. Jetzt muß noch der BFS-Baum konstruiert werden, was zusätzlich O(sort(|V | + |E|)) IOOperationen kostet [6]. Folglich benötigt die Breitensuche mit diesem Verfahren O(|V | + sort(|V | + |E|)) Zugriffe auf den Sekundärspeicher. 2 5 5.1 Externe Breitensuche nach Mehlhorn und Meyer Idee des Algorithmus Wie bereits mehrfach erwähnt, baut die hier vorzustellende Lösung auf der Idee von Munagala und Ranade auf, die im vorherigen Kapitel beschrieben wurde. Daher kann auch dieses Verfahren nur für die Breitensuche auf ungerichteten Graphen verwendet werden. Die grundlegende Idee ist die Unterteilung in zwei Phasen: In der ersten Phase (Partitionierungsphase) wird eine Datenstruktur aufgebaut, die den Zugriff auf die Adjazenzlisten beschleunigen soll. Wir erinnern uns: Der Standard-Algorithmus für die Breitensuche, welcher in Kapitel 3 wiederholt wurde, benötigt Θ(|V | + |E|) IO-Operationen wg. der Zugriffe auf die Adjazenzlisten und den Abfragen, ob ein Knoten bereits besucht wurde. Der Algorithmus von Munagala und Ranade löst bereits das zweite Problem, indem einzelne Ebenen des Graphens betrachtet werden. Die Beschleunigung der Zugriffe auf die Adjazenzlisten durch eine Datenstruktur ist also ein weiterer wichtiger Schritt auf dem Weg zu einem Verfahren, welches die externe Breitensuche mit möglichst wenig IO-Operationen löst. Daher wird in der zweiten Phase (BFS-Phase) die Breitensuche mit dem Algorithmus von Munagala und Ranade unter Zuhilfenahme der erwähnten Datenstruktur durchgeführt. Dies soll soweit zur Motivation der Idee genügen. Weitere Details folgen in den nächsten Abschnitten. 5.2 5.2.1 Partitionierungsphase Zieldarstellung und Verfahren In der Partitionierungsphase wird eine Datenstruktur aufgebaut, welche in der nachfolgenden BFSPhase die Zugriffe auf die Adjazenzlisten beschleunigen soll. Dazu wird der ungerichtete Graph in mehrere disjunkte zusammenhängende Teilgraphen Si , 0 ≤ i < K, zerlegt. Die Adjazenzlisten werden in der gleichen Weise partitioniert und in einer externen Datei F = F0 F1 . . . Fi . . . FK−1 gespeichert, wobei ein Fi die Adjazenzlisten für einen Teilgraphen Si aufnimmt. Mehlhorn und Meyer geben zwei verschiedene Vorgehensweisen zur Berechnung der Teilgraphen Si an: Randomisierte Partitionierung: Eine Menge von Master-Knoten (master nodes) wird per Zufall unabhängig aus den Knoten des Graphens bestimmt. Anschließend werden von allen Master-Knoten aus Breitensuchen gestartet. Die besuchten Knoten der einzelnen Breitensuchen entsprechen den Knoten der Teilgraphen Si . 9 Deterministische Partitionierung: Der Spannbaum Ts der Zusammenhangskomponente Cs , welche den Startknoten s der Breitensuche enthält, wird konstruiert. Danach wird eine EulerTour entlang Ts berechnet, welche eine Liste von Knoten liefert, wobei hier natürlich Knoten mehrmals auftauchen können. Diese Liste wird in mehrere Teile zerlegt und doppelte Knoten werden entfernt. Die Knoten in den einzelnen Listen entsprechen den Knoten der Teilgraphen Si . In dieser Ausarbeitung wird nur das randomisierte Verfahren behandelt, weil es anschaulicher und leichter zu verstehen ist. Auch Mehlhorn und Meyer geben diesem Verfahren in ihren Dokumenten [2] mehr Gewicht. Für weitere Informationen zum zweiten Verfahren sei auf die Literatur verwiesen. 5.2.2 Randomisierte Partitionierung p Die Master-Knoten si werden mit Wahrscheinlichkeit µ = min{1, scan(|V | + |E|)/|V |} unabhängig gewählt. Aus technischen Gründen, die später noch deutlich werden, wird vorausgesetzt, daß der Master-Knoten von S0 der Startknoten s der Breitensuche ist. Abbildung 5: Randomisierte Partitionierung auf einem Graphen (Ausschnitt): Knoten w wird sowohl von si als auch von sj beansprucht. Nun werden von allen Master-Knoten si aus Breitensuchen gestartet, welche im Prinzip parallel vorgehen: In jeder Runde nennt ein Master-Knoten alle noch unbesuchten Nachbarknoten seines aktuellen Teilgraphens Si . Wird ein Knoten nur von einem Master-Knoten si genannt, so wird dieser in den Teilgraphen Si aufgenommen. Falls mehrere Master-Knoten einen Knoten für sich beanspruchen, entscheidet der Zufall, welcher ihn tatsächlich erhält. Satz 5.1 (Randomisierte Partitionierung) Die randomisierte Partitionierung kann mit O(scan(|V |+|E|)/µ+sort(|V |+|E|)) IO-Operationen durchgeführt werden. Beweis: Es ist leicht einzusehen, daß die erwartete Anzahl der Master-Knoten K := O(µ · |V |) beträgt. Die erwartete Pfadlänge zwischen zwei Master-Knoten beträgt aufgrund der unabhängigen Wahl höchstens 1/µ. Daher kann die erwartete Pfadlänge zwischen zwei Knoten eines Teilgraphens Si höchstens 2/µ betragen. Man betrachte nun die Vorgehensweise während einer Runde: Sei Ri die Menge der tatsächlich erhaltenen Knoten für einen Master-Knoten si in der vorherigen Runde. Das Ermitteln der unbesuchten Nachbarknoten für die Teilgraphen Si kann durch Sortieren der Mengen Ri und Betrachten der Adjazenzlisten dieser Knoten durchgeführt werden. Damit läßt sich Pfolgern, daß die bei der Partitionierung betrachtete Datenmenge (in Elementen) durch X := O( vV 1/µ · (1 + degree(v))) = O((|V | + |E|)/µ) beschränkt werden kann. 10 Weiter kann die Anzahl der Knoten in den Mengen Ri und den Nachbarknoten, die während der gesamten Partitionierungsphase jeweils sortiert und durchlaufen werden müssen, durch Y := O(|V | + |E|) beschränkt werden. Folglich benötigt die Partitionierung O(scan(X)+sort(Y )) = O(scan(|V |+|E|)/µ+sort(|V |+ |E|)) IO-Operationen. 2 5.2.3 Aufbau der Datenstruktur Nach der Partitionierung liegt der ursprüngliche Graph als eine Menge von disjunkten und zusammenhängenden Teilgraphen Si vor. Nun muß noch die externe Datei F = F0 F1 . . . Fi . . . FK−1 erzeugt werden, wobei ein Fi die Adjazenzlisten für einen Teilgraphen Si enthalten soll. Um die vorliegenden Teilgraphen in dem gewünschten Format zu speichern, muß eine konstante Anzahl von Scan- und Sort-Operationen aufgewendet werden, wie sich leicht einsehen läßt. Einträge in die Fi sind von der Form (v, w, S(w), fS(w) ) und definieren eine Kante {v, w}, wobei w ein Knoten im Teilgraphen S(w) ist, dessen Datei FS(w) bei Position fS(w) in F beginnt. Die Einträge der einzelnen Fi werden lexikographisch sortiert. Man kann sich leicht klarmachen, daß die externe Datei F insgesamt O((|V | + |E|)/B) Blöcke auf der Platte bzw. den Platten belegt. 5.3 5.3.1 BFS-Phase Vorgehensweise In dieser Phase wird die eigentliche Breitensuche mit dem Algorithmus von Munagala und Ranade durchgeführt, wobei mit der erwähnten Datenstruktur die Zugriffe auf die Adjazenzlisten beschleunigt werden. Dazu wird eine sortierte externe Datei H (hot adjacency lists) verwendet, welche die aktuell benötigten Adjazenzlisten enthalten soll. Zu Beginn der Breitensuche wird H mit F0 initialisiert, welches alle Adjazenzlisten für den Teilgraphen S0 enthält, dessen Master-Knoten der Startknoten s der Breitensuche ist. Ein wesentliches Merkmal des Algorithmus von Munagala und Ranade ist das Berechnen von Mengen L(t) aus Mengen L(t − 1) und L(t − 2), um die BFS-Level der einzelnen Knoten zu erhalten. Dies kann mit Hilfe der externen Dateien H und F nun beschleunigt werden, weil auf die Adjazenzlisten der Knoten einer Ebene schneller zugegriffen werden kann: Anstatt Zugriffe auf einzelne Adjazenzlisten durchzuführen, kann ein Teil der benötigten Adjazenzlisten aus der Datei H mit sequentiellen Zugriffen gelesen werden. Genauer: Soll A(t) := N (L(t − 1)) berechnet werden, werden die Adjazenzlisten aller Knoten V1 ⊆ L(t−1), die sich zur Zeit in H befinden, eingelesen. Es kann natürlich passieren, daß nicht alle Adjazenzlisten, die zur Berechnung von A(t) erforderlich sind, auf diese Weise ermittelt werden können, weil die restlichen Adjazenzlisten der Knoten V2 := L(t − 1) \ V1 noch in den Dateien Fi gespeichert sind. Daher werden nun in einem zweiten Schritt die entsprechenden Dateien Fi ermittelt und eingelesen. Zur Beschleunigung weiterer Zugriffe werden die eingelesenen Adjazenzlisten sortiert und mit H gemischt. Insbesondere liegen jetzt aber alle Adjazenzlisten vor, die zur Berechnung von A(t) notwendig sind, so daß auch einer Berechnung der Menge L(t) analog zu Munagala und Ranade nichts mehr im Wege steht. Die berechneten Mengen L(·) werden sequentiell auf den Platten gespeichert und belegen folglich O(|V |/(D · B)) Blöcke. Dies soll soweit zur Schilderung der Vorgehensweise genügen. 11 Abbildung 6: Ein Snapshot des Algorithmus von Mehlhorn und Meyer [2] vor der Breitensuche auf einem Graphen (Ausschnitt). 5.3.2 Abschätzung der IO-Operationen Der folgende Satz liefert eine Abschätzung der IO-Operationen für die Breitensuche mit dem Verfahren von Mehlhorn und Meyer: Satz 5.2 (Breitensuche nach Mehlhorn und Meyer) p Die Breitensuche kann auf ungerichteten Graphen mit O( |V | · scan(|V | + |E|) + sort(|V | + |E|)) IO-Operationen durchgeführt werden. Beweis: Zur Berechnung einer Menge L(t) müssen die externe Datei H sowie die Mengen L(t − 1) und L(t − 2) konstant viele Male eingelesen werden. Dieses kann zusätzlich beschleunigt werden, indem die ersten D · B Blöcke dieser Mengen im internen Speicher untergebracht werden. Der eigentliche Aufwand verbirgt sich in der Aktualisierung der Datei H in jedem Schritt des Algorithmus: Es ist leicht einzusehen, daß eine Datei Fi höchstens einmal zu H hinzugefügt wird. Weiter ist bekannt, daß der kürzeste Weg zwischen zwei Knoten eines Teilgraphens Si höchstens 2/µ beträgt. Damit läßt sich folgern, daß eine Adjazenzliste höchstens O(1/µ) Mal verwendet wird, nachdem diese zu H hinzugefügt wurde. Der Aufwand für die Aktualisierung von H läßt sich folgendermaßen unterteilen: 1. Einlesen von H während des Mischens 2. Einlesen der Adjazenzlisten aus den Fi Aufgrund der Erklärungen läßt sich leicht folgern, daß die erste Aufgabe mit O(scan(|V | + |E|)/µ) IO-Operationen erledigt werden kann, denn eine Adjazenzliste wird höchstens O(1/µ) Mal verwendet, nachdem sie zu H hinzugefügt wurde. Bei der zweiten Aufgabe müssen die Fi eingelesen und sortiert werden. Dies läßt sich mit O(µ · |V | + sort(|V | + |E|) + 1/µ · scan(|V | + |E|)) IO-Operationen erreichen. p Wählt man nun µ = min{1, scan(|V | + |E|)/|V |} und nimmt die Abschätzung des Aufwands für die Randomisierte Partitionierung hinzu, so erhält man die Behauptung. 2 12 Ein analoges Resultat für die Abschätzung der IO-Operationen läßt sich auch für die deterministische Variante zeigen. Für weitere Informationen sei auf [2] verwiesen. In [2] erwähnen Mehlhorn und Meyer, daß p bei dünnen Graphen mit |E| = O(|V |) und realistischen Parametern im PDM-Modell der O( |V | · scan(|V | + |E|))-Term dominiert. In√diesem Fall übertrifft ihre Lösung den Algorithmus von Munagala und Ranade [3] um Faktor Ω( D · B). Bei dichten Graphen allerdings benötigen beide Verfahren O(sort(|V | + |E|)) IO-Operationen. 6 Zusammenfassung In dieser Ausarbeitung wurde gezeigt, daß die externe Breitensuche auf dünnen, ungerichteten Graphen mit sublinearem Aufwand für die IO-Operationen in Bezug auf die Anzahl der Knoten durchgeführt werden kann. Dazu wurde zuerst der Begriff des externen Algorithmus“ eingeführt und das PDM-Modell ” vorgestellt, mit dem der IO-Aufwand externer Algorithmen abgeschätzt werden kann. Als nächstes wurde ein interner Algorithmus für die Breitensuche wiederholt, der Laufzeit O(|V |+|E|) benötigt, wenn sich der Graph im schnellen Hauptspeicher befindet. Liegt der Graph aufgrund seiner Größe jedoch im langsamen Sekundärspeicher, werden im schlimmsten Fall Θ(|V | + |E|) IO-Operationen für die Breitensuche benötigt. Als Ursachen für dieses Verhalten wurden die Zugriffe auf die Adjazenzlisten sowie die Abfragen, ob ein Knoten bereits besucht wurde, identifiziert. Der daraufhin vorgestellte Algorithmus von Munagala und Ranade [3] berechnet zunächst die BFS-Level der einzelnen Knoten eines Graphens und konstruiert daraus den BFS-Baum. Durch diesen Ansatz werden die unstrukturierten Zugriffe, ob ein Knoten bereits besucht wurde, vermieden. Die Breitensuche benötigt mit diesem Algorithmus O(|V | + sort(|V | + |E|)) Zugriffe auf den Sekundärspeicher. Leider funktioniert dieser Ansatz nur auf ungerichteten Graphen. Nach diesen Vorbereitungen wurde die Lösung von Mehlhorn und Meyer [2] präsentiert, welche den Algorithmus von Munagala und Ranade verwendet. Die grundlegende Idee dieser Lösung ist die Partionierung des Graphens vor der eigentlichen Breitensuche und die Verwendung einer Datenstruktur für den Zugriff auf die Adjazenzlisten. Durch die Datenstruktur wird dieser Zugriff beschleunigt und damit die wesentliche Ursache für die hohe Anzahl an IO-Operationen interner Algorithmen für die p Breitensuche vermieden bzw. abgeschwächt. Die Breitensuche kann mit diesem Verfahren mit O( |V | · scan(|V | + |E|) + sort(|V | + |E|)) IO-Operationen durchgeführt werden. Damit liegt nun ein Verfahren vor, mit dem die Breitensuche auf ungerichteten Graphen auch effizient durchgeführt werden kann, falls ein Graph im Sekundärspeicher wie z.B. auf einer Festplatte gespeichert ist. Dieses Resultat ist angesichts des großen Einsatzbereiches der Breitensuche natürlich sehr bedeutend, auch wenn die Lösung zunächst von theoretischer Natur ist. Aber Mehlhorn und Meyer behaupten in der entsprechenden Literatur, daß ihre Lösung auch praktisch einsetzbar ist. Dies kann sicherlich vom Leser bestätigt werden, wenn man sich an dieser Stelle die Vorgehensweise des Verfahrens und insbesondere die Benutzung der neuen Datenstruktur noch einmal anschaut. Eine wirkliche Antwort auf die Frage nach der Bedeutung dieses Verfahrens für die Praxis wird aber erst die nahe Zukunft bringen. 13 Literatur [1] I. Katriel, U. Meyer. Elementary Graph Algorithms in External Memory. In Algorithms for Memory Hierarchies, noch nicht veröffentlicht. [2] K. Mehlhorn, U. Meyer. External-Memory Breadth-First Search with Sublinear I/O. In Proc. 10th Ann. European Symposium on Algorithms (ESA), LNCS, pp. 723-735. Springer, 2002. [3] K.V. Munagala, A. Ranade. I/O-Complexity of Graph Algorithms. In Proc. 10th Ann. Symposium on Discrete Algorithms, pp. 687-694. ACM-SIAM, 1999. [4] J. S. Vitter. External memory Algorithms and Data Structures: Dealing with Massive Data. In ACM Computing Surveys, Vol. 33, No. 2, June 2001, pp. 209-271. [5] T. H. Cormen, C. E. Leiserson, R. L. Rivest. Introduction to Algorithms. The MIT Press, Twenty-third printing, 1999. [6] A. Buchsbaum, M. Goldwasser, S. Venkatasubramanian, J. Westbrook. On external memory graph traversal. In Proc. 11th Ann. Symposium on discrete Algorithms, pp. 859-860. ACMSIAM, 2000. 14