Daten, Daten, Daten. Und immer an den Zugriff denken.

Werbung
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
Herunterladen