ML - belfrage.net

Werbung
Eidgenössische
Technische Hochschule
Zürich
Ecole polytechnique fédérale de Zurich
Politecnico federale di Zurigo
Federal Institute of Technology at Zurich
Institut für Theoretische Informatik
Peter Widmayer
Michael Gatto
Datenstrukturen & Algorithmen
Lösungen 7
SS 07
Aufgabe 7.1:
Der Algorithmus folgt dem Scan-Line Prinzip. Man betrachte eine Gerade aus der Lichtquelle hinaus. Der
Lichtstrahl belichtet dann einen Punkt auf dem ersten Segment, das von der Lichtquelle weg entlang der
Gerade getroffen wird. Die anderen Segmente, die getroffen werden, sind Kandidaten für eine zukünftige
Belichtung: Endet nämlich das jetzige belichtete Segment, so wird das nächste (falls immer noch vorhanden)
belichtet. Analog beim Starten eines neuen Segmentes. Kreuzen sich das erste und das zweite Segment auf
dem Lichtstrahl, und es beginnt kein weiteres Segment dazwischen, so ist jetzt das zweite Segment belichtet.
Aufgrund dieser Erkenntnisse lässt sich das Scan-line Prinzip anwenden. Die Idee ist, dass man die Scan-line
nicht von links nach rechts durchlaufen lässt, sondern radial, mit Zentrum in der Lichtquelle. Analog zur
Berechnung der existierenden Schnittpunkte in einer Menge beliebig orientierter Liniensegmente sind die
interessanten Stellen die Anfangs- und Endpunkte der Segmente, sowie die Kreuzungspunkte der Segmente.
In all diesen Punkten ändert sich die Nachbarschaft einiger Segmente auf der Scan-Line L. Dazu sortiert
man alle Anfangs- und Endpunkte der Segmente bezüglich des Winkels, den sie mit der Lichtquelle bezüglich
einer fixen Richtung (z.B. horizontal) bilden. Dies benötigt O(n log(n)) Zeit.
Die Operationen bei den Haltepunkten sind, analog wie für das Kreuzungsproblem:
Ein Segment a startet:
• a in L einfügen.
• Prüfen, ob a das oberste Element ist. Falls ja, ist a jetzt das belichtete Segment, und das Segment, das
vorher belichtet war, ist bis zum Kreuzungspunkt mit der aktuellen Scan-Line belichtet.
• Neue Nachbarschaft prüfen: Kreuzt a den Vorgänger oder den Nachfolger in L? Falls ja, in die Liste
der Haltepunkte Punkte aufnehmen. Aufwand: 2 Schnittprüfungen.
Ein Segment a endet:
• a aus L entfernen.
• Prüfen, ob sich das oberste Element auf der Scan-Line geändert hat (a war belichtet). Falls ja, so
beginnt die Belichtung des jetzt obersten Segment ab dem Kreuzpunkt mit der Scan-line L.
• Prüfe, ob die ursprünglichen Nachbarn von a sich kreuzen. Falls ja, und die Kreuzung nicht schon mal
berechnet wurde, so wird sie in der Liste der Haltepunkte aufgenommen. Aufwand: 1 Schnittprüfung.
Zwei Segmente kreuzen:
• Tausche die Reihenfolge der sich kreuzenden Segmente in L.
• Falls eine der zwei Geraden belichtet war, so wird jetzt die andere belichtet.
• Prüfe die Nachbarschaft der zwei getauschten Segmente auf Kreuzungen, und nehme die Kreuzungspunkte (falls solche existieren und nicht schon vorhanden sind) in die Liste der Haltepunkte auf.
Aufwand: 2 Schnittprüfungen.
Den Start- und Endpunkt der Belichtung eines Segments bezüglich einer Scan-Line berechnet man, indem
man den Kreuzungspunkt zwischen der Geradengleichung, die das Segment definiert, und der Geradengleichung der Scan-Line berechnet. Dies erfolgt in konstanter Zeit.
Für L benutzen wir einen AVL-Suchbaum, wo die Segmente in den Blättern gespeichert sind, und die Blätter
mittels einer doppelt verkettete Liste verbunden sind. Um die Position eines Elementes zu bestimmen, wertet
man die in einem Knoten des Suchbaumes gespeicherte Geradengleichung am Schnittpunkt mit der Scan-Line
aus. Somit ist eine Ordnung der Segmente auf der Scan-Line definiert, und man kann suchen, einfügen und
löschen. Bei einem Kreuzpunkt werden zwei benachbarte Geraden vertauscht. Dies kann effizient durchgeführt
werden, indem man nach einer Gerade sucht, und mittels der verketteten Liste den Nachbarn (d.h. den
Nachfolger oder den Vorgänger) sucht. Der Baum braucht O(n) Speicher, das Suchen, Einfügen und Löschen
erfolgt aufgrund des AVL-Baumes in O(log n) Zeit.
Die Haltepunkte müssen auch gespeichert werden, z.B. in einem Minimum Heap, mit Schlüssel den Winkel
der Geraden zwischen Lichtquelle und Haltepunkt benutzt. Existieren k Kreuzungspunkte, so braucht diese
Struktur O(n + k) zusätzlichen Speicher. Einfügen und Löschen erfolgen in O(log(n + k)) Zeit.
Der Algorithmus hat im Ganzen die Laufzeit O((n + k) log(n + k)). Um jeden Haltepunkt abzuarbeiten wird
je O(log(n)) Zeit benötigt, und es gibt 2n + k Haltepunkte. Jeder Haltepunkt muss in den Heap eingefügt
und daraus entfernt werden. Jede solche Operation kostet O(log(n + k)) Zeit, und es gibt O(n + k) davon.
Aufgabe 7.2:
Die Aufgabe löst man, indem man die Studenten zuerst aufsteigend nach Notenschnitt sortiert, und bei
gleichem Notenschnitt absteigend nach der Anzahl freier Stunden. Danach geht man die sortierte Liste der
Reihe nach durch und merkt sich jeweils das Minimum der Anzahl freier Stunden. Jedes mal, wenn man
einen Studenten trifft, der eine niedrigere oder gleiche Anzahl freier Stunden hat wie das aktuelle Minimum,
so handelt es sich um einen unzufriedenen Studenten: Es gibt keinen anderen Studenten, der sowohl einen
kleineren Schnitt hat als auch eine niedrigere Anzahl freier Stunden.
Das Sortieren kostet O(n log(n)) Zeit, der lineare Scan O(n) Zeit. Der Algorithmus hat deshalb eine Laufzeit
von O(n log n).
In der Erweiterung sortiert man die Studenten wie oben beschrieben. Danach führt man einen Scan durch
die sortierte Liste. Die Scanline führt einen AVL-Baum mit sich, der die Studenten enthält, als Schlüssel
die Anzahl freier Stunden verwendet und zusätzlich in jedem Knoten die Anzahl Knoten im Teilbaum
speichert. Dies erlaubt, ähnlich wie in der Aufgabe 3 des 5. Übungsblattes, effizient die Anzahl der Schlüssel
zu berechnen, die kleiner als der Knoten selbst sind. Beim Treffen eines neuen Studenten mit der Scanline
fügt man ihn in den AVL-Baum ein; gleichzeitig zum Einfügen, berechnet man die Anzahl Knoten im Baum,
die einen kleineren Schlüssel haben (in unserem Beispiel die Anzahl freier Stunden). All diese Knoten haben
sowohl eine kleinere Anzahl freier Stunden (gemäss Gestaltung des AVL-Baumes) als auch einen kleineren
Schnitt, da sie früher in den AVL-Baum eingefügt wurden. Ist diese Anzahl kleiner als k, so gibt man den
Student aus, und weiter geht’s mit der Scanline. Dieses Verfahren hat eine Laufzeit von O(n log(n)): wie
oben benötigt man O(n log(n)) Zeit um die Knoten zu sortieren; weiter verursacht das Einfügen und das
Berechnen der Anzahl kleineren Knoten im AVL-Baum bei jedem der n Haltepunkten der Scanline O(log(n))
Zeit, was insgesamt wieder zu O(n log(n)) führt.
Alternativ kann man sich während dem Scan die k Studenten mit bisher niedrigste Anzahl an freier Stunden
in einem Maximum-Heap merken. Beim treffen eines Studenten mit einer kleineren Anzahl an freier Stunden
als das oberste Element im Heap, so handelt es sich offensichtlich um einen unzufriedenen Student. Wir
entfernen das bisherige Maximum aus dem Heap, und fügen das getroffene Element ein. Dieses Verfahren
führt zu einer Laufzeit von O(n log n + n log k) = O(n log n), wobei man O(n log n) Zeit zum sortieren
benötigt und jeweils O(log k) zum Einfügen und Entfernen eines Elements im Heap.
Aufgabe 7.3:
Wir beschreiben einen rekursiven Aufbau eines Range-trees auf der Menge von d-dimensionalen Punkten P ,
wobei wir jeden Punkt als (xd , · · · , x2 , x1 ) repräsentieren. Zuerst erstellen wir d Arrays, in denen die Punkte
P jeweils nach einer anderen Koordinate sortiert sind: im ersten nach xd , im zweiten nach xd−1 , im letzten
nach x1 . Dies kostet O(dn log(n)) Zeit.
Danach bauen wir den Range-tree rekursiv auf. Jeder rekursive Aufruf zur Erstellung eines d-dimensionalen
Baums auf n Knoten bekommt beim Aufruf die oben definierten d sortierten Arrays, die sich jeweils genau
auf die n einzufügende Punkte beschränken.
2
Die Hauptidee ist, einen balancierten Baum der Dimension d zu erstellen, der als Schlüsseln die Koordinate
xd verwendet. Die Punkte werden in den Blättern gespeichert. Wenn man die Knoten sortiert hat, so kann
dieser rekursiv effizient erstellt werden. In jedem Knoten k dieses Baums wird einen balancierten Range-tree
der Dimension d − 1 gespeichert. Der Baum der Dimension d − 1 enthält k selbst und die Punkte, die im
d-dimensionale Baum unter k gespeichert sind, mit der d − 1 Koordinate als Schlüssel. Dies erfolgt rekursiv
weiter bis zur Dimension 1.
Der Pseudocode zum effizienten Aufbau des Range trees sieht wie folgt aus.
Algorithmus recursive_Tree(d,P , AP
d )
Input: d: Dimension der Punkte
P : Punktmenge zum Bilden des range-trees
AP
d : d Arrays mit Punkten P jeweils nach einer Koordinate sortiert.
Output: Range-Tree Td der Dimension d.
m
1.) Finde den Punkt m = {xm
d , · · · , x1 }, der Median bezüglich der Koordinate xd in P .
2.) Teile alle d Arrays in zwei sortierte Arrays, wobei für jedes Teil-Array gilt:
die eine Hälfte enthält die Punktmenge P ≤ = {p ∈ P : xpd ≤ xm
d } mit kleineren oder gleichen
xd -Koordinate wie m, die zweite die Punkte mit P > = {p ∈ P : xpd > xm
d }.
3.) Bilde den Baum rekursiv auf, auf die Knoten mit kleineren oder gleichen d Koordinate:
≤
Tleft := recursive_Tree(d,P ≤ , AP
)
d
4.) Bilde den Baum rekursiv auf, auf die Knoten mit grösseren d Koordinate:
>
Tright := recursive_Tree(d,P > , AP
d )
5.) Bilde den Baum zur Dimension d − 1 mit der gesamten Knotenmenge rekursiv auf:
Td−1 := recursive_Tree(d − 1,P , AP
d−1 )
.
6.) Bilde einen Knoten mit Schlüssel xm
d
7.) Hänge die zwei Teilbäume Tleft und Tright als linken bzw rechten Sohn an den Knoten.
8.) Setzte Td−1 als Range-tree der Dimension d − 1 des Knotens, und gib den Baum mit Wurzel m zurück.
Sei T (n, d) den Zeitaufwand um einen d-dimensionalen Range-tree zu bauen.
Wir betrachten zuerst die Laufzeit T (n, 1) zum Aufbauen eines 1-dimensionalen range-tree mit n Knoten. Da
das Array sortiert ist, erfolgt die Berechnung des Medians in konstanter Zeit durch eine Indexberechnung. Die
Aufteilung (Schritt 2) eines eindimensionalen sortierten Array kann in O(1) Zeit durchgeführt werden durch
das neue Setzen der Indizes im Array. Schritt 5 wird nicht ausgeführt, da wir die kleinste Dimension erreicht
haben. Schritte 6 bis 8 erfolgen (auch für Dimension > 1) in konstanter Zeit. Bis auf die rekursiven Aufrufe
erfolgt jeder Rekursionsaufruf in konstanter Zeit. Da jeder Knoten genau einmal als Median auftaucht,
werden n rekursive Aufrufe ausgeführt, was zu einer Laufzeit von O(n) führt.
Im d-Dimensionalen Fall erfolgt bis auf die Schritte 2 und 5 auch alles in Zeit O(n) für den gesamten Aufbau
der d-ten Dimension. Es bleibt abzuschätzen, wieviel Schritt 2 kostet und wieviele rekursive Aufrufe von
Schritt 5 gemacht werden.
Der Aufteilungsschritt 2 erfolgt im d-dimensionalen Fall in O(dn log(n)) Zeit: für jede Tiefe des d-dimensionalen
Baums braucht man insgesamt O(dn) Zeit, um die d − 1 Arrays mittels eines linearen Durchlaufs in zwei
sortierte Arrays zu teilen (um genau zu sein, kostet die Aufteilung jedes der d − 1 Arrays in der Wurzel des
Baumes n vergleiche, bei Tiefe 1 kostet es 2(d − 1) (n−1)
2 , da man für die zwei Söhne der Wurzel insgesamt d
Arrays mit n − 1 Knoten aufteilen muss, und so weiter. Man beachte, dass das sortierte Array zur Dimension
d in O(1) aufgeteilt werden kann, da man lediglich die Indizes des Arrays auf den relevanten Teil anpassen
muss).
Wir schätzen nun noch die Rekursionen auf Dimension d − 1 ab. Es erfolgt eine Rekursion mit n Knoten
(für die Wurzel), zwei mit höchstens n2 Knoten (die Söhne der Wurzel), vier mit höchstens n4 Knoten, und
so weiter.
Wenn wir (zu unserem Ungunsten) die Anzahl Knoten auf die nächstgrössere Zweierpotenz N , so ist der
gesamte Aufwand:
3
(
Plog N
O(dN log(N )) + i=0 2log N −i T (2i , d − 1),
T (N, d) =
O(N ),
d>1
.
d=1
Diese Rekursion führt unter der Annahme d < logd−2 (n) zu einer Laufzeit von O(N logd−1 (N )) = O(n logd−1 (n)).
Die Laufzeit für das Sortieren am Anfang ist durch diese Annahme berücksichtigt. Weiter wird unter dieser
Annahme auch die Zeit für die Aufteilung der Arrays berücksichtigt.
Wir verzichten auf einen formalen Beweis für diese Laufzeit, da die Verwendung der O-Notation in der
Rekursionsformel einen sauberen Beweis unmöglich macht.
Aufgabe 7.4:
Wir erweitern einen d-dimensionalen Range-tree und nehmen an, wir suchen nach dem Bereich [xa1 , xb1 ], · · · ,
[xad , xbd ], wobei xai < xbi , ∀i ∈ {1, · · · , d}.
Zur Erinnerung: In einem normalen Range-tree erfolgt die Suche nach den Knoten im obigen Bereich, indem
man zuerst im Suchbaum zur (ersten) Koordinate x1 nach dem Knoten v sucht, wo sich die Suchpfade nach
xa1 und xb1 trennen. Danach verfolgt man die Suche nach xa1 im linken Teilbaum von v, und jedesmal, wenn
man in der Suche nach links geht, gibt man das Resultat der Suche nach dem Bereich [xa2 , xb2 ], · · · , [xad , xbd ] im
(d − 1)-dimensionalen Range-tree des rechten Bruders aus. Wenn man im eindimensionalen Fall ist, so gibt
es im rechten Bruder keinen range tree, und man gibt die Punkte zurück, die in diesem Teilbaum gespeichert
sind. Für xb1 geht man symmetrisch vor.
Wir erweitern nun den d-dimensionale Range-tree, indem wir in jedem Knoten des 1-dimensionalen Rangetrees zur Koordinate xd zusätzlich die Anzahl Knoten in diesem Teilbaum speichern.
Um die Anfrage nach der Anzahl Punkte zu beantworten, erfolgt die Suche nach dem Bereich wie gewohnt. Anstatt jedoch die Knoten im Bereich auszugeben, gibt man nur deren Anzahl aus, und summiert
jeweils im d-dimensionalen Baum die Resultate der Suche auf allen besuchten d − 1-dimensionalen Bäumen.
Im eindimensionalen Baum wird jeweils anstatt der Knoten im Bereich die Summe der relevanten Zähler
zurückgegeben.
Diese Anfrage kann deshalb in O(logd (n)) Zeit beantwortet werden, und die Erweiterung des Range-trees
braucht immer noch O(n logd−1 (n)) Speicher, da man im 1-dimensionalen Baum konstant viel Speicher pro
Knoten braucht.
4
Herunterladen