4.3 d-Bäume
Das ist der Inhalt von Praktikumsaufgabe 3. Im Folgenden (2d-Baum). Höhere Dimensionen gehen genauso.
Idee (Folie 10)
- Die Ebene so aufteilen wie ein binärer Suchbaum eine Zahlengerade aufteilt.
- Als Schlüssel abwechselnd die - und die -Koordinate verwenden.
- Jeder Knoten entspricht einem der Punkte. Der Punkt teilt die Zelle des Knotens durch eine achsenparallele Gerade in zwei Hälften.
Die Folien zählen Ebenen ab 1 (level = depth + 1) und nehmen auf geraden Ebenen , auf ungeraden . Die Wurzel (Ebene 1) teilt also nach . Das passt zu Algorithmus 1 und 2, die mit vertical (= vergleichen) starten. De Berg et al. beginnen mit . Für Laufzeit und Korrektheit ist das egal, man sollte es nur konsistent machen. In der Demo unten kann man beides umschalten.
Eigenschaften (Folie 12)
Für einen Knoten mit Punkt , der nach teilt:
- linker Teilbaum = Punkte links von ,
- rechter Teilbaum = Punkte rechts von ,
- die Kinder teilen dann nach .
Für einen Knoten mit Punkt , der nach teilt:
- linker Teilbaum = Punkte unterhalb von ,
- rechter Teilbaum = Punkte oberhalb von ,
- die Kinder teilen dann nach .
Jeder Knoten gehört also zu einem achsenparallelen Rechteck (seiner Zelle / Region), und die Zellen der Kinder zerlegen die Zelle des Elternknotens.
Interaktiv: Aufbau und Bereichssuche
Die farbigen Linien sind die Teilungsgeraden, je dicker, desto näher an der Wurzel. Darunter steht der Baum. Ziehe die blauen Ecken des Anfragebereichs und gehe die Suche Schritt für Schritt durch.
Bereichssuche (Folien 13–14)
Wie die Suche im binären Suchbaum, mit zwei Unterschieden:
- In jedem Knoten wird je nach Ebene die - oder die -Koordinate verglichen.
- Bei einer Bereichssuche muss man oft beide Teilbäume besuchen: links, wenn in der Teilungskoordinate unter den Schlüssel reicht, rechts, wenn darüber reicht.
Algorithmus 1: RangeSearch(Knoten k, Richtung d, Bereich D)
1: if (k ≠ NULL) {
2: if (d == vertical) { // y vergleichen
3: (l, r) = (D.y1, D.y2);
4: coord = k.y;
5: dNew = horizontal;
6: } else { // x vergleichen
7: (l, r) = (D.x1, D.x2);
8: coord = k.x;
9: dNew = vertical;
10: }
11: if (k ∈ D) Füge k zur Ausgabe hinzu;
12: if (l < coord) RangeSearch(k.left, dNew, D);
13: if (r > coord) RangeSearch(k.right, dNew, D);
14: }
Aufruf: RangeSearch(root, vertical, D)
Mit den strikten Vergleichen in Zeilen 12–13 gehen Punkte verloren, wenn ein Punkt im linken Teilbaum dieselbe Koordinate hat wie der Knoten und genau auf dem Rand von liegt. Die Folien schließen das durch die Annahme verschiedener Koordinaten aus. In der Implementierung (Abgabe 3) sicherer: l <= coord und r >= coord. Die Demo macht es so.
Balancierter Aufbau am Median (Folien 16–19)
Problem: Ist der Baum unbalanciert, kann ein Pfad zur linearen Liste entarten, und die Suche wird linear.
Lösung: Immer am Median teilen. Dann haben beide Teilbäume (fast) gleich viele Punkte, und die Höhe ist .
Vorgehen:
- Alle Punkte einmal nach und nach sortieren: zwei Folgen und .
- am Median teilen, der Median wird die Wurzel. Das ergibt (darunter) und (darüber).
- so in , aufteilen, dass dieselben Punkte wie enthält und dieselben wie (stabil, die -Sortierung bleibt erhalten).
- Rekursiv weiter, jetzt am Median von bzw. , usw., bis nur noch ein Punkt übrig ist (Blätter).
Algorithmus 2: ConstructBalanced2DTree(l, r, k, d)
// global vorsortierte Folgen X und Y
1: if (l ≤ r) {
2: m = ⌊(l + r) / 2⌋;
3: if (d == vertical) {
4: k.value = Y[m];
5: PartitionField(X, l, r, m); // X[l..r] passend zu Y[l..m-1], Y[m+1..r] aufteilen
6: } else {
7: k.value = X[m];
8: PartitionField(Y, l, r, m);
9: }
10: ConstructBalanced2DTree(l, m-1, k.left, !d);
11: ConstructBalanced2DTree(m+1, r, k.right, !d);
12: }
Aufruf: ConstructBalanced2DTree(1, n, root, vertical)
PartitionField ist ein stabiles Aufteilen in : Man läuft durch und schreibt jeden Punkt nach links oder rechts, je nachdem, auf welcher Seite des Medians er in der anderen Koordinate liegt.
Statt zwei vorsortierter Listen kann man in jedem Schritt std::nth_element nach der aktuellen Koordinate verwenden. Das findet den Median in erwartet , Gesamtaufwand dann ebenfalls .
Laufzeit (Folien 20–21)
Aufbau
| Teil | Aufwand |
|---|---|
| Sortieren von und | |
| Aufteilen pro Rekursionsebene | |
| Rekursion | |
| Lösung (Master-Theorem, Fall 2) |
Speicher: , ein Knoten pro Punkt.
Bereichssuche:
Auf den Folien „Beweis: Tafel“. Hier ausgeführt (nach de Berg et al.):
1. Zerlegung der Kosten. Besuchte Knoten sind von drei Arten:
- Knoten, deren Zelle ganz in liegt: Ihr ganzer Teilbaum wird ausgegeben. Kosten zusammen .
- Knoten, deren Zelle gar nicht schneidet: werden nicht besucht (der Elternknoten schneidet sie weg).
- Knoten, deren Zelle vom Rand von geschnitten wird. Nur diese sind „teuer“.
2. Wie viele Zellen schneidet eine senkrechte Gerade? Sei die Zahl der Zellen, die eine senkrechte Gerade in einem 2d-Baum mit Punkten schneidet. Nach zwei Ebenen (einmal , einmal ) ist die Zelle in vier Teile mit je etwa Punkten zerlegt. Die -Teilung trennt sie in links und rechts, liegt nur auf einer Seite. Die -Teilung trennt oben und unten, schneidet beide. Also trifft höchstens zwei der vier Enkel:
Master-Theorem mit , : , also .
3. Der Rand von besteht aus vier achsenparallelen Strecken, jede schneidet höchstens Zellen.
Ergebnis: .
Für kleines ist das viel schneller als alle Punkte zu testen ( statt ), aber deutlich langsamer als .
Höhere Dimensionen (Folie 21)
Derselbe Algorithmus: Beim Aufbau zyklisch durch die Dimensionen gehen (), bei der Suche ebenso.
| d-Baum | |
|---|---|
| Aufbau | |
| Bereichssuche | |
| Speicher |
Für ergibt . Für großes geht gegen , der kd-Baum hilft dann kaum noch (Fluch der Dimensionen).
Nächster-Nachbar-Suche (Ergänzung)
Der häufigste praktische Einsatz von kd-Bäumen ist nicht die Rechteckanfrage, sondern die Nächste-Nachbarn-Suche (z.B. ICP, Punktwolken, k-NN in ML):
- Absteigen wie bei der Punktsuche bis zum Blatt, das den Anfragepunkt enthalten würde. Der Punkt dort ist der erste Kandidat mit Abstand .
- Beim Rücklauf in jedem Knoten prüfen, ob der Kreis um mit Radius die andere Teilungsseite schneidet (Abstand zur Teilungsgeraden ). Nur dann auch den anderen Teilbaum durchsuchen.
Bei gleichmäßig verteilten Punkten ist das im Mittel .