Effiziente Algorithmen und Komplexitätstheorie Vorlesung Thomas Jansen 03.07.2006 1 zur Implementierung von Kompressionsalgorithmen Suffix-Bäume als Implementierung des Wörterbuchs Suffix-Baum = kompaktierter Suffix-Trie Suffix-Trie = Trie aller Suffixe Trie = Baum, Kanten mit Buchstaben beschriftet ∀ Knoten ∀ ausgehende Kanten: Beschriftungen verschieden, mit Knoten w Konkatenation der Kantenbeschriftungen w assoziiert Suffix-Tries im Worst Case quadratische Größe, darum Suffix-Bäume 2 Suffix-Bäume I Kanten mit Zeichenketten ∈ Σ+ beschriftet I (Trie: Beschriftungen verschieden) Anfangsbuchstaben verschieden) I für Speicherplatz: statt Zeichenkette xi · · · xj nur Indizes (i, j) an Kanten schreiben I Größe ≤ 2n I Ziel Konstruktion in linearer Zeit auf linearem Platz I dafür (schon bei Suffix-Tries) Suffix-Links I Suffix-Link(aW ) = Zeiger auf W I für Algorithmus künstliche Wurzel ⊥ mit Suffix-Link(ε)=⊥ (Baum: effiziente Konstruktion von Suffix-Bäumen Ukkonen (1995): On-line construction of suffix trees 3 Begriffe für den Algorithmus von Ukkonen I X = x1 x2 · · · xn ∈ Σn = Zeichenkette, für die Suffix-Baum konstruiert wird I Ti = Suffix-Trie für x1 x2 · · · xi Tei = Suffix-Baum für x1 x2 · · · xi Phase i = Konstruktion von Tei aus Tg i−1 I I I Endknoten = sl , so dass ∀sj mit j < l xi -Kante angehängt werden muss I aktiver Knoten = sk , so dass ∀sj mit j < k sj Blatt ist I Änderungen nur zwischen Endknoten und aktivem Knoten erforderlich Knoten in Ti explizit, wenn auch in Tei vorhanden, sonst I implizit 4 Referenzen I (xj xj+1 · · · xk−1 , (k, p)) Referenz von xj xj+1 · · · xp , wenn xj xj+1 · · · xk−1 ∈ Tei , p ≤ i I Referenzen nicht eindeutig I Referenz heißt kanonisch, wenn p − k minimal I kanonische Referenzen nicht eindeutig I (xj xj+1 · · · xk−1 , (k, ∞)) offene Referenz von xj xj+1 · · · xn , wenn xj xj+1 · · · xk−1 ∈ Tei I Kanten zu Blättern mit offenen Referenzen beschriftet brauchen keine Änderungen 5 Vorbereitung des Algorithmus von Ukkonnen Lemma 217 e Sei X = x1 x2 · · · xn ∈ Σn , Tg i−1 Suffix-Baum für x1 · · · xi−1 , Ti Suffix-Baum für x1 · · · xi für i ∈ {1, . . . , n}. Ist (s, (k, i − 1)) eine Referenz in Tg i−1 auf den Endknoten, dann ist (s, (k, i)) eine Referenz in Tei auf den aktiven Knoten. hilft aktiven Knoten für nächste Phase zu finden Lemma 218 Sei X = x1 x2 · · · xn ∈ Σn , sei aW ∈ X mit a ∈ Σ, sei aW innerer Knoten im Suffix-Baum T für X . W ist innerer Knoten in T . garantiert Existenz der Suffix-Links 6 Der Algorithmus von Ukkonen KonstruiereSuffixBaum(X = x1 · · · xn ∈ Σn ) S 1. T := {⊥, ε, v }, {(ε, v )} ∪ {(⊥, ε)} x∈Σ 2. 3. 4. 5. 6. sufLink(ε) := ⊥ s := ε k := 2 Für i ∈ {2, 3, . . . , n} (s, k) := Update(s, (k, i − 1), i) Beobachtungen f1 konstruiert I initial T I (s, (k, i − 1)) ist immer aktiver Knoten in Tg i−1 I Korrektheit und Laufzeit hängt ganz von Update ab 7 Zentrale Prozedur Update Eingabe Referenz (s, (k, p)) des aktiven Knotens, Phase i Ausgabe Endknoten in Tg i−1 1. w := ε 2. (s, k) := Kanonisiere(s, (k, p)) 3. (fertig, r ) := TesteUndTeile(s, (k, p), xi ) 4. So lange nicht fertig 5. Erzeuge neuen Knoten m, Kante (r , m) mit Beschriftung (i, ∞) 6. Falls w 6= ε 7. suflink(w ) := r 8. w := r 9. (s, k) := Kanonisiere(suflink(s), (k, p)) 10. (fertig, r ) := TesteUndTeile(s, (k, p), xi ) 11. Falls w 6= ε 12. suflink(w ) := s 13. Ausgabe (s, k) 8 Hilfsprozedur TesteUndTeile Eingabe Referenz (s, (k, p)) des aktiven Knotens, neues Zeichen x Ausgabe falsch“ und Knoten, an den xi angehängt werden muss ” oder wahr“ ” 1. Falls |xk xk+1 · · · xp | = 0 2. Falls x-Kante aus s existiert 3. Dann Ausgabe (wahr, s). STOP 4. Sonst Ausgabe (falsch, s). STOP 5. Sonst w 6. Sei e Kante s → s 0 mit w1 = xk . 7. Falls x = w|xk xk+1 ···xp |+1 8. Dann Ausgabe (wahr, s). STOP 9. Sonst w 10. Teile e = s → s 0 wie folgt aus: w1 w2 ···w|x 11. ···xp | w|x ···xp |+1 ···w|w | s −→k m k −→ Ausgabe (falsch, m) s 0. 9 Beispiel Algrithmus von Ukkonen Σ = {A, B}, X = ABABBAA ⊥ A, B ε B B BAA A BBAA BABBAA BA A A BBAA A A A B AB ABBAA ABABBAA AA BAA ! KonstruiereSuffixBaum(X = ABABBAA, n = 7) ABBAA1. T := {⊥, ε, v}, {(ε, v)} ∪ S {(⊥, ε)} x∈Σ 2. 3. 4. 5. 6. fertig n=7 s = suflink(ε) := ⊥ s := ε k := 2 Für i ∈ {2, 3, . . . , n} –(s, k) := Update(s, (k, i − 1), i ) k =6 i =7 10 Laufzeit des Algorithmus von Ukkonen Wir wollen zeigen lineare Laufzeit bei geeigneter Implementierung klar Σ ist konstant Beobachtungen I Laufzeit proportional zur Anzahl betrachteter Knoten I es genügt, Knoten in Kanonisiere und TesteUndTeile zu zählen I k wird nirgendwo gesenkt 11 Laufzeit durch Kanonisiere Beobachtung jeder Aufruf von Kanonisiere von TesteUndTeile-Aufruf gefolgt also Kanonisiere-Aufrufe müssen nicht gezählt werden Beobachtung in Kanonisiere für jeden weiteren Knoten wird k erhöht Erinnerung k sinkt nie und steigt nicht über n + 1 also Rechenzeit in Kanonisiere = O(Rechenzeit in TesteUndTeile + n) 12 Laufzeit durch TesteUndTeile unterscheide Aufrufe mit Ergebnis wahr und falsch wahr-Aufrufe lassen Update enden beim Verlassen von Update endet Phase i also Aufwand durch wahr-Aufrufe von TesteUndTeile = O(n) Beobachtung in falsch-Aufruf von TesteUndTeile wächst Suffix-Baum Erinnerung Suffix-Baum hat Größe ≤ 2n also Aufwand durch falsch-Aufrufe von TesteUndTeile = O(n) also insgesamt Rechnzeit und Speicherplatz O(n) 13 Zur Implementierung von Suffix-Bäumen unsere Überlegungen setzen voraus I Platz je Knoten O(1) I Zeit je Zugriff auf xi -Kind O(1) Wie realisiert man das? Implementierung mit Arrays I Platz je Knoten O(|Σ|) = O(1) I Zugriff je xi -Kind O(1) + schnell − O(|Σ|) je Knoten sehr groß 14 Nicht-triviale Implementierung von Suffix-Bäumen Implementierung mit linearen Listen I Platz je Knoten O(|Kinder|) = O(1) I Zugriff je xi -Kind O(|Kinder|) = O(1) + wesentlich platzeffizienter − langsam Implementierung mit balancierten Bäumen (z. B. AVL-Bäume) I Platz je Knoten O(|Kinder|) = O(1) I Zugriff je xi -Kind O(log |Kinder|) = O(1) + wesentlich platzeffizienter − etwas langsamer als mit Arrays 15 Nicht-triviale Implementierung von Suffix-Bäumen (2) Idee Verwalte Kinder aller Knoten in einem Array, Größe O(n) Zugriff auf xi -Kind des Knotens v durch Hashfunktion h : V × Σ → N0 klar |V | · |Σ| = Θ(n · |Σ|) Hashtabelle also Hashing-typische Probleme mit Kollisionen Anmerkung mit vernünftiger Hashing-Implementierung Zeit O(1) bei Platz O(n) mit W’keit dicht bei 1 machbar Anmerkung in Suffix-Bäumen meist große Verzweigungsgrade nur nah bei der Wurzel naheliegend hybride Implementierung mit Arrays wurzelnah, sonst nicht-triviale Implementierung 16