2.7 Datenstrukturen für Intervalle Grundmenge {1, . . . , n}. Intervalle sind Teilmengen vom Typ {i, . . . , j}, 1 ≤ i ≤ j ≤ n. Sie spielen eine besondere Rolle, z. B. bei Bildverarbeitung, Geometrie, Genomsequenzen in der Molekularbiologie, . . . Problemstellung: n fest vorgegeben, i → wi , „◦“ eine assoziative Verknüpfung wie „+“, „∗“, „min“, . . . hier o. B. d. A. „+“. Unterstützt werden sollen: query(i, j) → w(i, j) := wi + · · · + wj . update(i, a) → je nach Bedarf wi := a oder wi := wi + a. . – Seite 167/726 Zwei naive Lösungen – Array der Länge n für wi an Position i. Platz Θ(n), update Zeit O(1), query Zeit O(n), preprocessing Zeit (Vorbereitung der Datenstruktur) O(n). Die meisten Intervalle erfordern query Zeit Θ(n). – Matrix der Größe n × n für w(i, j) an Position (i, j), i ≤ j , 0 sonst. Platz Θ(n2 ), update Zeit Θ(n2 ), query Zeit O(1), preprocessing Zeit Θ(n2 ). Beachte: i ist in i · (n − i + 1) Intervallen enthalten. . – Seite 168/726 Segmentbäume (segment trees) [l, r] allgemein [1,20] [l, ⌊(l + r)/2⌋] [11,20] [1,10] [1,5] [1,3] [6,10] [4,5] [⌊(l + r)/2⌋ + 1, r] [6,8] [11,15] [9,10] [11,13] [16,20] [14,15] [16,18] [19,20] [1,2] [3,3] [4,4] [5,5] [6,7] [8,8] [9,9] [10,10] [11,12] [13,13] [14,14] [15,15] [16,17] [18,18] [19,19] [20,20] [1,1] [2,2] [6,6] [7,7] [11,11] [12,12] [16,16] [17,17] . – Seite 169/726 Level k : Abstand k von der Wurzel. (Erinnerung ⌈⌈n/2⌉/2⌉ = ⌈n/4⌉, ⌊⌊n/2⌋/2⌋ = ⌊n/4⌋, also ⌊n/4⌋ ≤ ⌈⌊n/2⌋/2⌉, ⌊⌈n/2⌉/2⌋ ≤ ⌈n/4⌉ ). Also Intervalle auf Level k haben ⌊n/2k ⌋ oder ⌈n/2k ⌉ Elemente. Falls ⌈n/2k ⌉ ≥ 2, wird das größte Intervall halbiert. Das größte k mit ⌈n/2k ⌉ ≥ 2 ist k = ⌈log n⌉ − 1. Also: Tiefe des Segmentbaums für [1, n] ist ⌈log n⌉. . – Seite 170/726 Binäre Bäume mit n Blättern haben n − 1 innere Knoten und somit 2n − 1 Knoten und 2n − 2 Kanten. Induktionsbeweis: n = 1 (klar). n − 1 → n: Sei T binärer Baum mit n Blättern, v und w Geschwisterblätter. u v w Entferne v und w → u wird Blatt → n − 1 Blätter. Rest hat (Indvss.): n − 2 innere Knoten, also hat T n − 1 innere Knoten, n Blätter, zusammen 2n − 1 Knoten und 2n − 2 Kanten, da Ingrad 1 für alle Knoten mit Ausnahme der Wurzel. Speicherplatz O(n). . – Seite 171/726 Preprocessing – Baue Baum ohne w(i, j)-Werte top-down, z. B. Inorder. – Speichere wi am Blatt [i, i]. – Berechne alle w(i, j) mit Postorder-Traversierung. O(n) . – Seite 172/726 Update (i, a) – Starte an der Wurzel und suche das Blatt [i, i], O(log n) speichere Weg auf einem Stack. – Ändere den Wert an [i, i]. O(1) – Gehe den Weg rückwärts und berechne die Werte auf diesem Weg neu (alle anderen Werte bleiben aktuell). O(log n) O(log n) . – Seite 173/726 query (i, j) 1. Ansatz [k, l] w1 v1 ′ v2 w2 v2 w3 v3 w3′ ′ ′ v4 v4 w 4 w 4 v5 ′ w5′ w5 v6 v6 w6 [i, i] [j, j] Suche Wege zu [i, i] und [j, j], speichere die Wege ab dem Gabelungspunkt [k, l]. . – Seite 174/726 Gabelungspunkt gehöre zu Intervall [k, l]. Stets gilt: [i, j] ⊆ [k, l]. [k, l] v1 . . . . . . . [i, i] v2 Alle Intervalle [a, b] in T (v1 ) haben die Eigenschaft b ≤ j . Falls zusätzlich i ≤ a, können sie für w(i, j) benutzt werden. [j, j] T (v1 ) Weg zu [i, i] geht nach links → Verwende Informationen an w. u w v u v w Weg zu [i, i] geht nach rechts → Verwende Informationen an v nicht. . – Seite 175/726 Verwende schließlich die Information an [i, i]. Auf dem Weg zu [j, j] spiegelbildlich analog. Jedes wm mit i ≤ m ≤ j wird genau einmal verwendet. Wege zu [i, i] und [j, j] enthalten zusammen max. 2⌈log n⌉ + 1 Knoten. Pro Ebene wird auf jedem Weg maximal ein Extraknoten betrachtet. Arbeit pro Knoten: O(1), also insgesamt O(log n). . – Seite 176/726 Ansatz 2 Ansatz 1 + Verbesserung: Ist an der Gabelung [k, l] = [i, j], verwende Information dort und stoppe. Wird in T (v1 ) ein Intervall [a, b] mit a = i erreicht, verwende Information dort und stoppe. . – Seite 177/726 query(i, j): Aktuell erreichter Knoten gehöre zu Intervall (k, l). Starte mit (k, l) := (1, n). Phase 1: Fall 1 j ≤ ⌊(k + l)/2⌋, Gabelungspunkt noch nicht erreicht, nur links suchen, l := ⌊(k + l)/2⌋. Fall 2 i ≥ ⌊(k + l)/2⌋ + 1, Gabelungspunkt noch nicht erreicht, nur rechts suchen, k := ⌊(k + l)/2⌋ + 1. Fall 3 (k, l) = (i, j). Ausgabewert steht an diesem Knoten. STOP. Fall 4 NOT(Fall 1 OR Fall 2 OR Fall 3), d. h. i ≤ ⌊(k + l)/2⌋ < j , (k, l) 6= (i, j) Gabelungspunkt erreicht. Starte Phase 2 mit (k, ⌊(k + l)/2⌋) und Phase 3 mit (⌊(k + l)/2⌋ + 1, l). . – Seite 178/726 Phase 2: Stets gilt i ≤ k ≤ l ≤ j . Fall 1 i ≥ ⌊(k + l)/2⌋ + 1, alle Informationen dieses Teilbaumes im rechten Teilbaum, k := ⌊(k + l)/2⌋ + 1. Fall 2 i = k , benutze w(k, l) für Gesamtergebnis, STOP. Fall 3 i > k , aber nicht Fall 1, benutze alle Informationen im rechten Teilbaum, also w(⌊(k + l)/2⌋ + 1, l), für das Gesamtergebnis und suche Restinformationen im linken Teilbaum, l := (⌊(k + l)/2⌋). Phase 3: Spiegelbildlich analog. . – Seite 179/726 Zusammenfassung alle wi Speicherplatz Θ(n) Preprocessing Θ(n) update(i, a) Θ(1) query(i, j) O(n) alle Intervalle Segmentbaum Θ(n2 ) Θ(n) Θ(n2 ) Θ(n) O(n2 ) Θ(log n) Θ(1) O(log n) . – Seite 180/726 2.8 Datenstrukturen für Partitionen Grundmenge {1, . . . , n}. Zu jedem Zeitpunkt gehört i genau einer Teilmenge an, einer Firma, einer Zusammenhangskomponente, . . . Es gibt aber Fusionen. Zu unterstützende Operationen: FIND(i) → Name der Teilmenge, in der sich i zu diesem Zeitpunkt befindet. UNION(A,B) → Vereinigung der Mengen A und B; Name der Vereinigungsmenge frei wählbar, solange die Namen aktueller Mengen verschieden sind, A und B werden eliminiert. . – Seite 181/726 Zwei naive Lösungen – Array der n Elemente, an Position i der Name der Menge, die i enthält. Speicherplatz O(n), Preprocessing O(n), FIND O(1), UNION O(n). Sequenz von m FIND- und n − 1 UNION-Befehlen O(n2 + m). . – Seite 182/726 – Array für n Listenanfänge, die die aktuellen Elemente der Menge enthalten, zusätzlich size-Wert für jede Liste, zu Beginn: Menge i enthält genau i, UNION: kleinere Menge auflösen, Liste durchlaufen und Elemente in Liste der größeren Menge einfügen. size-Werte addieren. Speicherplatz O(n), Preprocessing O(n), FIND O(n) UNION O(min(size(A), size(B))). Sequenz von m FIND- und n − 1 UNION-Befehlen O(nm + Zeit für UNION). . – Seite 183/726 Datenstrukturen mit besonders schnellen FINDs Kombiniere die beiden naiven Datenstrukturen 3 1 7 3 3 1 7 3 1 1 3 7 Array mit FIND(i) 4 0 5 0 0 0 3 0 0 0 0 0 Array der Mengen mit size-Variable Speicherplatz O(n) 2 nil 5 nil nil nil 12 nil nil nil nil nil 6 4 3 10 11 7 9 1 nil nil 8 Preprocessing O(n) FIND O(1) nil . – Seite 184/726 UNION(A, B) – Teste, ob size(A) ≤ size(B). (Im Folgenden nur der Fall „Ja“, der andere Fall analog.) – Durchlaufe L(A): für alle i: FIND(i) := B , entferne i aus L(A), füge i in L(B) ein. – size(B) = size(A) + size(B), size(A) := 0. O(size(A)) m FIND-Befehle und n − 1 UNION-Befehle: O(m + Kosten UNIONs}) | {z O(n2 ) bessere Abschätzung? . – Seite 185/726 1 2 3 Kosten nur O(n) 4 .. . n n-1 n=2k 1 2 3 4 ...... balancierter Baum Gesamtkosten → n/2 UNION, Kosten 1 → n/4 UNION, Kosten 2 → n/2 → n/2 → n/2i UNION, Kosten 2i−1 → n/2 → n/2k UNION, Kosten 2k−1 = n/2 → n/2 (n/2) · log n = O(n log n). . – Seite 186/726 Kosten der UNION-Befehle O(n log n) Seien A1 , . . . , An−1 die jeweils kleineren Mengen der UNION-Befehle → Kosten |A1 | + · · · + |An−1 |, Anzahl der Summanden: n − 1, Größe der Summanden: mind. 1, max. n/2, schwer abzuschätzen. Ausweg: Buchhaltermethode Buche die Kosten auf andere Kostenträger um, addiere dann. Neue Kostenträger: 1, . . . , n. i-ter UNION-Befehl mit Kosten |Ai | gibt je eine Kosteneinheit an j ∈ Ai . . – Seite 187/726 Wie viele Kosten kann ein Kostenträger k maximal erhalten? Kosteneinheit an k → k bei Vereinigung in der kleineren Menge → die neue Menge ist mindestens doppelt so groß → bei s Kosteneinheiten enthält die zugehörige Menge am Ende mindestens 2s Elemente → 2s ≤ n → s ≤ ⌊log n⌋ → Gesamtkosten ≤ n · ⌊log n⌋. . – Seite 188/726 Datenstruktur für besonders schnelle UNIONs Mengen sind Bäume mit Zeigern auf Elter. Mengenname und -größe Array E (Elter), indiziert mit Elementnummern 1, . . . , n: Für i Verweis auf Elter; falls nil: Mengenname und -größe. Array M (Menge), indiziert mit Mengennamen aus 1, . . . , n: Für j entweder nil für ungültig, oder auf den Knoten, der der Wurzel der Menge entspricht. . – Seite 189/726 Initialisierung: i in Menge i, d. h. E(i) = (nil, i, 1) M (i) = i Mengenname Nummer der Wurzel der Menge i Mengengröße Speicherplatz O(n) Preprocessing Zeit O(n) UNION(i, j ) – M (i) und M (j) berechnen, – sei E(M (i)) = (nil, i, s1 ), E(M (j)) = (nil, j, s2 ), – falls s1 ≤ s2 : E(i) := j , M (i) := nil E(j) := (nil, j, s1 + s2 ), M (j) := j sonst analog. O(1) . – Seite 190/726 FIND(i) – Starte an i und suche über die Elterzeiger die Wurzel, wo der Mengenname steht. O(Tiefe des Baumes) Lemma: Bei n Daten haben die Bäume maximal Tiefe ⌊log n⌋. Beweis: Behauptung: Tiefe d ⇒ mind. 2d Knoten. Bei Tiefe ⌊log n⌋ + 1 wären das mindestens 2⌊log n⌋+1 > n Knoten, Widerspruch. Beweis der Behauptung durch Induktion über d. d = 0 : Tiefe 0 → 1 Knoten X . – Seite 191/726 d − 1 → d: Sei T ein Baum mit Tiefe d und kleinster Knotenzahl. Wie entstand T ? Also size(T1 ) ≤ size(T2 ) < size(T1 )+ size(T2 ) T1 T2 = size(T ). T depth(T1 ) ≤ d − 1, da depth(T ) = d. Falls depth(T1) < d − 1, dann depth(T2 ) = d und T2 hat Tiefe d und weniger Knoten als T , Widerspruch. Somit depth(T1 ) = d − 1 und (Indvor.) size(T1 ) ≥ 2d−1 . Auch size(T2 ) ≥ size(T1 ) ≥ 2d−1 und size(T ) = size(T1 )+ size(T2 ) ≥ 2d−1 + 2d−1 = 2d . . – Seite 192/726 Effizienzsteigerung durch Pfadkomprimierung (path compression) Etwas mehr Aufwand bei FIND-Operationen als nötig, um spätere FIND-Operationen zu beschleunigen. FIND(i) – Starte an i und suche über den Elterzeiger die Wurzel, wo der Mengenname steht. Speichere alle dabei gefundenen Knoten (z. B. Stack). – Wähle für alle Knoten auf dem Suchpfad die Wurzel als neuen Elter. . – Seite 193/726 v0 v0 v1 v1 T0 T0 v2 T1 T1 vk = x v2 T2 Tk vk = x T2 Tk Aufwand pro Knoten in etwa verdoppelt, aber Suchwege für viele Knoten verkürzt. Analyse? Schwierig. . – Seite 194/726 Funktion Zweierturm: Z(0) := 1, Z(i) := 2Z(i−1) . Z(1) = 2, Z(2) = 4, Z(3) = 16, Z(4) = 65536, Z(5) = 265536 . (Für jede Zahl z mit realem Bezug zu Anzahlen von Objekten, Zeitschritten, ... gilt z ≤ Z(5).) Funktion Log-Stern: log∗ n = min{k|Z(k) ≥ n}. z. B. log∗ 2000 = 4, log∗ 105 = 5, log∗ 10100 = 5. Also: limn→∞ log∗ n = ∞, aber log∗ n wächst ganz, ganz langsam. Satz Zeit für m FIND- und n − 1 UNION-Operationen O((n + m) log∗ n). . – Seite 195/726