RANGE TREES MBE, APRIL 2008 Zusammenfassung. Es wird eine Datenstruktur beschrieben (range tree), welche Bereichsabfragen für d-dimensionale Punktemengen in O(logd n + k) Zeit ermöglicht, wobei k die Anzahl ausgegebener Punkte sind. Die Datenstruktur kann in O(n logd−1 n) Zeit konstruiert werden und benötigt O(n logd−1 n) Speicher. Die Abfragezeit kann auf O(logd−1 n + k) verbessert werden, durch verwenden von fractional cascading. Das meiste hier ist sehr stark beeinflusst von [1] ab Seite 105, teilweise sogar 1:1. Da das Buch jedoch nicht allen leicht zugänglich ist, und zudem auf Englisch, ist dieses Dokument wohl nicht völlig sinnlos :-). Bei Unklarheiten, Verbesserungen etc. ungeniert ein E-Mail schreiben an [email protected]. 1. Motivation Seien n Punkte {p 1 , p 2 , . . . , p n } im dreidimensionalen Raum R3 gegeben durch x-, y- und z-Koordinate. (Für was ein solcher Punkt im realen Leben genau steht, soll uns im weiteren nicht interessieren, aber der Leser darf und soll gerne seiner Fantasie freien Lauf lassen.) Unser Ziel ist es Bereichsabfragen möglichst effizient durchzuführen. D.h. wir würden beispielsweise alle Punkte p i = (x i , y i , z i ) bestimmen wollen, wo 1.41 −53.58 23.84 ≤ ≤ ≤ xi yi zi ≤ 59.26 ≤ 97.93 ≤ 62.643 62.643 gilt. Geometrisch betrachtet wären das alle Punkte innerhalb des Quaders 23.84 [1.41, 59.26] × [−53.58, 97.93] × [23.84, 62.643]. 59.26 Ein erster leicht implementierbarer Ansatz wäre sicherlich für jeden der n Punkte zu testen, ob dieser im vorgegebenen Bereich enthalten ist, und falls ja, diesen zum Resultat hinzuzufügen. Somit hätten wir eine Laufzeit von O(n). Man kann sich leicht vergewissern, dass dies auch worst-case-optimal ist, denn falls unser Anfragebereich gross genug ist, werden alle n Punkte im Resultat enthalten sein. Und um n Punkte auszugeben benötigen wir linear viel Zeit. Ok.. und jetzt, fertig? Es kann ja scheinbar nichts besseres geben?? Die Datenstruktur, die wir jetzt ansehen werden, ermöglicht einen Abfragealgorithmus, der im worst-case auch lineare Laufzeit hat. Im Unterschied zum vorherigen Ansatz ist dieser jedoch output-sensitive. D.h. die Laufzeit ist nicht nur abhängig von der Grösse des Inputs (n), sondern auch von der Grösse des Resultats (k). Falls hier also k ≪ n, dann wird — wie wir im Folgenden sehen werden — unsere Laufzeit sublinear. 1.41 -53.58 97.93 2. Range Trees (2-Dimensional) Wir werden uns zu Beginn nicht auf den allgemeinen d-dimensionalen Fall stürzen, sondern die Funktionsweise anhand des d = 2 Falles vergegenwärtigen. (Danach ist die Verallgemeinerung nicht mehr schwierig.) Gegeben sei also eine Menge P von n Punkten in der Ebene, wo wir 2-dimensionale Bereichs-Suchabfragen ausführen wollen. Der range tree (wortwörtlich übersetzt “Bereichsbaum”) ist nun folgendermassen aufgebaut: ● Die Basis besteht aus einem balancierten binären Blattsuchbaum1 T , gebildet aus den x-Koordinaten der Punkte aus P. ● Jeder (innere oder äussere) Knoten v in T hat einen Zeiger auf einen anderen Blattsuchbaum Tassoc (v). Dieser besteht jeweils aus Kopien aller Blätter2 des Teilbaumes mit Wurzel v in T , diesmal jedoch aufgebaut nach y-Koordinaten. 1Ein Blattsuchbaum entspricht einem normalen (binären) Suchbaum, mit dem Unterschied, dass die Schlüssel in den Blättern gespeichert werden. In den inneren Knoten sind nur “Wegweiser” vorhanden. Beispielsweise könnte ein innerer Knoten den maximalen Schlüsselwert seines linken Teilbaums speichern. (Alles was grösser ist, wird somit im rechten Teilbaum sein). Wie auch immer, endet die Suche somit immer in einem Blatt, was hier essentiell ist! (siehe auch Abbildung rechts) 2Diese Blätter mit Wurzel v werden im weiteren als P(v) gekennzeichnet. 1 49 23 80 10 3 3 19 30 89 62 37 49 10 19 23 30 37 μ Blattsuchbaum 59 70 89 59 62 70 80 μ 100 100 105 Abbildung 1. Aufbau der range trees Wir nehmen an, dass keine der Punkte aus P = {p 1 , . . . , p n } dieselbe x- oder y- Koordinate haben3. Nun kann man den range tree folgendermassen rekursiv aufbauen, falls man als Input die Punkte P nach x-Koordinate sortiert erhält: Algorithm B UILD 2DR ANGE T REE(P) Input. A set P of points in the plane. Output. The root of a 2-dimensional range tree. 1. Construct the associated structure: Build a binary search tree Tassoc on the set Py of ycoordinates of the points in P. Store at the leaves of Tassoc not just the y-coordinate of the points in Py , but the points themselves. 2. if P contains only one point 3. then Create a leaf ν storing this point, and make Tassoc the associated structure of ν. 4. else Split P into two subsets; one subset Pleft contains the points with x-coordinate less than or equal to xmid , the median x-coordinate, and the other subset Pright contains the points with x-coordinate larger than xmid . 5. νleft ← B UILD 2DR ANGE T REE(Pleft ) 6. νright ← B UILD 2DR ANGE T REE(Pright ) 7. Create a node ν storing xmid , make νleft the left child of ν, make νright the right child of ν, and make Tassoc the associated structure of ν. 8. return ν p p p p Lemma. Ein 2-dimensionaler range tree benötigt O(n log n) Speicher. Beweis. Jedes p i ∈ P wird für eine gegebene Tiefe von T in genau einem referenzierten Tassoc (v) vorhanden sein. Da die Höhe logarithmisch beschränkt ist und der Basisbaum (mit den x-Koordinaten) nur linearen Speicher benötigt, folgt die behauptete Aussage daraus. So wie Build2dRangeTree geschrieben ist, wird die Konstruktionszeit nicht O(n log n) betragen. Dies liegt am Schritt 1, welcher schon für sich selber anfänglich O(n log n) Zeit kostet. Um die totale Zeit auf die geforderte Schranke hinunterzubringen, muss man die Punkte (in einer separaten Liste) auch schon nach der y-Koordinate sortiert haben, bevor man den Algorithmus aufruft. Danach kann der Schritt 1 in linearer Zeit ausgeführt werden. (Wie?) Die Datenstruktur ist nun aufgebaut und was folgt ist der eigentliche Abfragealgorithmus. Wir versuchen nun kurz eine Intuition zu bekommen, wie der Ablauf ist, und sehen uns danach den Pseudocode an. Nehmen wir an unser Abfragebereich wäre [x ∶ x ′ ] × [y ∶ y ′ ]. Im eindimensionalen Fall (vergessen wir einmal y) werden wir alle Knoten suchen, welche im gegeben x-Intervall liegen. Hierzu suchen wir von der Wurzel aus startend nach x und x ′ im Baum, bis sich die beiden Suchpfade an einem Knoten vsplit trennen. Von vsplit ’s linkem Kind aus suchen wir nach x. Dabei geben wir jedes Mal, wenn wir an einem Knoten v links abbiegen, alle Punkte des rechten Teilbaumes von v aus. Analog suchen wir von vsplit ’s rechtem Kind aus nach x ′ und geben jedes Mal, wenn wir an einem Knoten v rechts abbiegen, alle Punkte des linken Teilbaums von v aus. Schlussendlich überprüfen wir noch die beiden Blätter µ und µ′ , wo die beiden Pfade endeten, um festzustellen ob sie im gewünschten Bereich liegen. 3Diese Annahme kann implizit entfernt werden. Wie? 26 2 root(T) νsplit μ die ausgewählten Teilbäume μ F IND S PLIT N ODE(T, x, x ) Input. A tree T and two values x and x with x x . Output. The node ν where the paths to x and x split, or the leaf where both paths end. 1. ν ← root(T) 2. while ν is not a leaf and (x xν or x > xν ) 3. do if x xν 4. then ν ← lc(ν) 5. else ν ← rc(ν) 6. return ν Algorithm 1DR ANGE Q UERY(T, [x : x ]) Input. A binary search tree T and a range [x : x ]. Output. All points stored in T that lie in the range. 1. νsplit ←F IND S PLIT N ODE(T, x, x ) 2. if νsplit is a leaf 3. then Check if the point stored at νsplit must be reported. 4. else (∗ Follow the path to x and report the points in subtrees right of the path. ∗) 5. ν ← lc(νsplit ) 6. while ν is not a leaf 7. do if x xν 8. then R EPORT S UBTREE(rc(ν)) 9. ν ← lc(ν) 10. else ν ← rc(ν) 11. Check if the point stored at the leaf ν must be reported. 12. Similarly, follow the path to x , report the points in subtrees left of the path, and check if the point stored at the leaf where the path ends must be reported. Wie kommen jetzt die y-Koordinaten ins Spiel? — Erhöhen wir die Dimension des eben beschriebenen Algorithmuses. Statt jeweils alle Punkte des rechten bzw. linken Teilbaumes eines Knoten v auszugeben (Linie 8), rufen wir neu stattdessen die Prozedur 1DRangeQuery auf mit dem gesuchten y-Bereich! Wir erhalten somit folgenden (sehr leicht modifizierten) Algorithmus: Algorithm 2DR ANGE Q UERY(T, [x : x ] × [y : y ]) Input. A 2-dimensional range tree T and a range [x : x ] × [y : y ]. Output. All points in T that lie in the range. 1. νsplit ←F IND S PLIT N ODE(T, x, x ) 2. if νsplit is a leaf 3. then Check if the point stored at νsplit must be reported. 4. else (∗ Follow the path to x and call 1DR ANGE Q UERY on the subtrees right of the path. ∗) 5. ν ← lc(νsplit ) 6. while ν is not a leaf 7. do if x xν 8. then 1DR ANGE Q UERY(Tassoc (rc(ν)), [y : y ]) 9. ν ← lc(ν) 22 10. else ν ← rc(ν) 11. Check if the point stored at ν must be reported. 12. Similarly, follow the path from rc(νsplit ) to x , call 1DR ANGE Q UERY with the range [y : y ] on the associated structures of subtrees left of the path, and check if the point stored at the leaf where the path ends must be reported. Wie würde nun die Prozedur 3DRangeQuery aussehen? Wie 4DRangeQuery? dDRangeQuery? Lemma. Eine Bereichsabfrage eines 2-dimensionalen range trees mit n Punkten benötigt O(log2 n + k) Zeit, wobei k die Anzahl ausgegebener Punkte ist. 23 Beweis. Is left to the reader as an exercise :-) Fazit für 2-dimensionale range trees: Theorem. Sei P eine Menge von n Punkten in der Ebene. Ein range tree für P... ● benötigt O(n log n) Speicher, ● kann in O(n log n) konstruiert werden, ● und ermöglicht Bereichsabfragen in O(log2 n + k) Zeit, wobei k the Anzahl ausgegebener Punkte ist. N.B. durch benutzen von fractional cascading kann die Abfragezeit auf O(log n+k) verbessert werden. Sehr elegante Technik, wird aber an dieser Stelle übersprungen. (Siehe hierzu [1]) 3 3. Range Trees (d-Dimensional) Wie wir gesehen haben, ist die Idee zur Verallgemeinerung nun einigermassen klar (falls nicht, nochmals durchdenken!). Wir verzichten deshalb auf einen genaueren Beschrieb an dieser Stelle und geben die Resultate für den allgemeinen Fall an: Theorem. Sei P eine Menge von Punkten im d-dimensionalen Raum, wobei d ≥ 2. Ein range tree für P... ● benötigt O(n logd−1 n) Speicher, ● kann in O(n logd−1 n) konstruiert werden, ● und ermöglicht Bereichsabfragen in O(logd n + k) Zeit, wobei k the Anzahl ausgegebener Punkte ist. N.B. durch benutzen von fractional cascading kann hier wiederum die Abfragezeit auf O(logd−1 n + k) verbessert werden. Literatur [1] Mark de Berg, Otfried Cheong, Marc van Kreveld, and Mark Overmars. Computational Geometry: Algorithms and Applications. Springer-Verlag, third edition, March 2008. 4