Weiners Algorithmus

Werbung
Weiner´s Algorithmus
Ausarbeitung von Daniel Stahr
Vortrag,gehalten am 26.11.02 im
Hauptseminar „Pattern Matching“
Inhalt:
1
2
3
Einleitung
1.1 Motivation
1.2 Naiver Algorithmus
1.3 Beispiel
Weiner´s Algorithmus
2.1 Ansatz
2.2 Grundidee
2.3 Aktualisierung der Vektoren
2.4 Korrektheit
2.5 Beispiel
2.6 Zeitanalyse
Zusammenfassung
1. Einleitung
1.1 Motivation
Weiner führte die Suffix Bäume 1973 als Index ein. Mit seinem Algorithmus kann
man Suffix Bäume in linearer Zeit bauen. Suffix Bäume waren damals eine
großartige Entdeckung, da man mit Ihnen viele Anwendungen auf Text in linearer
Zeit bewerkstelligen kann.
Einige Beispiele dafür sind :
die Textsuche, die Suche nach dem längsten sich wiederholenden Substring oder
den längsten gemeinsamen Substring, sowie das Finden von Palindromen ( abba =
reverse(abba) ist ein Palindrom).
Es gibt noch viele weitere Anwendungen, jedoch ist die Textsuche eines der
wichtigsten. Ein großer Vorteil ist, dass sowohl exakte als auch unexakte Textsuche
möglich ist.
1.2 Naiver Ansatz: Definitionen
Im Gegensatz zu den anderen Verfahren, konstruiert Weiner seine Suffix Bäume von
hinten nach vorne. Man bezeichnet den aktuellen Substring als Sffi oder als S(i) bzw.
S[i..m], wobei m die Länge des Eingabewortes ist.
Angenommen m ist der String „abba“, so ist S(4) = Suff4 = „a“, S(1) = „abba“.
Der aktuelle Baum wird als Ti bezeichnet. Anhand von Ti wird Ti+1 konstruiert.
Um die Eindeutigkeit des Baums zu gewährleisten, wird am Ende des Strings „$“
angehängt (siehe letzter Vortrag).
Man geht also von Tm+1 nach T1.
Ablauf:
S(m) = „$“ wird als erstes Blatt eingefügt ( „$“ ist die Kantenbezeichnung)
Nun wird S(i) absteigend in den jeweiligen Baum Ti+1 eingefügt.
Man vergleicht von der Wurzel aus jeweils den ersten Buchstaben der Kante
mit dem ersten Buchstaben von S(i). Findet man keine Kante, so fügt man
eine neue Kante vom Knoten aus ein. Gibt es eine Übereinstimmung, so geht
man diesen Pfad so lange entlang (eventuell über Knoten hinaus) und
vergleicht das jeweilige Zeichen mit dem entsprechenden Zeichen von S(i), bis
man ein Missmatch findet. An dieser Stelle fügt man den neuen Knoten ein.
Den Pfad von der Wurzel bis zum neuen Knoten nennt man Head(i).
Head(i) ist somit der längste gemeinsame Präfix von S[i…m], der mit einen
Substring von S[i+1…m] übereinstimmt.
1.2 Naiver Ansatz: Beispiel
Wort „aba$“:
Wir haben Baum T2 und fügen nun S(1) in den Baum ein.
S(1) = „aba$“. Pfad 2 fängt mit „a“ an, erstes Missmatch bei „$“
Head(1) = 1
Neuer Knoten wird an dieser Stelle eingefügt. Alte Kantenbezeichnung wird
geteilt
Baum T2
Wurzel r
b
a
$
Baum T1
Wurzel r
b
a
$
2
a
a
w
3
$
2
$
b
a
$
1
3
1.3 Naiver Ansatz: Analyse
Der naive Ansatz vergleicht bei jedem Durchgang jedes Zeichen bis zum ersten
Missmatch. Für das Erzeugen von Knoten braucht man konstant Zeit. Der
„kostspielige“ Prozess ist das Finden von Head(i). Der Algorithmus braucht dafür
O(m²) Zeit.
Um den Suffix Baum in linearer Zeit konstruieren zu können, muss also das Finden
von Head(i) optimiert werden. Dies wird im kommenden Abschnitt erläutert.
Der große Platzbedarf wird dadurch reduziert, dass nicht mehr explizit der String in
den Baum geschrieben wird, sondern nur noch ein Pointer auf Anfang und Ende
(siehe letzter Vortrag).
2 Weiner´s Algorithmus
2.1 Ansatz
Die Grundidee von Weiner´s Algorithmus sind zwei Vektoren, die in jedem NichtBlatt-Knoten abgespeichert werden (auch Wurzel).
Der erste Vektor heißt Indikator Vektor und ist ein Binärvektor. Der Schlüssel des
Vektors ist das verwendete Alphabet.
Der Linkvektor hat ebenfalls als Schlüssel das verwendete Alphabet. Allerdings sind
die entsprechenden Einträge Links auf andere Knoten im Baum.
Während des gesamten Algorithmus werden diese beiden Vektoren eingefügt und
erneuert.
Indikatorvektor:
Für jedes Zeichen x und jeden Knoten u ist Iu(x) = 1, falls ein Pfad xα in Ti+1 von der
Wurzel aus existiert, wobei α der Pfad von der Wurzel bis Knoten u ist.
xα muss nicht an einem Knoten enden.
Andernfalls ist der Eintrag 0.
Linkvektor:
Für jedes Zeichen x und jeden Knoten u ist Lu(x) ein Link auf einen anderen Knoten
u, falls u den Pfad α und u den Pfad xα hat.
Ansonsten ist der Eintrag null.
Diese Definitionen bedeuten, dass falls Lu(x) ≠ null ist, Iu(x) = 1 ist. Jedoch gilt dies
nicht umgekehrt, z.B. ,der gesuchte Pfad existiert, jedoch endet er nicht an einem
Knoten.
2.2 Weiner´s Algorithmus: Grundidee
Anhand der beiden Vektoren ist es nicht mehr nötig jedes Zeichen zu vergleichen.
Im aktuellen Baum Ti hat man gerade einen neuen Pfad α eingefügt. Als nächstes
fügt man xα in den Baum ein. Man startet bei Blatt ti+1 und sucht auf dem Pfad vom
Blatt bis zur Wurzel den ersten Knoten ,dessen Eintrag Iu(x) = 1 ist. Hat man den
Knoten gefunden, so weis man, dass es den gesuchten Pfad gibt, jedoch nicht, wo
dieser Pfad ist.
Der Pfad muss auch nicht an einem Knoten enden.
Als nächstes prüft man, ob Lu(x) ≠ null. Existiert der Eintrag, so springt man zu dem
gefundenen Knoten. Head(i) endet in diesem Fall am Knoten. Ist Lu(x) = null, so
springt man zum nächsten Knoten auf dem Weg zur Wurzel, bis man die Wurzel
erreicht hat. Auf dem Weg merkt man sich die Anzahl der übersprungenen Zeichen ti.
Findet man nun einen Knoten mit Lu(x) = u, so endet Head(i) genau ti Zeichen von
dem Knoten u aus. Findet man kein u, so endet Head(i) genau ti Zeichen auf einer
Kante von der Wurzel aus.
Ist Iu(x) = 0 für alle Knoten auf dem Weg, so wird S(i) als neue Kante von der Wurzel
aus eingefügt.
2.3 Weiner´s Algorithmus: Aktualisieren der Vektoren
Damit der Algorithmus funktioniert, müssen die Vektoren nach jedem Schritt
aktualisiert werden.
Linkvektor:
Da pro Schritt maximal ein Knoten eingefügt wird, muss auch nur maximal ein
Eintrag verändert werden. Auf einen Knoten kann auch maximal ein Knoten zeigen.
Falls Iv(S(i)) = 1 und Lv´(S(i)) ≠ null sind, so muss Lv(S(i)) auf den neuen Knoten w
verweisen.
Neue Knoten werden mit einem leeren Vektor initialisiert.
Indikatorvektor:
Da α existiert und wir xα einfügen, können alle Einträge auf dem Pfad α von der
Wurzel aus, auf Iv(x) = 1 gesetzt werden, soweit sie es noch nicht sind. Dies kann
schon während der Suche geschehen. Damit muss der Weg nur einmal durchlaufen
werden.
Fügt man einen neuen Knoten w in die Kante e(v´´, z) ein, so kopiert man den
Indikatorvektor vom darunter liegenden Knoten z.
2.4 Weiner´s Algorithmus: Korrektheit des Algorithmus
Um die Korrektheit zu beweisen, müssen zwei Theoreme bewiesen werden.
Theorem 1: Angenommen Knoten v mit Pfad α wurde gefunden, dann ist der
gesuchte String genau S(i)α.
Beweis:
Head(i) ist der längste Präfix von Suffi, der auch in Suffk ist, mit k > i.
Da Iv(S(i)) = 1 gilt, gibt es einen Pfad in Ti+1, der mit S(i) beginnt. Head(i) ist also
mindestens ein Zeichen lang, wir bezeichnen Head(i) mit S(i)β.
Suffi und Suffk beginnen also beide mit Head(i) = S(i)β und divergieren danach, also
S(i)βa und S(i)βb. Dann beginnen folglich Suffi+1 und Suffk+1 mit βa und βb. Somit
gibt es eine Kante von der Wurzel aus, die mit β beginnt und sich später teilt. Also
gibt es einen Knoten u in Ti+1, mit Pfad β und Iu(S(i)) = 1, wenn es einen Pfad S(i)β
gibt. U muss auf dem Pfad zu Blatt i+1 sein, daβ ein Prefix von Suffi+1 ist.
Nehmen wir nun an, Iv(S(i)) = 1 und v hat Pfad α, dann beginnt Head(i) mit S(i) α.
Das bedeutet, das α ein Prefix von β ist und deshalb muss u mit Pfad β entweder v
oder ein Knoten unterhalb von v auf dem Pfad sein. Wäre u unterhalb von v, so wäre
Iu(S(i)) = 1 und wir hätten v anstatt von u gewählt. Dies bedeutet: v = u und α = β.
Theorem 2: Angenommen v und v´ wurden gefunden und Lv´(S(i)) zeigt auf v´´.
Ist li = 0 (Anzahl der gelesenen Zeichen), so endet Head(i) am Knoten v´´.
Andernfalls endet Head(i) nach genau li Zeichen auf einer Kante von v´´.
Beweis:
Aufgrund der Definition des Linkvektors, muss v´´ in Head(i) enthalten sein. Head(i)
muss mindestens bis zu diesem Knoten gehen. Laut Theorem 1 ist Head(i) = S(i)α,
also muss Head(i) li Zeichen unter v´´ enden. Jetzt muss man noch zeigen, dass
Head(i) nicht über die Kante e(v´´, z) hinaus gehen kann.
Ist z ein Blatt, dann wäre der Pfad bis z ein Suffix von Head(i), was ein Widerspruch
an sich ist.
Angenommen z ist ein Knoten mit Pfad S(i)γ. In diesem Fall müsste es bereits einen
Knoten z´ mit Pfad γ in Ti+1 geben. Dieser Knoten müsste unterhalb von v´, auf dem
Pfad i+1 sein. Dies widerspricht der Wahl von v´.
Somit muss Head(i) innerhalb der Kante e enden.
2.5 Weiner´s Algorithmus: Beispiel
Die Bäume in den Beispielen dienen nur zur Veranschaulichung und sind keine
richtigen Suffix Bäume.
1. Beispiel
Wir fügen den String „bab aa cd$“ in den Baum ein.
Der erste Knoten, mit I(S(i)) = 1, ist auch gleichzeitig der erste Knoten mit L(S(i)) =
Link. Es werden also keine Zeichen gelesen ( t = 0). Die neue Kante entspringt dem
Knoten v´´
.
b
a
a
b
b
v10
v1
Iv2(b) = 1
a
a
Lv2(b) = v11
a
a
b
c
v11
v2
c
d
c
d
v3
$
Iv3(b) = 0
Lv3(b) = null
b
b
b
2. Beispiel
Wir fügen den String „bab aa d$“ in den Baum ein.
Bei v3 finden wir das erste Mal I(S(i)) = 1 (v). Nun suchen wir v´, in unserem Fall v2.
Auf dem Weg zu v´ merken wir die übersprungenen Zeichen ( t = 1).
Der neue Knoten muss also nach dem ersten Zeichen, auf einer Kante von v´´
eingefügt werden.
b
a
a
b
a
a
b
v10
v1
v11
v2
b
Iv2(b) = 1
Lv2(b) = v11
a
a
b
c
d
$
v3
1 Zeichen
gemerkt: ti
=1
Iv3(b) = 1
Lv3(b) = null
b
b
b
2.6 Weiner´s Algorithmus: Zeitanalyse
Das Springen zwischen Knoten, sowie das Einfügen von Knoten, kann in
konstanter Zeit bewerkstelligt werden. Die Zeitkomplexität des Verfahrens hängt
von der Anzahl der Sprünge ab, die man während der Suche nach Head(i) macht.
Pro Schritt wird ein neuer Knoten erzeugt. Da durch einen Sprung die Pfadlänge
höchstens um eins erhöht wird, kann die aktuelle Pfadlänge höchstens um 2
Schritte erhöht werden. Ingesamt kann die aktuelle Pfadlänge also nur 2m mal
erhöht werden. Daraus folgt auch, dass sie maximal 2m mal erniedrigt werden
kann (wenn man den Pfad nach oben läuft, auf der Suche nach Head(i)). D.h.
man muss höchstens 2m Knoten bei der Suche nach Head(i) besuchen. Die
Laufdauer des Algorithmus ist proportional zur Anzahl der Knoten, die wir
besuchen.
=> Lineare Laufzeit
3. Schlusswort
Da Weiner´s Algorithmus in linearer Zeit terminiert, ist er in Bezug auf die Laufzeit
genauso gut, wie seine Nachfolger. Allerdings hat er einen höheren Platzbedarf.
Dies kommt daher, dass man in jedem Nicht-Blatt-Knoten die zwei Vektoren
abspeichert. Ukkonens Algorithmus speichert im Ganzen nur 2 Vektoren ab (siehe
Vortrag Ukkonens Algorithmus).
Dies ist auch der Grund, weshalb Weiner´s Algorithmus nicht mehr so sehr verbreitet
ist, wie die anderen Verfahren.
Herunterladen