Effiziente Algorithmen Suchen in Zeichenfolgen: Teil 1 Martin Gruber Gabriele Koller 1 1 Exakte Suche in Strings (Exact String Matching Problem) Geg.: Muster P der Länge n, String S der Länge m (n<m), endliches Alphabet Ges.: alle Vorkommen von P in S Bsp.: P = ana, T = bananas Vielzahl von Anwendungen: Textverarbeitung, Information Retrieval, Datenbanksuche in der Bioinformatik, ... 2 2 Ansätze zum Pattern Matching Naiver Algorithmus - O(m·n): praktisch proportional zu m+n Lineare Algorithmen - O(m+n): • 1976 Knuth, Morris und Pratt • 1976 Boyer, Moore • 1980 Karp, Rabin Suche nach vielen Patterns im selben String? 3 Der Aufwand bei den genannten Algorithmen verteilt sich üblicherweise wie folgt: O(n) für das Preprocessing des Patterns und O(m) für die Suche nach dem Pattern im String, egal wie viele Patterns in ein und demselben String gesucht werden. Sollen in einem (langen) String viele Patterns gesucht werden, dann wäre eine effizientere Suche wünschenswert, die nicht mehr von der Stringlänge abhängt. 3 Substring Problem Geg.: (langer) String S der Länge m, endliches Alphabet Ziel: Vorverarbeitung des Strings in O(m) Zeit; danach soll für beliebige Patterns P der Länge n (n<m) in O(n) Zeit bestimmt werden, ob P in T Suchaufwand unabhängig von der Textlänge! Bsp.: T = bananas P1 = na, P2 = ana, P3 = ast, ... 4 Substring-Problem: Für eine Textfolge der Länge m soll nach einer Vorverarbeitungszeit von O(m) für beliebige Patterns P der Länge n (mit n<m) in O(n) Zeit bestimmt werden, ob das Pattern P im Text T enthalten ist. D.h. im Gegensatz zu den bisher bekannten linearen Algorithmen zur Stringsuche, deren Suchaufwand sowohl von der Text- als auch von der Patternlänge abhängt, wird hier gefordert, dass der Suchaufwand unabhängig von der Textlänge ist. Ein beliebiges endliches Alphabet kann vorgegeben werden. Eine alternative Zielsetzung kann auch hier sein, alle Vorkommen von P in T zu finden. 4 Suffixe eines Strings Suffixe für S = bananas: Sortiert: S1 = S2 = S3 = S4 = S5 = S6 = S7 = S2 = S4 = S6 = S1 = S3 = S5 = S7 = bananas ananas nanas anas nas as s Sortieren ananas anas as bananas nanas nas s P1 = na, P2 = ana, P3 = ast, ... 5 Ein Ansatz zur Lösung des Substring-Problems beruht auf Zerlegung des Strings in alle seine Suffixe, d.h. alle Teilstrings S[i..n] für alle i = 1 bis n. Suffix Si = Suffix, das an der Position i in S beginnt. Wenn man die Suffixe effizient verwaltet (z.B. sortiert), kann man darin schnell nach einem Pattern suchen. Frage: welche Datenstruktur eignet sich am besten zur Verwaltung aller Suffixe? 5 Suffix-Trie • Suchbaum für m Suffixe • Einzelne Buchstaben als Kanteninformation • Gleiche Anfänge (bzw. Fortsetzungen) nur einmal gespeichert • (Lange) Kantenketten ohne Verzweigung • 1 Schritt pro Zeichen a n a s 2 • Suchaufwand O(n)? • Erstellungsaufwand O(m)? n s s a 6 n n s s a a 5 4 n s a n b a a 7 3 s bananas 1 6 Wir können einen Trie verwenden, um die Suffixe zu speichern. Die Knoteninformationen an den Blättern zeigen an, welche Suffixe dort gespeichert sind. Die Suche nach einem Pattern erfolgt in O(n). Dieses Kriterium wird also erfüllt. Das Einfügen eines Suffixes Si in den Suffix-Trie für den String S benötigt m-i+1 Operationen: für S1 m Schritte, S2 m-1 Schritte, ..., für Sm 1 Schritt. Damit benötigt der Aufbau des kompletten Suffix-Tries i (für i = 1, ... m) Operationen und hat damit einen Aufwand von O(m²). Das entspricht noch nicht unserer Forderung. 6 Suffix-Tree • Suffix-Tree T für String S der Länge m enthält alle Suffixe S1, ..., Sm • Ableitung aus Trie: Kantenfolgen ohne Verzweigung zu einer Kante zusammenfassen • Kanteninformation: (längere) Folge von Zeichen • Im Blatt für Suffix Si wird Index i gespeichert a s s n a 6 s n a s 4 b a n a n a s n a 7 s n a s 5 3 2 bananas 1 7 Eine bessere Möglichkeit bietet der Suffix-Tree. Der Suffix-Tree für die Zeichenfolge S kann aus dem Trie, der alle Suffixe von S enthält, abgeleitet werden, indem alle Kantenfolgen ohne Verzweigung zu einer Kante zusammengefasst werden. Als Kanteninformation wird die entsprechende Zeichenfolge (ein Substring von S) gespeichert - im Gegensatz zum Trie, wo nur ein Zeichen gespeichert wird. 7 Suffix-Tree - Definition • Suffix-Tree T für einen String S der Länge m ist ein Baum mit m Blättern (von 1 bis m nummeriert) • Jeder innere Knoten (außer der Wurzel) hat mindestens 2 Nachfolger • Jede Kante ist mit einem nicht-leeren Substring von S beschriftet • Keine zwei Kanten eines Knotens haben Kantenbeschriftungen mit gleichem Anfangszeichen • Konkatenation der Kantenbeschriftungen von der Wurzel bis zu einem Blatt i entspricht Suffix Si =S[i..m], für alle i = 1, ..., m 8 8 Suffix als Präfix eines Suffixes • Suffix-Tree für S = banana • Manche Suffixe enden nicht in einem Blatt, sondern mitten auf einer Kante • Hat weniger als m Blätter • Problem: Suffix ist Präfix eines anderen Suffixes (letztes Zeichen von S kommt mehrfach in S vor) • Bsp.: „na“ von „nana“ a 6 n a 4 n a b a n a n a n a 5 n a 3 2 banana 1 9 9 Suffix-Tree mit Schlusszeichen • Lösung des Präfix-Problems: Schlusszeichen, das nicht im String S vorkommt, z.B. $ a $ $ • Impliziter Suffix-Tree: ohne Schlusszeichen (für manche Zwecke ausreichend) n a 6 $ n a $ 4 b a n a n a $ n a 7 $ n a $ 5 3 2 banana$ 1 10 10 Suffix-Tree - Speicherbedarf • • • • m Blätter < m innere Knoten < 2m Kanten < 2m Kanteninformationen (Anfangs- und Endindex des Substrings in S, damit O(1) Speicherplatz pro Kante) Speicherbedarf für T für m Suffixe: O(m) a2:2 s7:7 n 3:4 a 66 s7:7 n a 5:7 s 44 s7:7 b a n a n 1:7 a s n a3:4 77 s7:7 n 5:7 a s 55 33 22 11 bananas bananas 1234567 11 Der Speicherbedarf ist wegen der vielen Kanteninformationen sehr groß. Ein einfacher Trick hilft hier aber: Der String S muss ja irgendwo gespeichert sein. Jede Kanteninformation ist ein Substring von S, nämlich vom p-ten bis q-ten Zeichen. Wenn man nun statt des Substrings lediglich die Indizes p und q einträgt, ist der Speicherbedarf des gesamten Suffix-Trees O(m). 11 Suffix-Tree - Eigenschaft E1 Substring: • Für ein Pattern P gibt es in T einen Weg von der Wurzel aus P ist Substring von S • P ist Präfix eines Suffixes a s n a 6 s Beispiele: P = „ana“ ist Präfix von S2=„ananas“ und S4=„anas“, P = „ast“ ist kein Präfix eines Suffixes s n a s 4 b a n a n a s n a 7 s n a s 5 3 2 bananas 1 12 Für ein Pattern P, bestehend aus n Zeichen, gibt es im Suffix-Tree für S mit oder ohne Schlusszeichen genau dann einen Pfad von der Wurzel aus (d.h. P ist im Suffix-Tree enthalten), wenn P Substring von S ist. Das ist leicht einzusehen: P sei Substring und beginne mit dem r-ten Zeichen; dann ist P auch Präfix des Suffixes Sr und daher gibt es den zugehörigen Pfad. Der Pfad für P muss nicht unbedingt bis zu einem Blatt führen, sondern kann auch auf einer Kante oder in einem inneren Knoten enden. Anmerkung: Das Schlusszeichen ist beim Suffix-Baum für S = „bananas“ nicht notwendig, da „s“ ohnehin nicht mehrfach im String S vorkommt. 12 Suffix-Tree - Eigenschaft E2 Anzahl und Lage von Substrings: • Wenn P in S mehrmals (k-mal) vorkommt, dann hat der durch das Ende des Pfades für P n festgelegte Unterbaum k a Blätter • Indizes dieser Blätter geben n Vorkommen von P in S an as • gilt nicht im impliziten T 2 Bsp.: P = „ana“ in S2=„ananas“ und S4=„anas“ a s s 6 s 4 b a n a n a s n a 7 s n a s 5 3 bananas 1 13 Wenn ein Pattern P im String S mehrmals (k-mal) vorkommt, dann hat der durch das Ende des Pfades für P im Suffix-Tree mit Schlusszeichen festgelegte Unterbaum k Blätter. Die Indizes dieser Blätter geben an, wo die Vorkommen von P beginnen. Als Schlusszeichen fungiert hier wiederum das „s“. Bei Verwendung des impliziten Suffix-Trees (ohne Schlusszeichen) ist diese Eigenschaft gestört, weil manche Suffixe im Inneren des Baumes enden und kein eigenes Blatt haben. 13 Suchen im Suffix-Tree Ist Pattern P vorhanden? = Ist P Substring von S? Beginne in Wurzel von T; i 1; Wiederhole: Mit i-tem Zeichen von P verzweigen; i i +1; Kante verfolgen und Kanteninfo prüfen, dabei bei jedem überprüften Zeichen: i i +1; Bis Ende von P erreicht oder keine passende Fortsetzung von P in T mehr möglich Aufwand: O(n) - proportional zur Patternlänge 14 Die beiden Eigenschaften ermöglichen eine einfache Suche im Suffix-Tree. Beispiel: Suchen nach „ana“ im Suffix-Tree für „banana“. Bei der Suche soll geprüft werden, ob ein Pattern P im Suffix-Tree T enthalten ist. Es müssen genau n Schritte (Zeichenvergleiche) durchgeführt werden, wenn das Pattern enthalten ist. Die Anzahl der Verzweigungen ist n* (Anzahl der Knoten auf dem Pfad, n* n). Ist P nicht enthalten, wird die Schleife schon früher beendet. Diese Suche ist in expliziten und impliziten Suffix-Trees sinnvoll. 14 Schnelles Suchen im Suffix-Tree (1) Voraussetzung: Pattern ist garantiert vorhanden • In je einem Schritt von Knoten zu Knoten • Kanteninfo kann übersprungen werden • Zeichentiefe d des erreichten Knotens ist entscheidend für Weitergehen oder Ende Aufwand: O(n*) n* = Anzahl der Knoten auf dem Suchweg 15 Skip-Count: Wenn für ein Pattern P bereits bekannt ist, dass es garantiert im Suchwort enthalten ist, kann das Prüfen der gesamten Kanteninformation entfallen. Lediglich das erste Zeichen muss zwecks Verzweigung betrachtet werden. Man erkennt an der Zeichentiefe d des Nachfolgeknotens, ob das Ende des Patterns auf der letzten durchlaufenen Kante oder im erreichten Knoten liegt, oder ob man von diesem Knoten aus noch weiter gehen muss: d = n: Ende des Patterns in diesem Knoten d > n: Ende des Patterns liegt um d-n Zeichen davor auf der Kante d < n: Weiterverzweigen Der Aufwand reduziert sich dadurch auf O(n*), wobei n* die Anzahl der Knoten auf dem Pfad für P ist. 15 Schnelles Suchen im Suffix-Tree (2) Suche für das in S vorhandene Pattern P alle Vorkommen Wieviele gibt es? Wo beginnen sie? Algorithmus: schnell, alle Kanteninfos ignorieren! • P bis an sein Ende verfolgen • von erreichtem Knoten aus zu allen k Blättern des Unterbaums (max. 2k Kanten) Aufwand: O(n* + k) k = Anzahl der Vorkommen von P 16 Der erste Teil dieser Suchaufgabe besteht darin, das Ende des Patterns zu finden. Man kann wiederum die Kanteninformationen überspringen. Im zweiten Teil (Wieviele Vorkommen gibt es und wo beginnen sie?) muss vom erreichten Ende von P zu allen, von dort aus erreichbaren Blättern weitergegangen werden. Das kann wiederum durch Überspringen der Kanteninformationen erfolgen. Bei k Vorkommen kommt man zu k Blättern, dazu gibt es maximal 2k Kanten und man benötigt auch nur ebensoviele Verzweigungen. In diesem Fall folgt man der Reihe nach allen Verzweigungen. Der Aufwand ist also O(n*+k). Hier sollte man noch betonen, dass die Suchzeit nur von der Länge des Patterns P (z.B. ein Wort) abhängt, aber unabhängig von der Größe des Strings S (vielleicht ein ganzes Buch) ist. Das ist natürlich ein ausgezeichnetes Ergebnis, wenn viele verschiedene Patterns in ein und demselben sehr langen String gesucht werden sollen. In weiterer Folge werden wir sehen, dass man den Suffix-Tree mit einem Aufwand konstruieren kann, der nur linear mit der Länge des Strings S wächst. 16 Naiver Algorithmus zum Aufbau von T Idee: Suffixe nacheinander zu T hinzufügen Erzeuge Kante für S1 inkl. $ und Blatt mit Beschriftung 1 Für alle Suffixe Si mit i = 2 bis m: Finde längsten übereinstimmenden Pfad für Si Am Ende dieses Pfades: Füge falls nötig Knoten in die Kante ein, erzeuge neue Kante und neues Blatt mit Beschriftung i Aufwand linear in O(m)? 17 Die Idee für einen naiven Algorithmus zum Aufbau eines Suffix-Trees T besteht darin, die Suffixe nacheinander zu T hinzuzufügen. Wir fügen also zuerst das Suffix ab Position 1 (d.h. den gesamten String) in T ein. Danach suchen wir für alle weiteren Suffixe den längsten übereinstimmenden Pfad und fügen am Ende dieses Pfades einen Knoten in die Kante ein, erzeugen eine neue Kante und ein neues Blatt. Dabei versehen wir jedes eingefügte Blatt mit dem Index des eingefügten Suffixes und alle Kanten mit den Anfangs- und Endindizes der entsprechenden Substrings in S. Dieser Algorithmus ist einfach und übersichtlich, hat aber leider einen quadratischen Aufwand: es müssen m Suffixe eingetragen werden. Für das Eintragen des Suffixes i sind bis zu i Schritte (Vergleiche bzw. Verzweigungen) nötig. Wie kommen wir also zu einem Konstruktionsalgorithmus mit linearem Aufwand? 17 Suffix Trees in O(m) Zeit erstellen Weiner (1973): Linear Pattern Matching Algorithms, Proc. of the 14th IEEE Symp. on Switching and Automata Theory „The algorithm of 1973“ (Knuth) McCreight (1976): A Space-Economical Suffix Tree Construction Algorithm, J. ACM 23 Lange Zeit wenig bekannt, weil „extremely difficult to understand“ (Gusfield) Ukkonen (1995): On-Line Construction of SuffixTrees, Algorithmica 14 Viel einfacher (Gusfield), wird hier erklärt 18 18 Einfachster Aufbau-Algorithmus Ti: impliziter Suffix-Tree für die i ersten Zeichen von S, enthält also alle Suffixe des Strings S[1..i] Konstruiere Baum T1 Phase i Für i 2 bis m: // für jedes Zeichen Für j 1 bis i: // für alle bisherigen Teil-Suffixe Verlängere den Pfad für S[j..i-1] um S[i] // Ti -1 Ti (naiv: von Wurzel aus schnell hingehen) Pfaderweiterung i,j Füge $ an alle Pfade für S1 ,..., Sm an // Tm T einfach, übersichtlich, ineffizient - O(m³) 19 Der Grundgedanke des ersten, einfachsten, übersichtlichen, aber sehr ineffizienten Algorithmus ist die sukzessive Konstruktion der Bäume Ti; Ti ist der implizite Suffix-Tree für die ersten i Zeichen des Strings S. In Phase i wird aus Ti-1 der Baum Ti erzeugt, indem das i-te Zeichen aus S hinzugefügt wird. Da auch Tm noch ein impliziter Suffix-Tree ist, ist man beim Erreichen von Tm noch nicht fertig, der endgültige Suffix-Tree soll ja auf jeden Fall eindeutig sein. Das erreicht man ganz einfach, indem eine Phase m+1 das Zeichen $ anfügt; diese Phase unterscheidet sich von den vorhergehenden überhaupt nicht. In jeder Phase i muss nun für jedes Teil-Suffix E = S[j..i-1], das schon im Baum Ti-1 enthalten ist, der Pfad um das i-te Zeichen aus S verlängert werden. Dazu verfolgen wir den Pfad von der Wurzel aus und fügen am Ende das i-te Zeichen hinzu (= Pfaderweiterung (i, j)). Wir haben also m+1 Phasen, pro Phase i Pfaderweiterungen (in Summe also i, für i = 1 bis m+1). Auch die Pfaderweiterung ist, wenn man naiv von der Wurzel aus zum Ende des Pfades geht, linear in m. Damit haben wir einen Gesamtaufwand von O(m³). Im Gegensatz zum naiven Algorithmus, wo wir nacheinander die kompletten Suffixe eingefügt haben, bauen wir jetzt die Zeichen i aus S nacheinander ein. Nun wollen wir die Pfaderweiterungen (i, j) genau betrachten. 19 Pfaderweiterungen (1) T enthalte Pfad fürE = S[j..i-1], füge S[i] an Fall 1: E endet in einem Blatt letzte Kanteninfo um S[i] erweitern ... E1 ... E2 +S[i] j Was passiert an diesem Pfad in Phase i+1? Aufwand: Anzahl der Knoten auf dem Weg + O(1) für Verlängern 20 Fall 1: Das Teil-Suffix E = S[j..i-1] endet in einem Blatt: Man muss ganz einfach die letzte Kanteninformation von E, um S[i] erweitern. In diesem Blatt hat man schon von vorher stehen, dass hier das Ende des j-ten Suffix ist. Für später ist es ganz nützlich zu überlegen, was bei diesem j in der Phase i+1 passieren wird. Das E wird dann schon bis S[i] reichen und wegen der gerade besprochenen Pfaderweiterung (i, j) in einem Blatt enden. Wir haben also wieder Fall 1, und wieder wird nur die letzte Kanteninformation verlängert. Wenn wir jetzt, wie vorher besprochen, die Kanteninformation durch ein Indexintervall definieren, also [a:i] („von einem festen Index bis zum aktuellen Schlussindex), dann brauchen wir für dieses j später nie mehr etwas zu tun. Die beiden folgenden Folien enthalten die beiden anderen möglichen Fälle, die noch auftreten können. 20 Pfaderweiterungen (2) Fall 2: E = S[j..i-1] endet im Inneren des Baumes und es gibt keine Fortsetzung mit S[i] neue Kante und Blatt einfügen (a+b), eventuell sogar neuen inneren Knoten (b) a) ... E1 ... E2 b) ... E1 ... ... E2 E1 ... E1 ... E2+F E2 ... S[i] F G F G j S[i] F j Aufwand: AnzKno auf dem Weg + O(1) f. Veränd. 21 Fall 2: E endet im Inneren des Baumes, entweder in einem inneren Knoten (Fall 2a) oder auf einer Kante (Fall 2b). Es ist wichtig zu sehen, dass in beiden Varianten ein neues Blatt entsteht, das mit dem neuen Zeichen S[i] endet. In dieses neue Blatt trägt man die Information j ein, d.h. hier endet das j-te Suffix. Bei genaueren Überlegungen sollte man sich durch die stark vereinfachte Darstellung der Folien nicht irre führen lassen: Die Fortsetzungen F und G brauchen keine einfachen Kanten zu sein, es können auch ganze Unterbäume sein. Man sollte auch hier wieder überlegen, was bei gleichem j in Phase i+1 passieren wird. Antwort: Natürlich wird E dann in einem Blatt enden und damit auch Fall 1 auftreten. 21 Pfaderweiterungen (3) Fall 3: E +S[i] ist schon im Baum enthalten nichts tun ... E1 ... E2+S[i]+F Aufwand: AnzKnoten für Weg + O(0) für nichts tun 22 Fall 3: Der um S[i] verlängerte Pfad ist schon im Baum enthalten. Man muss also nichts tun. 22 Aufwand für Pfaderweiterung Beim einfachsten Algorithmus pro Phase: O(m²) Pfaderweiterungen = O(m²) Pfade + O(m²) Veränderungen Mit zwei effizienzsteigernden Maßnahmen kann man Anzahl der Pfaderweiterungen, bei denen tatsächlich etwas zu tun ist, auf O(m) reduzieren 23 Wir fassen jetzt zusammen: Jede Pfaderweiterung erfordert zwar nur konstanten Aufwand (manche sogar gar keine Aktionen), aber wir müssen für jede auf einem (langen) Pfad erst an die Stelle des Geschehens hingehen. Außerdem haben wir O(m2) Pfaderweiterungen durchzuführen. Diese Anzahl wollen wir im nächsten Abschnitt auf weniger als 2m reduzieren. 23 Bemerkungen zur Pfaderweiterung Fall 1: automatische Erweiterung bei Verwendung von [a:i] für die letzte Kanten-information von E Fall 2: Es entsteht mindestens ein neuer Knoten (Blatt), das geschieht höchstens m-mal Fall 3: nichts tun 24 Zusammenfassung der Pfaderweiterungen Aufwandsabschätzung Fall 2: In Fall 2 entsteht immer ein Blatt, und da der Suffix-Tree nur m Blätter hat, kann dieser Fall auch nur m-mal auftreten. 24 1. Effizienzsteigerung: Einmal Blatt, immer Blatt Wenn nach der Pfaderweiterung für j in Phase i der Pfad für S[j..i] in einem Blatt endet für dieses j Fall 1 in allen späteren Phasen (automatische Erweiterung) Bedingung ist erfüllt bei: Fall 1 (Pfad endet schon in Blatt) Fall 2 (Blatt wird als Pfadende erzeugt) Beweis: In Phase k = i+1 (und k > i+1) tritt immer wieder Fall 1 auf. Keine Regel erweitert über ein Blatt hinaus 25 Wenn nach der Pfaderweiterung für j in Phase i der Pfad für S[j..i] in einem Blatt endet, dann tritt in allen späteren Phasen für dieses j Fall 1 auf (da in Phase i immer nur ein Zeichen an ein bereits bestehendes Suffix angehängt wird, kann sich ein Blatt nie „aufspalten“, d.h. plötzlich 2 oder gar mehr Nachfolger bekommen, daher kann es nur ein Blatt bleiben). 25 2. Effizienzsteigerung: j-Schleife bei Fall 3 abbrechen Die (innere) j-Schleife von Phase i kann bei Auftreten von Fall 3 abgebrochen werden Begründung: Im Fall 3 in Phase i bei j muss Pfad für S[j..i] schon in Ti-1 gewesen sein (Pfaderweiterungen mit kleinerem j haben Veränderungen in größerer Zeichentiefe als i - j + 1 verursacht, sie können also S[i] nicht in dieser Tiefe erzeugt haben). Da S[j..i] schon in Ti-1, sind auch S[k..i] für k > j schon dort (Substrings) Fall 3 für diese k 26 Man darf die j-Schleife des einfachsten Algorithmus abbrechen, wenn Fall 3 auftritt. Das ist einfach zu erläutern: Wenn in Phase i bei einem j Fall 3 auftritt, dann ist S[j..i] schon im aktuellen Baum. War es auch schon in Ti-1? Ja, denn sonst hätten die vorhergehenden Pfaderweiterungen der Phase i (also mit kleinerem j) das S[i] für Pfad j in der Zeichentiefe i - j + 1 einsetzen müssen; sie haben aber nur Veränderungen in größerer Zeichentiefe gemacht. Daher sind auch alle S[k..i] für k > j schon in Ti-1, sie sind ja Substrings von S[j..i] (1. Eigenschaft: Substrings). Bei größeren j tritt also immer wieder Fall 3 auf, der heißt aber „nichts tun“, also können wir die j-Schleife abbrechen. D.h. in jeder Phase folgt auf eine Serie von Fällen 1 und 2 nach dem ersten Auftreten von Fall 3 nur mehr Fall 3. 26 Verbesserte Pfaderweiterung (1) Pfaderweiterung unter Verwendung der beiden effizienzsteigernden Maßnahmen • Für alle j mit Pfaderweiterung nach Fall 1 oder 2 in Phase i oder früher: in späteren Phasen nichts mehr tun • j-Schleife der Phase i+1 beginnt also mit dem Index, bei dem Fall 3 auftrat, und läuft von dort aus bis wieder Fall 3 auftritt 27 Zusammenfassung, was bezüglich der Effizienz schon erreicht wurde und was noch zu tun ist: Für alle j, bei denen in Phase i oder früher Pfaderweiterung nach Fall 1 oder 2 aufgetreten ist, wird in späteren Phasen nichts mehr getan, denn es entstand ein Blatt, und Blätter erweitern sich automatisch. Die j-Schleife der Phase i+1 beginnt also mit dem Index, bei dem Fall 3 zum ersten Mal auftrat, und läuft von dort aus, bis wieder Fall 3 auftritt. 27 Verbesserte Pfaderweiterung (2) Phase: i: i+1: i+2: j p ... q q q+1 .... r r r+1 ... Es ist also bei insgesamt höchstens 2m Pfaderweiterungen etwas zu tun 28 Es muss also bei insgesamt höchstens 2m Pfaderweiterungen etwas getan werden. 28 Verbesserter Aufbau-Algorithmus Algorithmus unter Verwendung der beiden Effizienzsteigerungen Konstruiere Baum T1; j 1; Für i 2 bis m+1: // S[m+1] = $ Wiederhole: Pfaderweiterung (i, j): Wenn Fall 1 oder Fall 2: j j+1; // nächstes j // Wenn Fall 3: j-Schleife beenden Bis Fall 3 oder j > i // j > i: E leer 29 Der Ablauf: Solange Fall 1 oder 2 vorliegt, mit j weitergehen, diese j jedoch in späteren Phasen nicht mehr beachten. Wenn Fall 3 auftritt, sofort zu Phase i+1 übergehen und diese von dem eben erreichten j aus durchführen. 29 Verbesserter Algorithmus: Aufwand • Anzahl der Pfaderweiterungen: nur mehr O(m) • Jede Änderung im Baum: O(1) Aber: die jeweiligen Pfade von der Wurzel zum Ort des Geschehens können viel Zeit erfordern Lösung: Verwendung von Suffix-Links (dann muss man i.A. nicht bei der Wurzel beginnen) 30 Wir haben die Anzahl der Pfaderweiterungen auf O(m) reduziert, jede Änderung im Baum geht in konstanter Zeit, aber die jeweiligen Pfade von der Wurzel zum Ort des Geschehens können noch viel Zeit erfordern. Der endgültige Algorithmus von Ukkonen, der dann wirklich O(m) Aufwand erreicht, erfordert noch das Ausnutzen einer dritten Eigenschaft des Suffix Trees: Man muss im Allgemeinen nicht von der Wurzel ausgehen, sondern es gibt eine effizientere Methode mit Hilfe der Suffix-Links. 30 Suffix-Tree - Eigenschaft E3 Suffix-Link: • Für jeden inneren Knoten v auf dem Pfad für P = yE gibt es einen entsprechenden inneren Knoten s(v) für P' = E (bei leerem E gilt s(v) = Wurzel) Beweis: Es muss Pfade für yEF und yEG mit FG geben, damit v existiert. Daher gibt es auch Pfade für EF und EG und daher existiert auch s(v); für alle Vorgängerknoten gilt dasselbe. E yE v F F G s(v) G j+1 j 31 Wir betrachten einen Pfad für P = yE, dabei ist y ein einzelnes Zeichen, E eine beliebige Zeichenfolge, die auch leer sein kann. Die Eigenschaft E3 sagt zunächst einfach, dass es zu jedem inneren Knoten v an diesem Pfad einen entsprechenden inneren Knoten s(v) an dem Pfad für P' = E, der ja auch im Suffix-Tree sein muss, gibt. Der Beweis ist einfach. F und G sind dabei nicht-leere Zeichenfolgen (auch Unterbäume) mit verschiedenem Anfangszeichen. Falls ein v zu leerem E existiert, ist s(v) die Wurzel. 31 Suffix-Links - Beispiel a s s n a 6 s n a s 4 b a n a n a s n a 7 s n a s 5 3 2 bananas 1 32 32 3. Effizienzsteigerung: Suffix-Link Wenn beim Baum-Aufbau ein neuer Knoten v am Ende von S[j..i-1] entsteht: v merken (siehe: Pfaderweiterung (i, j), Fall 2b) Bei der anschließenden Pfaderweiterung (i, j+1) am Ende von S[j+1..i-1] findet man den Knoten s(v) oder erzeugt ihn neu: Suffix-Link v s(v) einfügen. Übergang von j zu j+1 immer in einem Schritt über den Suffix-Link, unmittelbar vor dem Blatt des Pfades j, nur direkt nach Entstehen des Knotens muss man einen Schritt zurückgehen. 33 Wir nutzen die Eigenschaft E3 für eine letzte Effizienzsteigerung. Zunächst tragen wir beim Aufbau für jeden entstehenden neuen inneren Knoten v einen Zeiger auf seinen Partner s(v) ein. Wir nennen diesen Zeiger Suffix-Link. Möge v bei der Pfaderweiterung (i, j) entstehen. In diesem Augenblick kann s(v) schon im Baum sein, er kann aber auch erst bei der anschließenden Pfaderweiterung (i, j+1) entstehen. Wir merken uns daher v und fügen den Suffix-Link v s(v) erst bei j+1 ein. Im Algorithmus nutzen wir dann künftig den Suffix-Link für den Übergang von j zu j+1. Wir beginnen den (j+1)-Pfad also nicht oben in der Wurzel, sondern tief unten, nahe bei den Blättern. Beim Übergang haben wir an Knotentiefe maximal eins verloren (siehe E3). Damit gelingt der Übergang von j zu j+1 immer in einem Schritt über den Suffix-Link, unmittelbar vor dem Blatt des Pfades j, nur unmittelbar nach Entstehen des Knotens muss man einen Schritt zurückgehen. 33 Pseudocode zu Ukkonens Algorithmus Konstruiere Baum T1; j 1; i 2; Z Zeiger auf Wurzel; Wiederhole: Gehe mit Z an das Ende des Pfades j; Pfaderweiterung (Z, i, j); Wenn NK: Suffix-Link Z0 Z eintragen; NK zurücksetzen; Wenn Fall 1 oder Fall 2: j j+1; Wenn Fall 2b: Z0 Z; NK neuer Knoten; Z Suffix-Link(Z) oder mit Z um 1 zurückgehen; Wenn Fall 3: i i+1; // nächste Phase Bis j > i; // j > i: E leer 34 Falls im vorigen Schleifendurchgang ein neuer innerer Knoten (NK) eingefügt wurde, dann erfolgt im aktuellen Schleifendurchgang die Zuordnung des entsprechenden Suffix-Links. Bei Fall 1 oder Fall 2: dem Suffix-Link folgen, falls einer existiert, um an die richtige Stelle für die Pfaderweiterung für j+1 zu gelangen. Wenn Z kein alter innerer Knoten ist, dann geht man mit Z um 1 Knoten auf dem Pfad zurück (und landet entweder bei einem alten inneren Knoten oder bei der Wurzel). 34 Suffix-Tree - Zusammenfassung • Ein Suffix-Tree für einen String der Länge m lässt sich mit Hilfe von Ukkonens Algorithmus in O(m) Zeit und O(m) Platz konstruieren (bei konstantem Alphabet ) • Im Suffix-Tree können beliebige Patterns der Länge n in O(n) Zeit gefunden werden Suffix-Trees: effiziente Lösung für eine Vielzahl komplexer Stringprobleme 35 35 Verwaltung der Nachfolger zum Teil abhängig von der Größe des Alphabets • Array: mit || Einträgen pro Knoten Platz: O(m·||), Zeit: O(1) • Lineare Liste: Platz: O(m), Zeit: O(||) • Balancierter Baum: Platz: O(m), Zeit: O(log ||) • Hashtabelle: Platz: O(m), Zeit: O(1), aber Hashfunktion 36 Möglichkeiten zur Verwaltung der Nachfolger eines Knotens (für String der Länge m, endliches Alphabet ): Arrays: Die Nachfolger eines Knotens werden in einem Array der Größe || gespeichert. Damit ergibt sich ein Gesamtplatzbedarf O(m ·||) für den gesamten Suffix-Tree. Der Aufwand für den Zugriff auf einen Nachfolger ist dafür konstant. Der schnelle Zugriff geht also auf Kosten des Speicherbedarfs, insbesondere bei großen Alphabeten. Lineare Liste (sortiert oder unsortiert): Die Nachfolger eines Knotens werden in einer linearen Liste gespeichert. Der Platzbedarf ist damit proportional zur Anzahl der Nachfolger. Damit ist der gesamte Platzbedarf für den Suffix-Tree proportional zur Anzahl der Knoten des Suffix-Trees, da jeder Knoten (außer der Wurzel) der Nachfolger eines Knotens ist, diese wiederum ist proportional zu m, damit liegt der Gesamtplatzbedarf in O(m). Leider ist hier die Zugriffszeit proportional zur Größe des Alphabets, also in O(||). Schlimmstenfalls müssen || Elemente durchlaufen werden, um den gewünschten Nachfolger zu finden, was bei großen Alphabeten sehr langsam sein kann. Balancierte Bäume: Wenn die Verwaltung der Nachfolger eines Knotens mittels balancierter Bäume erfolgt, ändert sich gegenüber den linearen Listen nichts am Platzbedarf, allerdings erfolgen Zugriffe etwas schneller, nämlich in O(log ||). Hashtabellen: Die Nachfolger eines Knotens können auch in einer Hashtabelle der Größe O(m) verwaltet werden. Im Wesentlichen erfolgt dann ein Zugriff in konstanter Zeit, wenn man davon ausgeht, dass sich die Hashfunktion schnell - d.h. in O(1) berechnen lässt und dass sich eventuelle Kollisionen effizient auflösen lassen. Bei näherer Betrachtung sieht man, dass die Knoten nahe der Wurzel oft sehr viele Nachfolger haben - dort eignen sich Arrays besonders gut. Knoten, die weiter weg von der Wurzel sind, haben dagegen üblicherweise relativ wenige Kinder - für sie eignet sich aus Speicherplatzgründen eine der anderen Varianten besser. Ideal wäre daher eine Mischung der Datenstrukturen, die diesem Umstand Rechnung trägt. 36 Aufwand mit Berücksichtigung von Speicherplatz O(m·||) Konstruktion O(m) Suche O(n) oder: Speicherplatz O(m) Konstruktion Min(O(m·log m), O(m·log ||)) Suche Min(O(n·log m), O(n·log ||)) 37 Aufwand für Suffix-Tree-Konstruktion und Suche unter Berücksichtigung des Alphabets: Entweder O(m·||) Platz, Aufbau des Suffix-Trees in O(m) und Suche im SuffixTree in O(n), oder O(m) Platz, Aufbau des Suffix-Trees in Min(O(m·log m) [im worst-case hat der String m unterschiedliche Zeichen], O(m·log ||)) und Suche im Suffix-Tree in Min(O(n·log m), O(n·log ||)). Dagegen sind Knuth-MorrisPratt und Boyer-Moore unabhängig vom Alphabet. (Bisher gingen wir implizit von einem fixen Alphabet mit konstanter Größe aus daher bezogen wir es in die Aufwandsberechnungen nicht ein.) 37 Suffix-Tree - Verallgemeinerung (1) Suffix-Tree für eine Menge von Strings Idee: • jeder String bekommt eigenes Schlusszeichen • alle werden aneinandergehängt • Blätter erhalten String- und Suffix-Index T enthält „uninteressante“ Suffixe kürzen 38 38 Suffix-Tree - Verallgemeinerung (2) Idee: • jeder String bekommt gleiches Schlusszeichen • alle werden nacheinander eingefügt Ann.: Strings S1 und S2 haben gemeinsamen Anfang: S[1..i], z.B. S1 = banana, S2 = band • S1 mit Ukkonen‘s Alg. einfügen • Phasen 1 bis i wurden für S2 schon durchgeführt: bei Phase i+1 weitermachen • für alle Strings wiederholen 39 Auf der Kante muss zusätzlich zur Anfangs- und Endposition des Strings der Stringindex angegeben werden. Wenn mehrere Strings identische Suffixe haben, dann muss das entsprechende Blatt für alle Strings die String- und Positionsindizes angeben. 39 Suffix-Tree für 2 Strings S1: banana$ S2: band$ d $ a n $ a 1:6 b a n $ n 2:2 a $ 1:2 1:4 a n a $ n d $ 1:7, 2:5 a 2:4 d $ 2:1 $ n a $ $ d $ 1:5 2:3 Blattinfo: String:Position 1:3 1:1 40 40