Document

Werbung
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ürE = 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 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.
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
Herunterladen