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 Effiziente Algorithmen Suchen in Zeichenfolgen: Teil 1 Martin Gruber Gabriele Koller Vielzahl von Anwendungen: Textverarbeitung, Information Retrieval, Datenbanksuche in der Bioinformatik, ... 1 2 Ansätze zum Pattern Matching Substring Problem Naiver Algorithmus - O(m·n): praktisch proportional zu m+n 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! Lineare Algorithmen - O(m+n): • 1976 Knuth, Morris und Pratt • 1976 Boyer, Moore • 1980 Karp, Rabin Bsp.: T = bananas P1 = na, P2 = ana, P3 = ast, ... Suche nach vielen Patterns im selben String? 3 Suffixe eines Strings Suffix-Trie 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 4 • 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 ananas anas as bananas nanas nas s • Suchaufwand O(n)? • Erstellungsaufwand O(m)? P1 = na, P2 = ana, P3 = ast, ... 5 a n n s 2 n s a 6 n n s s a a 5 4 n s a a b s a s 1 a 7 3 bananas 6 1 Suffix-Tree Suffix-Tree - Definition • 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 • 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 a s s n a 6 s n a s 4 n a b a n a n a s 7 s n a s 5 3 2 bananas 1 7 8 Suffix als Präfix eines Suffixes Suffix-Tree mit Schlusszeichen • 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“ • Lösung des Präfix-Problems: Schlusszeichen, das nicht im String S vorkommt, z.B. $ a ←6 n a ←4 n a n a ←5 n a b a n a n a 1 $ $ • Impliziter Suffix-Tree: ohne Schlusszeichen (für manche Zwecke ausreichend) 3 2 a n a 6 $ n a $ 4 b a n a n a $ 2 banana 1 n a 7 $ n a $ 5 3 banana$ 9 10 Suffix-Tree - Speicherbedarf 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 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 22 11 n a3:4 77 s7:7 n 5:7 a s a n a 6 s 55 Beispiele: P = „ana“ ist Präfix von S2=„ananas“ und S4=„anas“, P = „ast“ ist kein Präfix eines Suffixes 33 bananas bananas 1234567 11 s s n a s 4 b a n a n a s 2 1 n a 7 s n a s 5 3 bananas 12 2 Suffix-Tree - Eigenschaft E2 Suchen im Suffix-Tree 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“ Ist Pattern P vorhanden? = Ist P Substring von S? a s s 6 s 4 b a n a n a s 1 n a 7 s n a s 5 3 bananas 13 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 Schnelles Suchen im Suffix-Tree (1) Schnelles Suchen im Suffix-Tree (2) Voraussetzung: Pattern ist garantiert vorhanden 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) • 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 14 Aufwand: O(n* + k) k = Anzahl der Vorkommen von P 15 16 Naiver Algorithmus zum Aufbau von T Suffix Trees in O(m) Zeit erstellen Idee: Suffixe nacheinander zu T hinzufügen Weiner (1973): Linear Pattern Matching Algorithms, Proc. of the 14th IEEE Symp. on Switching and Automata Theory 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 „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 ⇒ Aufwand linear in O(m)? Viel einfacher (Gusfield), wird hier erklärt 17 18 3 Einfachster Aufbau-Algorithmus Pfaderweiterungen (1) Ti: impliziter Suffix-Tree für die i ersten Zeichen von S, enthält also alle Suffixe des Strings S[1..i] T enthalte Pfad für E = S[j..i-1], füge S[i] an Fall 1: E endet in einem Blatt ⇒ letzte Kanteninfo um S[i] erweitern 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) ... E1 E2 +S[i] ... Pfaderweiterung i,j j Was passiert an diesem Pfad in Phase i+1? Aufwand: Anzahl der Knoten auf dem Weg + O(1) für Verlängern // Tm →T Füge $ an alle Pfade für S1 ,..., Sm an ⇒ einfach, übersichtlich, ineffizient - O(m³) 19 20 Pfaderweiterungen (2) Pfaderweiterungen (3) 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) Fall 3: E +S[i] ist schon im Baum enthalten ⇒ nichts tun a) ... E1 ... E1 ... E2 b) ... ... E1 ... E2 E1 ... E2+F ... F G j E1 ... E2 S[i] F G ... E2+S[i]+F S[i] F j Aufwand: AnzKnoten für Weg + O(0) für nichts tun Aufwand: AnzKno auf dem Weg + O(1) f. Veränd. 21 22 Aufwand für Pfaderweiterung Bemerkungen zur Pfaderweiterung Beim einfachsten Algorithmus pro Phase: O(m²) Pfaderweiterungen = O(m²) Pfade + O(m²) Veränderungen 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 ⇒ Mit zwei effizienzsteigernden Maßnahmen kann man Anzahl der Pfaderweiterungen, bei denen tatsächlich etwas zu tun ist, auf O(m) reduzieren Fall 3: nichts tun 23 24 4 1. Effizienzsteigerung: Einmal Blatt, immer Blatt 2. Effizienzsteigerung: j-Schleife bei Fall 3 abbrechen 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: 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 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 26 Verbesserte Pfaderweiterung (1) Verbesserte Pfaderweiterung (2) 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 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 27 28 Verbesserter Aufbau-Algorithmus Verbesserter Algorithmus: Aufwand Algorithmus unter Verwendung der beiden Effizienzsteigerungen • Anzahl der Pfaderweiterungen: nur mehr O(m) • Jede Änderung im Baum: O(1) 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 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) 29 30 5 Suffix-Tree - Eigenschaft E3 Suffix-Links - Beispiel Suffix-Link: a • 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 F≠G 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. v F F G n a 6 s E yE s(v) G j+1 s s n a s 4 b a n a n a s 2 1 n a 7 s n a s 5 3 bananas j 31 3. Effizienzsteigerung: Suffix-Link 32 Pseudocode zu Ukkonens Algorithmus 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. 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 33 34 Suffix-Tree - Zusammenfassung Verwaltung der Nachfolger • 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 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 |Σ|) ⇒Suffix-Trees: effiziente Lösung für eine Vielzahl komplexer Stringprobleme • Hashtabelle: Platz: O(m), Zeit: O(1), aber Hashfunktion 35 36 6 Aufwand mit Berücksichtigung von Σ Suffix-Tree - Verallgemeinerung (1) Speicherplatz O(m·|Σ|) Konstruktion O(m) Suche O(n) 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 oder: Speicherplatz O(m) Konstruktion Min(O(m·log m), O(m·log |Σ|)) Suche Min(O(n·log m), O(n·log |Σ|)) T enthält „uninteressante“ Suffixe → kürzen 37 Suffix-Tree - Verallgemeinerung (2) 38 Suffix-Tree für 2 Strings Idee: S1: banana$ S2: band$ • jeder String bekommt gleiches Schlusszeichen • alle werden nacheinander eingefügt a 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 a d $ 1:6 n a $ 1:4 1:2 a n a $ d $ 2:1 $ 1:7, 2:5 a 2:4 $ 2:2 n d $ b a n $ n n a $ $ d $ 1:5 2:3 Blattinfo: String:Position 1:3 1:1 39 40 Wiederholung Suffix-Tree a Effiziente Algorithmen s s n a Suchen in Zeichenfolgen: Teil 2 6 s n a s Martin Gruber Gabriele Koller 4 b a n a n a s 2 1 41 n a s n a s Substring Problem: 7 5 3 bananas Geg.: String S der Länge m, Pattern P der Länge n, endliches Alphabet Σ Suffix-Tree-Aufbau: O(m) Suche nach P: O(n) 42 7 Demo-Programm strmat Suffix-Tree für großes Alphabet Webseite von Dan Gusfield: http://www.cs.ucdavis.edu/~gusfield/strmat • Natürliche Sprachen Sammlung von C-Programmen für String Matching Algorithmen Aufwand für Aufbau des Suffix-Trees: • Weiner, McCreight, Ukkonen: O(m) für konstantes Alphabet • Farach (1997): O(m) für Integer-Alphabet im Intervall [1,m] (z.B. chinesische Schriftzeichen) • Integer-Alphabet 43 44 Farachs Ansatz Farachs Ansatz - 1. Schritt (1) Divide-and-Conquer-Prinzip: • Erstelle kompaktierten Trie To für alle „ungeraden Suffixe“ (mit m/2 Blättern) • Berechne aus To den Suffix-Tree Te für die „geraden Suffixe“ • Füge To und Te zu T zusammen - O(m) Suffix-Tree To für „ungerade Suffixe“ erstellen • Paare bilden: 5 4 3 5 5 1 4 2 5 5 6 • Paare mit Radix-Sort sortieren → String S' i 1 2 3 4 5 6 S' = 4 1 3 2 5 6 Rangzahl von Paar i • Suffix-Tree TS' für S' rekursiv aufbauen • To aus TS' ableiten Bsp.: S = 5 4 3 5 5 1 4 2 5 5 6 (TRITTBRETT$) 45 Farachs Ansatz - 1. Schritt (2) Suffix-Tree TS' 5 1 (6) 11:11 7 (5) 10:11 3 (4) 2:11 (4) 7:11 6 (5 1: ) 1 (1) 6:11 5 To (3) 3:11 1 Farachs Ansatz - 2. Schritt (1) S=54355142556 (6) 6:6 3 (5) 5:6 (4) 1:6 4 (3) 3:6 (1) 2:6 2 (2) 4:6 S' = 4 1 3 2 5 6 46 Suffix-Tree Te für gerade Suffixe aus To ableiten • „Ungerade Suffixe“ mit vorangehendem Zeichen der „geraden Suffixes“ und mittels stabilem Radix-Sort sortieren: S=54355142556 lex. Reihenfolge in To: 3 7 5 1 9 11 11 9 (4,3) (1,7) (5,5) (2,9) (5,11) → (1,7) (2,9) (4,3) (5,5) (5,11) Blattindex: i → 2i-1 Bei gleichem Anfangszeichen Zwischenknoten einfügen 47 →Lex. Reihenfolge für gerade Suffixe: 6 8 2 4 10 →Longest Common Prefix (lcp) berechnen (-1) 48 8 Farachs Ansatz - 2. Schritt (2) Farachs Ansatz - 3. Schritt • Lex. Reihenfolge für gerade Suffixe: 6 8 2 4 10 • lcp für benachbarte Suffixe (über To): Suffix-Trees To und Te zusammenfügen • Kanten aus To und Te mittels gekoppelter Tiefensuche der Reihe nach zu T hinzufügen • Merge: Kanten mit gleichem Anfangszeichen verschmelzen → es könnte zuviel verschmolzen worden sein • Unmerge: jeden inneren Knoten u prüfen, ob Verschmelzung bis an diese Stelle passt Aufwand für Merge und Unmerge: jeweils O(m) lcp(l2i, l2j) = lcp(l2i+1, l2j+1) + 1, wenn S[2i] = S[2j] lcp(l2i, l2j) = 0, sonst Te (4) 2:11 6 8 2 4: 4 (5) 5:11 (2) 8:11 S=54355142556 (1) 6:11 (5 ) (6) 11:11 lcp(l6, l8) = 0 lcp(l8, l2) = 0 lcp(l2, l4) = 0 lcp(l4, l10) = 1 4 10 49 50 Farachs Ansatz - 3. Schritt: Merge S=54355142556 5 1 9 11 x: Zeichentiefe(x) = 5 lcp(S2,S7) = 1 (6) 11:11 3 y (5) 5:6 z (4) 2:11 8 2 4 10 6 7 (1) 6:11 (6) 11:11 10 (5) 1 :1 x (4) 7:11 1 9 S=54355142556 (6) 11:1 1 2:6 (4) 7:11 (5) 5:11 4 5 5:6 (2) 8:11 2 (4) 2:11 (2) 8:11 (1) 6:11 3 (4) (3) 3:11 2 8 (5) (1) 6:11 (4) 2:11 8 6 4: 4 7 T 11 (4) 7:11 (1) 6:11 (2) 8:11 6 9 (6) 11:1 1 (5) 1 :1 (4 )2 :6 (4) 7:11 (5 ) Te (6) 11:11 (5) 10:11 1 (3) 3:11 5 11 Baum passt bis zu Knoten u ⇔ Zeichentiefe(u) = lcp(Si,Sj) (i aus Te, j aus To; u Vorfahr von Si und Sj) T (6) 11:11 7 (4) 2:11 (4) 7:11 3 (1) 6:11 (3) 3:11 (5 1: ) 1 (1) 6:11 To Farachs Ansatz - 3. Schritt: Unmerge y: Zeichentiefe(y) = 1 lcp(S1,S4) = 1 10 z: Zeichentiefe(z) = 3 lcp(S4,S9) = 2 4 51 Resultierender Suffix-Tree Zusammenfassung Suffix-Trees T S=54355142556 5 4 (6) 11:11 (4) 2:11 1 5: 5 (6) 11:11 2 11 (5) (1) 6:11 7 (1) 6:11 3 (5) 1 :1 (3) 3:11 (2) 8:11 8 Suffix-Trees eignen sich hervorragend für verschiedene Suchaufgaben in Strings String S der Länge m, Pattern P der Länge n • Aufbau des Suffix-Trees für S in O(m) (6) 11:1 1 2 :2 (2) 8:11 6 (3) 3:11 (1) 6:11 (4) 52 - Konstruktionsalgorithmus für konstante Alphabete von Ukkonen (1995) - Konstruktionsalgorithmus für große Alphabete von Farach (1997) 10 • Suche nach P im Suffix-Tree in O(n) • Platzbedarf O(m) 9 53 54 9 Suffix-Array Suffix-Array: Beispiel Manber, Myers (1991) Speichersparende Datenstruktur für Suffixe String S der Länge m, Pattern P der Länge n • Suffix Si: si, ..., sm (für i = 1 bis m) Grundidee: • m Suffixe lexikographisch ordnen, nur SuffixIndex speichern • mit binärer Suche ein Suffix mit Präfix P suchen • maximal log2 m Vergleiche P ⇔ Si 1 S 2 3 4 5 6 ↓ a n a n a s ↓ b a n a n a s ↓ a n a s ↓ n a n a s 1 7 b a n a n a s SA ↓ a n a n a s ↓ ↓ ↓ n a s a s s 2 3 4 5 6 7 2 4 6 1 3 5 7 ↓ ↓ ↓ ↓ ↓ ↓ a a b n n s n s a a a a n n s s a a n s a s 55 Einfachste Datenstruktur 1 2 3 4 5 6 Suffix-Array durch einfaches Sortieren Suffix-Array für String S der Länge m • Mit Standard-Sortierverfahren sortieren 7 b a n a n a s S Pos 56 2 4 6 1 3 5 7 1 bananas 2 ananas 3 nanas 4 anas 5 nas 6 as 7s S3 = nanas Pos: Array zur Festlegung der lexikographischen Ordnung Pos[k] = i, wenn Si an k-ter Stelle Nur S und Pos speichern Sortieren 2 ananas 4 anas 6 as 1 bananas 3 nanas 5 nas 7s ⇒ Einfach, aber ineffizient ⇒ Aufwand? 57 58 Suffix-Tree und Suffix-Array Suffix-Array aus Suffix-Tree ableiten Suffix-Tree • Konstruktion des Suffix-Trees: O(m) • Traversieren in lexikal. Reihenfolge und Blattindizes der Reihe nach im Array eintragen: O(m) T Suffix-Array a s s n a 6 s n a s 4 b a n a n a s n a SA 2 4 6 1 3 5 7 S b a n a n a s 7 s n a s 5 7 6 4 2 3 5 Traversieren SA 2 4 6 1 3 5 7 3 1 2 ⇒ Suffix-Array in linearer Zeit konstruierbar 1 59 60 10 Einfachste Suche (1) Einfachste Suche (2) Einfachste Suche für „P in S?“ (ohne Sonderfälle) Sonderfälle: • P < SPos[1] oder P > SPos[m]: P kein Substring • P = SPos[1] oder P = SPos[m]: erster/letzter Suffix ist Lösung 1 2 3 4 5 6 7 L ← 1; R ← m; Pos 2 4 6 1 3 5 7 Wiederhole M ← (L + R) / 2; Wenn P < SPos[M]: R ← M; L M R Wenn P > SPos[M]: L ← M; Bis (P ist Präfix von SPos[M]) oder (L + 1 = R); Nachbehandlung nach der binären Suche: a) Frage „Ist P Suffix?“: Längen überprüfen b) Alle Vorkommen von P in S: von M aus nach links und rechts gehen, solange es Lösungen gibt c) Anzahl der Vorkommen (wenn sehr oft): Je eine binäre Suche für linke und rechte Grenze Ergebnis: P in S vom Pos[M]-ten Zeichen an 61 62 Einfachste Suche (3) Ineffizienz beim Vergleichen Vergleichen von Substrings: Zeichen für Zeichen vergleichen (ab erstem Zeichen) Oft stimmen die vorderen Zeichen der beiden Substrings schon überein → mit lcp ermitteln Funktion Plcp(i) = lcp(P, SPos[i]): schwach monoton steigend bis zur Lösung (wenn es eine gibt), dort Höhe n, dann schwach monoton fallend Aufwand: log2 m String-Vergleiche zu je max. n Zeichen-Vergleichen: O(n·log2 m) Plcp n ... 0 ... i 1 m 63 64 Schnelleres Vergleichen mittels lcp (1) Schnelleres Vergleichen mittels lcp (2) Es sei bekannt: l = lcp(P, SPos[L]) r = lcp(P, SPos[R]) (für l ≥ r, sonst symmetrisch mit Rlcp): l a) l > Llcp[M]: c = Llcp[M], Llcp[M] = c Lösung links P L M c? lcp l ? ? r L M R i Ges.: c = lcp(P, SPos[M]) Lösung gefunden, links oder rechts von M? Weiters seien bekannt: Llcp[M] = lcp(SPos[L], SPos[M]) Rlcp[M] = lcp(SPos[M], SPos[R]) b) l = Llcp[M]: falls P[l+1..k] = SPos[M][l+1..k] → c = l+k, P[c+1] >/< SPos[M][c+1] ⇒ Lösung links/rechts c) l < Llcp[M] ⇒ c = l, Lösung rechts 65 l k c = l+k Llcp[M] P L M Llcp[M] c=l P L M 66 11 Effizienzsteigerung Zusätzliche Arrays Llcp und Rlcp Zusammenfassung: Für festes m kommt man bei der binären Suche zu einem M immer auf demselben Weg (beginnend mit L = 1 und R = m), man hat dort immer die selben Werte von L und R a) und c) Keine Zeichenvergleiche erforderlich, c kann einfach aus Werten am Rand des Intervalls hergeleitet werden b) Jeder der insgesamt k (≤ n) Zeichenvergleiche erhöht Max(l,r) um 1 Wenn Llcp[M] = lcp(SPos[L], SPos[M]) und Rlcp[M] = lcp(SPos[M], SPos[R]) bekannt: bei binärer Suche max. 2n Zeichenvergleiche nötig Gesamtaufwand für Suche: O(n + log2 m) Können wir alle Llcp[M] und Rlcp[M] vorab berechnen? ⇒ Es gibt nur m-2 Tripel (L,M,R) für M = 2, ..., m-1 ⇒ Für jeden String S mit m Zeichen kann man die Arrays Llcp[M] und Rlcp[M], M = 2, ..., m-1 vorausberechnen 67 Binärbaum mit lcp-Werten 68 Berechnung der lcp-Arrays lcp(1,1) Für benachbarte Blätter im Suffix-Tree i,i+1: lcp(i,i+1) = Zeichentiefe von v, wobei v der innere Knoten auf dem Weg vom Blatt i zum Blatt i+1 mit niedrigster Zeichentiefe ist Für (i, j) mit j > i+1: lcp(i, j) = Min(lcp(k, k+1)) für k = i, ..., j-1 lcp(1,2) lcp(1,2) lcp(1,4) lcp(2,3) lcp(2,4) lcp(3,4) lcp(1,8) lcp(4,5) lcp(4,6) lcp(5,6) lcp(4,8) lcp(6,7) ⇒ Aufwand für Suche im Suffix-Array (Pos, Llcp, Rlcp): O(n + log2 m) Llcp[M] Rlcp[M] lcp(6,8) lcp(7,8) 69 Suffix-Tree für lcp-Berechnung 70 Direkte Erstellung des Suffix-Arrays T Manber und Myers (1991): Direkte Konstruktion des Suffix-Arrays in O(m log m) 0 a 1 n a 3 n a s s 6 s (3) 4 (2) 2 (1) s 2 b a n a n a s n a s n a s 5 (6) 3 (5) 7 (7) Bsp.: lcp(i,i+1) lcp(1,2) = 3 lcp(2,3) = 1 lcp(3,4) = 0 Kärkkäinen, Sanders (2003): Direkte Konstruktion des Suffix-Arrays in O(m) = Skew-Algorithmus Bsp.: lcp(i,j): lcp(2,4) = 0 1 (4) 71 72 12 Struktur des Skew-Algorithmus Skew-Algorithmus - Datenstrukturen Divide-and-Conquer-Prinzip: 1. Erstelle das Suffix-Array SA12 für die Suffixe an Positionen i mod 3 ≠ 0 2. Berechne das Suffix-Array SA0 für die übrigen Suffixe mit Hilfe von SA12 3. Füge beide Suffix-Arrays zusammen Lexikographische Ordnung der Suffixe • SA: Suffix Array für S Ord. 1 2 3 4 SA[i] = j: an i-ter Stelle der lex. Ordnung steht Sj SA 2 4 6 1 • SO: Ordnungsfeld für S 1 2 3 4 Suffix enthält S* SO[j] = i: Sj steht in der SO 4 1 5 2 lex. Ordnung an Stelle i (welches Suffix ist lexikographisch kleiner?) 5 6 7 3 5 7 5 6 7 6 3 7 SO aus SA bestimmen: SO[SA[i]] = i Aufwand: O(m) Eingabe: String S über Integer-Alphabet Σ 73 74 Skew Algorithmus - 1. Schritt Skew Algorithmus - 2. Schritt 1. Wähle Tripel an Positionen i mod 3 ≠ 0 Suffix-Array SA0 für übrige Suffixe Si erstellen (jene mit i mod 3 = 0) 2. Sortiere diese Tripel mit Radix-Sort Si = (S[i], Si+1) 3. Berechne SO12 und SA12 für diese Tripel (ev. rekursiv) Reihenfolge der Si+1 ist aus SA12/SO12 bekannt, nur stabil nach S[i] sortieren 75 76 Skew Algorithmus - 3. Schritt Suffix-Tree versus Suffix-Array Vergleich von Si aus SA0 mit Sj aus SA12: S: String der Länge m, P: Pattern der Länge n Fall 1: j mod 3 = 1: Si = (S[i], Si+1), Sj = (S[j], Sj+1) wenn S[i]=S[j]: Reihung nach Si+1 und Sj+1 Erstpublikation Datenstruktur Speicherbedarf Fall 2: j mod 3 = 2: Si = (S[i], S[i+1], Si+2), Sj = (S[j], S[j+1], Sj+2) wenn S[i]=S[j]: Reihung nach S[i+1] und S[j+1] wenn S[i+1]=S[j+1]: Reihung nach Si+2 und Sj+2 Suchaufwand Suffix-Tree 1973 1 Baum O(2m) Knoten O(n) Suffix-Array 1991 3 Arrays Θ(3m) Integers Θ(n + log m) zusätzlich von Größe des Alphabets abhängig 77 78 13 Anwendungen für Suffix-Trees Exaktes String Matching Exaktes String Matching Longest Common Substring Linearisierung zirkulärer Strings Substrings für Pattern-Datenbank DNA-Kontaminierung Common Substring von > 2 Strings Geg.: String S, Pattern P Ges.: Ist P in S enthalten? Varianten: • Geg.: String S fix, viele Patterns P Ges.: alle Vorkommen von P in S • Geg.: Pattern P fix, viele Strings S Ges.: alle Vorkommen von P in S 79 80 Longest Common Substring (LCS) Longest Common Substring - Lösung Geg.: zwei Strings Sa, Sb (Länge: ma, mb) • Allg. Suffix-Tree für Sa und Sb erstellen - Blatt entspricht Suffix aus Sa und/oder Sb - jeden internen Knoten mit 1 und/oder 2 markieren, wenn Unterbaum Blatt aus Sa und/oder Sb enthält - mit 1+2 markierte Knoten: Substrings aus Sa und Sb Ges.: längster Substring, der in Sa und Sb enthalten ist Bsp.: TRITTBRETT, RAUBRITTER • Liefere Knoten mit größter Zeichentiefe, der mit 1+2 markiert ist „vermutlich nicht linear lösbar“ (Knuth, 1970) Linearer Aufwand: O(ma+mb) (Baum-Konstruktion und Traversieren) 81 82 Linearisierung zirkulärer Strings Linearisierung zirkulärer Strings - Lsg. Geg.: zirkulärer String S mit Nummerierung 1 bis m (ab beliebiger Position) • S an beliebiger Position durchschneiden → linearer String L; • Suffix-Tree für LL$ aufbauen • Suffix-Tree bis Zeichentiefe m traversieren: immer lexikalisch kleinste Kante wählen • Beliebiges Blatt aus Unterbaum gibt Schnitt-Position an Ges.: Schnitt-Position, sodass resultierender linearer String der lexikalisch kleinste aller m möglichen Strings ist Anwendung: zirkuläre Moleküle (Chemie) Aufwand: O(m) 83 84 14 Literatur (1) Literatur (2) Dan Gusfield: Algorithms on Strings, Trees, and Sequences Cambridge University Press, 1997 Udi Manber, Gene Myers: Suffix Arrays: A New Method for On-line String Searches SIAM Journal on Computing 23, S. 262-272, 1993 Martin Farach: Optimal Suffix Tree Construction with Large Alphabets Proc. of the 38th Annual Symposium on Foundations of Computer Science, S. 137-143, IEEE, 1997 Juha Kärkkäinen, Peter Sanders: Simple Linear Work Suffix Array Construction Int. Colloquium on Automata, Languages and Programming (ICALP 2003), Springer LNCS 2719, S. 943-955, 2003 85 86 Substrings für Pattern-Datenbank Verallgemeinerung des Substring Problems Effiziente Algorithmen Geg.: fixe Menge von Strings (Datenbank) mit großer Gesamtlänge m, Folge von Strings P Suchen in Zeichenfolgen: Teil 3 Anhang: Weitere Anwendungen Ges.: alle Strings aus der Datenbank, die p als Substring enthalten Bsp.: DNA-DB für US-Militärpersonal 87 88 Substrings für Pattern-DB - Lösung DNA-Kontaminierung • Allg. Suffix-Tree für Strings in DB aufbauen • Suche nach Substring wie bisher (auch für alle Vorkommen) • Gesamter String ist in der DB, wenn Pfad für P an einem Blatt endet Geg.: String Sa und String Sb Aufwand: O(m) für Aufbau des Suffix-Trees + O(n) bzw. O(n+k) für jede Suche Sa: neue DNA Sb: mögliche Verunreinigungen Ges.: alle Substrings von Sb in Sa, die eine Länge l (Schwellwert) überschreiten d.h. Kandidaten für Verunreinigung 89 90 15 DNA-Kontaminierung - Lösung Allg. DNA-Kontaminierung • Allg. Suffix-Tree für Sa und Sb aufbauen • Alle internen Knoten, die Blätter aus Sa und Sb im Unterbaum haben, markieren • Markierte Knoten mit Zeichentiefe > l liefern Verallgemeinerung: Menge von Strings, die mögliche Verunreinigungen darstellen Geg.: String S und Menge von Strings P Ges.: alle Substrings von Strings aus P in S, die eine Länge l überschreiten Aufwand: O(|Sa|+|Sb|) 91 92 Allg. DNA-Kontaminierung - Lösung Common Substrings von >2 Strings • Allg. Suffix-Tree für S und alle Strings aus P aufbauen • Alle internen Knoten, die Blätter aus S und einem String aus P im Unterbaum haben, markieren • Markierte Knoten mit Zeichentiefe > l liefern Aufwand: O(Summe der Stringlängen) Geg.: Menge von K Strings mit Gesamtlänge von n Ges.: Finde Substrings in dieser Menge, die in vielen der Strings vorkommen 93 94 Common Substrings - Lösung (1) Common Substrings - Lösung (2) für k = 2, ..., K: l(k) = Länge des längsten Substrings, der in mindestens k Strings enthalten ist Tabelle mit K-1 Einträgen für l(k) und einen der jeweiligen Substrings erzeugen Aufwand: O(nK) (O(n) mittels lowest common ancestor) • Allg. Suffix-Tree für K Strings aufbauen - Blätter mit jeweiligem String-Index (1 bis K) markieren - jeder String erhält eigenes Schlusszeichen - C(v) = Anzahl der unterschiedlichen StringIndizes im Unterbaum von internem Knoten v • Wenn C(v) und Stringtiefe für jeden Knoten bekannt: l(k) durch Traversierung in O(n) finden 95 96 16 Common Substrings - Lösung (3) Berechnung von C(v) in O(n·K) Bit-Vektor der Länge K an jedem v: - Bit i = 1, wenn Blatt mit i im Unterbaum von v - C(v) = Anzahl der 1-Bits - Bit-Vektoren der Nachfolger oder-verknüpfen in O(K) Vektor V(k): Stringtiefe des tiefsten Knotens mit C(v)=k 97 17