Ich fühle mich an dieser Stelle etwas dick. Ich habe Tage damit verbracht, meinen Kopf vollständig um die Suffixbaumkonstruktion zu wickeln, aber da ich keinen mathematischen Hintergrund habe, entziehen sich viele der Erklärungen mir, da sie anfangen, die mathematische Symbologie übermäßig zu nutzen. Eine gute Erklärung, die ich gefunden habe, kommt der schnellen Suche nach Zeichenfolgen mit Suffixbäumen am nächsten , aber er beschönigt verschiedene Punkte und einige Aspekte des Algorithmus bleiben unklar.
Eine schrittweise Erklärung dieses Algorithmus hier auf Stack Overflow wäre für viele andere außer mir von unschätzbarem Wert, da bin ich mir sicher.
Als Referenz finden Sie hier Ukkonens Artikel zum Algorithmus: http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf
Mein bisheriges Grundverständnis:
- Ich muss jedes Präfix P eines gegebenen Strings T durchlaufen
- Ich muss jedes Suffix S im Präfix P durchlaufen und das zum Baum hinzufügen
- Um dem Baum das Suffix S hinzuzufügen, muss ich jedes Zeichen in S durchlaufen, wobei die Iterationen entweder aus einem vorhandenen Zweig bestehen, der mit demselben Zeichensatz C in S beginnt, und möglicherweise eine Kante in absteigende Knoten aufteilen, wenn ich Erreichen Sie ein anderes Zeichen im Suffix, ODER wenn es keine passende Kante zum Abwärtsgehen gab. Wenn keine passende Kante für C gefunden wird, wird für C eine neue Blattkante erstellt.
Der grundlegende Algorithmus scheint O (n 2 ) zu sein, wie in den meisten Erklärungen ausgeführt wird, da wir alle Präfixe durchlaufen müssen, dann müssen wir jedes der Suffixe für jedes Präfix durchlaufen. Der Algorithmus von Ukkonen ist anscheinend aufgrund der von ihm verwendeten Suffix-Zeigertechnik einzigartig, obwohl ich denke, dass ich Probleme habe, diese zu verstehen.
Ich habe auch Probleme zu verstehen:
- genau wann und wie der "aktive Punkt" zugewiesen, verwendet und geändert wird
- Was ist mit dem Heiligsprechungsaspekt des Algorithmus los?
- Warum die Implementierungen, die ich gesehen habe, die von ihnen verwendeten Begrenzungsvariablen "reparieren" müssen
Hier ist der vollständige C # -Quellcode. Es funktioniert nicht nur korrekt, sondern unterstützt auch die automatische Heiligsprechung und rendert ein besser aussehendes Textdiagramm der Ausgabe. Quellcode und Beispielausgabe finden Sie unter:
Update 2017-11-04
Nach vielen Jahren habe ich eine neue Verwendung für Suffixbäume gefunden und den Algorithmus in JavaScript implementiert . Das Wesentliche ist unten. Es sollte fehlerfrei sein. Legen Sie es in einer js-Datei npm install chalk
vom selben Speicherort ab und führen Sie es dann mit node.js aus, um eine farbenfrohe Ausgabe zu sehen. Es gibt eine abgespeckte Version im selben Gist, ohne den Debugging-Code.
https://gist.github.com/axefrog/c347bf0f5e0723cbd09b1aaed6ec6fc6
quelle
Antworten:
Das Folgende ist ein Versuch, den Ukkonen-Algorithmus zu beschreiben, indem zuerst gezeigt wird, was er tut, wenn die Zeichenfolge einfach ist (dh keine wiederholten Zeichen enthält), und dann auf den vollständigen Algorithmus erweitert wird.
Zunächst einige vorläufige Aussagen.
Was wir bauen, ist im Grunde wie ein Suchversuch. Es gibt also einen Wurzelknoten, aus dem Kanten herausgehen, die zu neuen Knoten führen, und weitere Kanten, die aus diesen herausgehen, und so weiter
Aber : Anders als bei einem Suchversuch sind die Kantenbeschriftungen keine einzelnen Zeichen. Stattdessen wird jede Kante mit einem Paar von Ganzzahlen beschriftet
[from,to]
. Dies sind Zeiger in den Text. In diesem Sinne trägt jede Kante eine Zeichenfolgenbezeichnung beliebiger Länge, benötigt jedoch nur O (1) Leerzeichen (zwei Zeiger).Grundprinzip
Ich möchte zunächst zeigen, wie der Suffixbaum einer besonders einfachen Zeichenfolge erstellt wird, einer Zeichenfolge ohne wiederholte Zeichen:
Der Algorithmus arbeitet in Schritten von links nach rechts . Für jedes Zeichen der Zeichenfolge gibt es einen Schritt . Jeder Schritt kann mehr als eine einzelne Operation umfassen, aber wir werden sehen (siehe die letzten Beobachtungen am Ende), dass die Gesamtzahl der Operationen O (n) ist.
Wir beginnen also von links und fügen zuerst nur das einzelne Zeichen ein,
a
indem wir eine Kante vom Wurzelknoten (links) zu einem Blatt erstellen und als kennzeichnen.[0,#]
Dies bedeutet, dass die Kante den Teilstring darstellt, der an Position 0 beginnt und endet am aktuellen Ende . Ich benutze das Symbol#
, um das aktuelle Ende zu bezeichnen , das sich an Position 1 befindet (direkt danacha
).Wir haben also einen ersten Baum, der so aussieht:
Und was es bedeutet ist folgendes:
Nun kommen wir zu Position 2 (gleich danach
b
). Unser Ziel bei jedem Schritt ist es, alle Suffixe bis zur aktuellen Position einzufügen . Wir machen das durcha
Angebots aufab
b
In unserer Darstellung sieht das so aus
Und was es bedeutet ist:
Wir beobachten zwei Dinge:
ab
ist dieselbe wie im ursprünglichen Baum :[0,#]
. Die Bedeutung hat sich automatisch geändert, da wir die aktuelle Position#
von 1 auf 2 aktualisiert haben .Als nächstes erhöhen wir die Position erneut und aktualisieren den Baum, indem wir
c
an jede vorhandene Kante ein anhängen und eine neue Kante für das neue Suffix einfügenc
.In unserer Darstellung sieht das so aus
Und was es bedeutet ist:
Wir beobachten:
#
und das Einfügen der einen neuen Kante für das endgültige Zeichen in O (1) -Zeit erfolgen kann. Daher ist für eine Zeichenfolge mit der Länge n nur O (n) Zeit erforderlich.Erste Erweiterung: Einfache Wiederholungen
Das funktioniert natürlich nur deshalb so gut, weil unsere Saite keine Wiederholungen enthält. Wir betrachten nun eine realistischere Zeichenfolge:
Es beginnt mit
abc
wie im vorherigen Beispiel, wird dannab
wiederholt und gefolgt vonx
und wird dannabc
wiederholt, gefolgt vond
.Schritte 1 bis 3: Nach den ersten 3 Schritten haben wir den Baum aus dem vorherigen Beispiel:
Schritt 4: Wir bewegen uns
#
zu Position 4. Dies aktualisiert implizit alle vorhandenen Kanten auf Folgendes:und wir müssen das letzte Suffix des aktuellen Schritts
a
an der Wurzel einfügen .Bevor wir dies tun, stellen wir (zusätzlich zu ) zwei weitere Variablen vor
#
, die natürlich die ganze Zeit vorhanden waren, aber bisher noch nicht verwendet wurden:(active_node,active_edge,active_length)
remainder
ist eine Ganzzahl, die angibt, wie viele neue Suffixe eingefügt werden müssenDie genaue Bedeutung dieser beiden wird bald klar, aber jetzt sagen wir einfach:
abc
Beispiel war der aktive Punkt immer(root,'\0x',0)
, dhactive_node
der Wurzelknoten,active_edge
wurde als Nullzeichen angegeben'\0x'
undactive_length
war Null. Dies hatte zur Folge, dass die eine neue Kante, die wir in jedem Schritt eingefügt haben, am Wurzelknoten als frisch erstellte Kante eingefügt wurde. Wir werden bald sehen, warum ein Triple erforderlich ist, um diese Informationen darzustellen.remainder
wurde zu Beginn jedes Schritts immer auf 1 gesetzt. Die Bedeutung davon war, dass die Anzahl der Suffixe, die wir am Ende jedes Schritts aktiv einfügen mussten, 1 war (immer nur das letzte Zeichen).Das wird sich jetzt ändern. Wenn wir das aktuelle Endzeichen
a
an der Wurzel einfügen , stellen wir fest, dass bereits eine ausgehende Kante beginnta
, insbesondere mit :abca
. Folgendes tun wir in einem solchen Fall:[4,#]
am Wurzelknoten ein. Stattdessen bemerken wir einfach, dass das Suffixa
bereits in unserem Baum ist. Es endet in der Mitte einer längeren Kante, aber das stört uns nicht. Wir lassen die Dinge einfach so, wie sie sind.(root,'a',1)
. Das bedeutet, dass sich der aktive Punkt jetzt irgendwo in der Mitte der ausgehenden Kante des Wurzelknotens befindeta
, die speziell nach Position 1 an dieser Kante beginnt . Wir bemerken, dass die Kante einfach durch ihr erstes Zeichen angegeben wirda
. Dies reicht aus, da es nur eine Kante geben kann, die mit einem bestimmten Zeichen beginnt (bestätigen Sie, dass dies der Fall ist, nachdem Sie die gesamte Beschreibung gelesen haben).remainder
, so dass es zu Beginn des nächsten Schritts 2 ist.Beobachtung: Wenn festgestellt wird, dass das endgültige Suffix, das wir einfügen müssen, bereits im Baum vorhanden ist, wird der Baum selbst überhaupt nicht geändert (wir aktualisieren nur den aktiven Punkt und
remainder
). Der Baum ist dann keine genaue Darstellung des Suffixbaums bis zur aktuellen Position mehr, sondern enthält alle Suffixe (da das endgültige Suffix implizita
enthalten ist ). Abgesehen von der Aktualisierung der Variablen (die alle eine feste Länge haben, also O (1)), wurden in diesem Schritt keine Arbeiten durchgeführt.Schritt 5: Wir aktualisieren die aktuelle Position
#
auf 5. Dadurch wird der Baum automatisch auf Folgendes aktualisiert:Und weil
remainder
es 2 ist , müssen wir zwei letzte Suffixe der aktuellen Position einfügen:ab
undb
. Dies ist im Grunde, weil:a
Suffix aus dem vorherigen Schritt wurde nie richtig eingefügt. So ist es geblieben , und seit wir einen Schritt vorangekommen sind, ist es jetzt vona
bis gewachsenab
.b
.In der Praxis bedeutet dies, dass wir zum aktiven Punkt gehen (der hinter
a
dieabcab
Kante zeigt) und das aktuelle Endzeichen einfügenb
. Aber: Es stellt sich wieder heraus, dassb
auch schon an derselben Kante vorhanden ist.Also ändern wir den Baum nicht. Wir einfach:
(root,'a',2)
(den gleichen Knoten und die gleiche Kante wie zuvor, aber jetzt zeigen wir auf hinter denb
)remainder
auf 3, da wir die letzte Kante aus dem vorherigen Schritt immer noch nicht richtig eingefügt haben und auch die aktuelle letzte Kante nicht einfügen.Um es klar auszudrücken: Wir mussten einfügen
ab
undb
im aktuellen Schritt, aber daab
bereits gefunden, haben wir den aktiven Punkt aktualisiert und nicht einmal versucht einzufügenb
. Warum? Denn wennab
es im Baum ist, muss jedes Suffix (einschließlichb
) auch im Baum sein. Vielleicht nur implizit , aber es muss da sein, weil wir den Baum bisher so gebaut haben.Wir fahren mit Schritt 6 fort, indem wir inkrementieren
#
. Der Baum wird automatisch aktualisiert auf:Weil
remainder
3 ist , müssen wir einfügenabx
,bx
undx
. Der aktive Punkt sagt uns, woab
endet, also müssen wir nur dorthin springen und das einfügenx
. In der Tatx
ist es noch nicht da, also teilen wir dieabcabx
Kante und fügen einen internen Knoten ein:Die Kantendarstellungen sind immer noch Zeiger in den Text, sodass das Teilen und Einfügen eines internen Knotens in O (1) -Zeit erfolgen kann.
Wir haben uns also mit 2 befasst
abx
undremainder
auf 2 dekrementiert . Jetzt müssen wir das nächste verbleibende Suffix einfügenbx
. Aber bevor wir das tun, müssen wir den aktiven Punkt aktualisieren. Die Regel hierfür wird nach dem Teilen und Einfügen einer Kante unten als Regel 1 bezeichnet und gilt immer dann, wenn dieactive_node
Wurzel ist (wir lernen Regel 3 für andere Fälle weiter unten). Hier ist Regel 1:Daher zeigt das neue Aktivpunkt-Tripel
(root,'b',1)
an, dass die nächste Einfügung ambcabx
Rand hinter 1 Zeichen, dh hinter, erfolgen mussb
. Wir können die Einfügemarke in O (1) -Zeit identifizieren und prüfen, ob siex
bereits vorhanden ist oder nicht. Wenn es vorhanden wäre, würden wir den aktuellen Schritt beenden und alles so lassen, wie es ist. Istx
aber nicht vorhanden, so fügen wir es durch Teilen der Kante ein:Auch dies dauerte O (1) Zeit und wir aktualisieren
remainder
auf 1 und der aktive Punkt auf(root,'x',0)
Regel 1.Aber wir müssen noch etwas tun. Wir nennen das Regel 2:
Wir müssen noch das letzte Suffix des aktuellen Schritts einfügen
x
. Da dieactive_length
Komponente des aktiven Knotens auf 0 gefallen ist, erfolgt die endgültige Einfügung direkt an der Wurzel. Da es am Wurzelknoten keine ausgehende Kante gibtx
, fügen wir eine neue Kante ein:Wie wir sehen können, wurden im aktuellen Schritt alle verbleibenden Einfügungen vorgenommen.
Wir fahren mit Schritt 7 fort, indem wir
#
= 7 setzen, wodurch das nächste Zeichena
wie immer automatisch an alle Blattränder angehängt wird . Dann versuchen wir, das neue Endzeichen in den aktiven Punkt (die Wurzel) einzufügen und stellen fest, dass es bereits vorhanden ist. Also beenden wir den aktuellen Schritt, ohne etwas einzufügen, und aktualisieren den aktiven Punkt auf(root,'a',1)
.In Schritt 8 ,
#
= 8, hängen wir anb
, und wie zuvor gesehen bedeutet dies nur, dass wir den aktiven Punkt aktualisieren(root,'a',2)
und erhöhen,remainder
ohne etwas anderes zu tun, da diesb
bereits vorhanden ist. Jedoch bemerkt man (in O (1) die Zeit) , daß die aktive Stelle nun am Ende einer Kante ist. Wir reflektieren dies, indem wir es auf neu einstellen(node1,'\0x',0)
. Hiernode1
beziehe ich mich auf den internen Knoten, an dem dieab
Kante endet.Dann müssen wir in Schritt
#
= 9 'c' einfügen, um den letzten Trick zu verstehen:Zweite Erweiterung: Verwenden von Suffix-Links
Wie immer wird das
#
Updatec
automatisch an die Blattränder angehängt und wir gehen zum aktiven Punkt, um zu sehen, ob wir 'c' einfügen können. Es stellt sich heraus, dass 'c' bereits an dieser Kante vorhanden ist, also setzen wir den aktiven Punkt auf(node1,'c',1)
, erhöhen ihnremainder
und tun nichts anderes.Nun in Schritt
#
= 10 ,remainder
4, und so wir zunächst eingelegt werden mussabcd
(die von 3 Schritten vor bleibt) durch Einfügend
an der aktiven Stelle.Der Versuch,
d
am aktiven Punkt einzufügen , führt zu einer Kantenaufteilung in O (1) -Zeit:Das
active_node
, von dem aus die Teilung eingeleitet wurde, ist oben rot markiert. Hier ist die letzte Regel, Regel 3:Der aktive Punkt ist also jetzt
(node2,'c',1)
undnode2
wird unten rot markiert:Da das Einfügen von
abcd
abgeschlossen ist, dekrementieren wirremainder
auf 3 und betrachten das nächste verbleibende Suffix des aktuellen Schrittsbcd
. Regel 3 hat den aktiven Punkt genau auf den richtigen Knoten und die richtige Kante gesetzt, sodass das Einfügenbcd
durch einfaches Einfügen des endgültigen Zeichensd
am aktiven Punkt erfolgen kann.Dies führt zu einer weiteren Kantenaufteilung. Aufgrund von Regel 2 müssen wir eine Suffix-Verknüpfung vom zuvor eingefügten zum neuen Knoten erstellen:
Wir beobachten: Suffix-Links ermöglichen es uns, den aktiven Punkt zurückzusetzen, damit wir die nächste verbleibende Einfügung bei O (1) durchführen können. Sehen Sie sich die obige Grafik an, um zu bestätigen, dass der Knoten bei der Bezeichnung tatsächlich
ab
mit dem Knoten beib
(seinem Suffix) und dem Knoten beiabc
verknüpft istbc
.Der aktuelle Schritt ist noch nicht abgeschlossen.
remainder
ist jetzt 2 und wir müssen Regel 3 folgen, um den aktiven Punkt wieder zurückzusetzen. Da der Stromactive_node
(oben rot) keinen Suffix-Link hat, werden wir auf root zurückgesetzt. Der aktive Punkt ist jetzt(root,'c',1)
.Daher erfolgt die nächste Einfügung an der einen ausgehenden Kante des Wurzelknotens, deren Beschriftung mit beginnt
c
:cabxabcd
hinter dem ersten Zeichen, dh hinterc
. Dies führt zu einer weiteren Aufteilung:Und da dies die Erstellung eines neuen internen Knotens beinhaltet, folgen wir Regel 2 und setzen einen neuen Suffix-Link aus dem zuvor erstellten internen Knoten:
(Ich verwende Graphviz Dot für diese kleinen Grafiken. Der neue Suffix-Link hat dazu geführt, dass dot die vorhandenen Kanten neu angeordnet hat. Überprüfen Sie daher sorgfältig, ob das einzige, was oben eingefügt wurde, ein neuer Suffix-Link ist.)
Damit
remainder
kann auf 1 gesetzt werden und da dasactive_node
root ist, verwenden wir Regel 1, um den aktiven Punkt auf zu aktualisieren(root,'d',0)
. Dies bedeutet, dass die letzte Einfügung des aktuellen Schritts darin besteht, eine einzelned
an der Wurzel einzufügen :Das war der letzte Schritt und wir sind fertig. Es gibt jedoch eine Reihe von abschließenden Beobachtungen :
In jedem Schritt bewegen wir
#
uns um 1 Position vorwärts. Dadurch werden automatisch alle Blattknoten in O (1) -Zeit aktualisiert.Es geht jedoch nicht um a) verbleibende Suffixe aus vorherigen Schritten und b) um das letzte Zeichen des aktuellen Schritts.
remainder
gibt an, wie viele zusätzliche Einsätze wir machen müssen. Diese Einfügungen entsprechen eins zu eins den endgültigen Suffixen der Zeichenfolge, die an der aktuellen Position endet#
. Wir betrachten nacheinander und machen den Einsatz. Wichtig: Jede Einfügung erfolgt in O (1) -Zeit, da der aktive Punkt genau angibt, wohin wir gehen sollen, und wir nur ein einziges Zeichen am aktiven Punkt hinzufügen müssen. Warum? Weil die anderen Zeichen implizit enthalten sind (andernfalls wäre der aktive Punkt nicht dort, wo er ist).Nach jeder solchen Einfügung dekrementieren wir
remainder
den Suffix-Link und folgen ihm, falls vorhanden. Wenn nicht, gehen wir zu root (Regel 3). Wenn wir bereits an der Wurzel sind, ändern wir den aktiven Punkt mithilfe von Regel 1. In jedem Fall dauert es nur O (1).Wenn wir während einer dieser Einfügungen feststellen, dass das Zeichen, das wir einfügen möchten, bereits vorhanden ist, tun wir nichts und beenden den aktuellen Schritt, auch wenn
remainder
> 0. Der Grund dafür ist, dass alle verbleibenden Einfügungen Suffixe derjenigen sind, die wir gerade versucht haben. Daher sind sie alle implizit im aktuellen Baum. Die Tatsache, dassremainder
> 0 ist, stellt sicher , dass wir uns später mit den verbleibenden Suffixen befassen.Was ist, wenn am Ende des Algorithmus
remainder
> 0? Dies ist immer dann der Fall, wenn das Ende des Textes eine Teilzeichenfolge ist, die irgendwo zuvor aufgetreten ist. In diesem Fall müssen wir ein zusätzliches Zeichen am Ende der Zeichenfolge anhängen, das zuvor noch nicht aufgetreten ist. In der Literatur wird üblicherweise das Dollarzeichen$
als Symbol dafür verwendet. Wieso spielt das eine Rolle? -> Wenn wir später den fertigen Suffixbaum verwenden, um nach Suffixen zu suchen, müssen wir Übereinstimmungen nur akzeptieren, wenn sie an einem Blatt enden . Andernfalls würden wir viele falsche Übereinstimmungen erhalten, da implizit viele Zeichenfolgen im Baum enthalten sind, die keine tatsächlichen Suffixe der Hauptzeichenfolge sind. Erzwingenremainder
0 am Ende zu sein ist im Wesentlichen eine Möglichkeit, um sicherzustellen, dass alle Suffixe an einem Blattknoten enden. Aber wenn wir den Baum suchen verwenden mögen allgemeinen Teil , nicht nur die Suffixe der Hauptsaite, ist dieser letzte Schritt in der Tat nicht erforderlich, wie unten durch die OP Kommentar vorgeschlagen.Wie komplex ist der gesamte Algorithmus? Wenn der Text n Zeichen lang ist, gibt es offensichtlich n Schritte (oder n + 1, wenn wir das Dollarzeichen hinzufügen). In jedem Schritt tun wir entweder nichts (außer die Variablen zu aktualisieren) oder wir fügen
remainder
Einfügungen ein, die jeweils O (1) Zeit in Anspruch nehmen. Daremainder
angibt, wie oft wir in den vorherigen Schritten nichts getan haben und für jede Einfügung, die wir jetzt vornehmen, dekrementiert wird, beträgt die Gesamtzahl der Aktionen genau n (oder n + 1). Daher ist die Gesamtkomplexität O (n).Es gibt jedoch eine kleine Sache, die ich nicht richtig erklärt habe: Es kann vorkommen, dass wir einem Suffix-Link folgen, den aktiven Punkt aktualisieren und dann feststellen, dass seine
active_length
Komponente mit dem neuen nicht gut funktioniertactive_node
. Stellen Sie sich zum Beispiel eine Situation wie diese vor:(Die gestrichelten Linien geben den Rest des Baums an. Die gepunktete Linie ist ein Suffix-Link.)
Lassen Sie nun den aktiven Punkt so sein
(red,'d',3)
, dass er auf die Stelle hinterf
demdefg
Rand zeigt. Nehmen wir nun an, wir haben die erforderlichen Aktualisierungen vorgenommen und folgen nun dem Suffix-Link, um den aktiven Punkt gemäß Regel 3 zu aktualisieren. Der neue aktive Punkt ist(green,'d',3)
. Dasd
Verlassen des grünen Knotens ist jedochde
so, dass es nur 2 Zeichen enthält. Um den richtigen aktiven Punkt zu finden, müssen wir dieser Kante natürlich bis zum blauen Knoten folgen und auf zurücksetzen(blue,'f',1)
.In einem besonders schlimmen Fall
active_length
könnte das so groß sein wieremainder
, was so groß sein kann wie n. Und es kann sehr gut vorkommen, dass wir nicht nur über einen internen Knoten springen müssen, um den richtigen aktiven Punkt zu finden, sondern im schlimmsten Fall über viele, bis zu n. Bedeutet das, dass der Algorithmus eine versteckte O (n 2 ) -Komplexität aufweist, da in jedem Schrittremainder
im Allgemeinen O (n) ist und die Nachanpassungen des aktiven Knotens nach dem Folgen einer Suffix-Verknüpfung auch O (n) sein könnten?Nein. Der Grund ist, dass, wenn wir tatsächlich den aktiven Punkt anpassen müssen (z. B. von grün nach blau wie oben), dies uns zu einem neuen Knoten bringt, der eine eigene Suffix-Verknüpfung hat und
active_length
reduziert wird. Wenn wir der Kette der Suffix-Glieder folgen, nehmen wir die verbleibenden Einfügungen vor,active_length
können nur abnehmen, und die Anzahl der aktiven Punktanpassungen, die wir unterwegs vornehmen können, kann nicht größer sein alsactive_length
zu einem bestimmten Zeitpunkt. Daactive_length
niemals größer seinremainder
kann undremainder
nicht nur in jedem einzelnen Schritt O (n) ist, sondern die Gesamtsumme der jemalsremainder
im Verlauf des gesamten Prozesses vorgenommenen Inkremente auch O (n) ist, beträgt die Anzahl der aktiven Punktanpassungen auch begrenzt durch O (n).quelle
abcdefabxybcdmnabcdex
. Der erste Teil vonabcd
wird inabxy
(dies erzeugt einen internen Knoten nachab
) und erneut in wiederholtabcdex
und endet inbcd
, was nicht nur imbcdex
Kontext, sondern auch imbcdmn
Kontext erscheint. Nach demabcdex
Einfügen folgen wir dem Suffix-Link zum Einfügenbcdex
, und dort wird canonicizeIch habe versucht, den Suffixbaum mit dem in der Antwort von jogojapan angegebenen Ansatz zu implementieren, aber er funktionierte in einigen Fällen aufgrund der für die Regeln verwendeten Formulierung nicht. Außerdem habe ich erwähnt, dass es mit diesem Ansatz niemandem gelungen ist, einen absolut korrekten Suffixbaum zu implementieren. Im Folgenden werde ich eine "Übersicht" über die Antwort von Jogojapan mit einigen Änderungen an den Regeln schreiben. Ich werde auch den Fall beschreiben, in dem wir vergessen, wichtige Suffix-Links zu erstellen .
Zusätzliche verwendete Variablen
Verwenden wir ein Konzept eines internen Knotens - alle Knoten außer der Wurzel und den Blättern sind interne Knoten .
Beobachtung 1
Wenn festgestellt wird, dass das endgültige Suffix, das wir einfügen müssen, bereits im Baum vorhanden ist, wird der Baum selbst überhaupt nicht geändert (wir aktualisieren nur das
active point
undremainder
).Beobachtung 2
Wenn irgendwann
active_length
größer oder gleich der Länge der aktuellen Kante (edge_length
) ist, bewegen wir uns nachactive point
unten, bis sieedge_length
streng größer als istactive_length
.Definieren wir nun die Regeln neu:
Regel 1
Regel 2
Diese Definition von
Rule 2
unterscheidet sich von jogojapan ', da wir hier nicht nur die neu erstellten internen Knoten berücksichtigen , sondern auch die internen Knoten, aus denen wir eine Einfügung vornehmen.Regel 3
In dieser Definition von betrachten
Rule 3
wir auch die Einfügungen von Blattknoten (nicht nur Split-Knoten).Und schließlich Beobachtung 3:
Wenn sich das Symbol, das wir dem Baum hinzufügen möchten, bereits am Rand befindet
Observation 1
, aktualisieren wir gemäß nuractive point
undremainder
lassen den Baum unverändert. ABER wenn es einen internen Knoten gibt, der als Suffix-Link erforderlich markiert ist , müssen wir diesen Knotenactive node
über eine Suffix-Verbindung mit unserem Strom verbinden.Schauen wir uns das Beispiel eines Suffixbaums für cdddcdc an, wenn wir in einem solchen Fall einen Suffixlink hinzufügen und wenn wir dies nicht tun:
Wenn wir die Knoten NICHT über eine Suffix-Verbindung verbinden:
Wenn wir DO die Knoten durch einen Suffix Link verbinden:
Es scheint keinen signifikanten Unterschied zu geben: Im zweiten Fall gibt es zwei weitere Suffix-Links. Diese Suffix-Links sind jedoch korrekt , und einer von ihnen - vom blauen zum roten Knoten - ist für unseren Ansatz mit aktivem Punkt sehr wichtig . Das Problem ist, dass, wenn wir hier später keinen Suffix-Link einfügen, wenn wir dem Baum einige neue Buchstaben hinzufügen, wir möglicherweise das Hinzufügen einiger Knoten zum Baum aufgrund des , weil dementsprechend kein a vorhanden ist , weglassen Suffix Link, dann müssen wir das an die Wurzel setzen.
Rule 3
active_node
Als wir den letzten Buchstaben zum Baum hinzufügten, war der rote Knoten bereits vorhanden, bevor wir eine Einfügung aus dem blauen Knoten vorgenommen haben (die Kante ist mit 'c' gekennzeichnet ). Da es eine Einfügung vom blauen Knoten gab, markieren wir ihn als Suffix-Link erforderlich . Dann wurde unter Verwendung des aktiven Punktansatzes der
active node
auf den roten Knoten gesetzt. Wir machen jedoch keine Einfügung vom roten Knoten, da der Buchstabe 'c' bereits am Rand steht. Bedeutet dies, dass der blaue Knoten ohne Suffix-Link belassen werden muss? Nein, wir müssen den blauen Knoten mit dem roten über eine Suffix-Verbindung verbinden. Warum ist es richtig? Weil der aktive PunktDer Ansatz garantiert, dass wir an den richtigen Ort gelangen, dh an den nächsten Ort, an dem wir eine Einfügung eines kürzeren Suffixes verarbeiten müssen.Zum Schluss hier meine Implementierungen des Suffixbaums:
Ich hoffe, dass diese "Übersicht" in Kombination mit der detaillierten Antwort von jogojapan jemandem hilft, seinen eigenen Suffixbaum zu implementieren.
quelle
aabaacaad
Dies ist einer der Fälle, in denen das Hinzufügen eines zusätzlichen Suffix-Links die Aktualisierungszeiten des Triple verkürzen kann. Die Schlussfolgerung in den letzten beiden Absätzen des Postens von Jogojapan ist falsch. Wenn wir die in diesem Beitrag erwähnten Suffix-Links nicht hinzufügen, sollte die durchschnittliche Zeitkomplexität O (nlong (n)) oder mehr betragen. Weil es zusätzliche Zeit braucht, um über den Baum zu gehen, um das Richtige zu findenactive_node
.Vielen Dank für das gut erklärte Tutorial von @jogojapan , ich habe den Algorithmus in Python implementiert.
Ein paar kleinere Probleme , die durch @jogojapan Wendungen erwähnt , um mehr zu sein anspruchsvoll , als ich erwartet habe, und müssen sehr sorgfältig behandelt werden. Es hat mich mehrere Tage gekostet, meine Implementierung robust genug zu machen (nehme ich an). Probleme und Lösungen sind unten aufgeführt:
Ende mit
Remainder > 0
Es stellt sich heraus, dass diese Situation auch während des Entfaltungsschritts auftreten kann , nicht nur am Ende des gesamten Algorithmus. In diesem Fall können wir den Rest, den Actnode, den Actedge und die Actlength unverändert lassen , den aktuellen Entfaltungsschritt beenden und einen weiteren Schritt starten, indem wir entweder weiter falten oder entfalten, je nachdem, ob sich das nächste Zeichen in der ursprünglichen Zeichenfolge auf dem aktuellen Pfad befindet oder nicht.Sprung über Knoten: Wenn wir einem Suffix-Link folgen, aktualisieren Sie den aktiven Punkt und stellen Sie fest, dass seine Komponente active_length mit dem neuen aktiven Knoten nicht gut funktioniert. Wir müssen vorwärts an die richtige Stelle gehen, um zu spalten oder ein Blatt einzufügen. Dieser Prozess könnte sein , nicht so einfach , weil während der Bewegung des actlength und actedge halten alle die Art und Weise ändern, wenn Sie sich zu bewegen zurück zu dem haben Wurzelknoten , der actedge und actlength sein könnte falsch , weil dieser sich bewegt. Wir benötigen zusätzliche Variablen, um diese Informationen zu speichern.
Auf die beiden anderen Probleme hat @managonov irgendwie hingewiesen
Teilen könnte entartet sein Wenn Sie versuchen, eine Kante zu teilen, werden Sie manchmal feststellen, dass die Teilungsoperation direkt auf einem Knoten ausgeführt wird. In diesem Fall müssen wir diesem Knoten nur ein neues Blatt hinzufügen. Nehmen Sie es als Standard-Edge-Split-Operation. Dies bedeutet, dass die Suffix-Links, falls vorhanden, entsprechend beibehalten werden sollten.
Versteckte Suffix-Links Es gibt einen weiteren Sonderfall, der bei Problem 1 und Problem 2 auftritt . Manchmal müssen wir mehrere Knoten an der richtigen Stelle für Split - hop über, könnten wir übertreffen den richtigen Punkt , wenn wir durch den Vergleich der Rest - String und die Pfad Etiketten bewegen. In diesem Fall wird der Suffix-Link unbeabsichtigt vernachlässigt, falls vorhanden. Dies könnte vermieden werden, indem Sie sich beim Vorwärtsbewegen an den richtigen Punkt erinnern . Die Suffix-Verknüpfung sollte beibehalten werden, wenn der geteilte Knoten bereits vorhanden ist oder sogar das Problem 1 während eines Entfaltungsschritts auftritt.
Schließlich ist meine Implementierung in Python wie folgt:
quelle
Entschuldigung, wenn meine Antwort überflüssig erscheint, aber ich habe kürzlich den Algorithmus von Ukkonen implementiert und tagelang damit zu kämpfen gehabt. Ich musste mehrere Artikel zu diesem Thema durchlesen, um das Warum und Wie einiger Kernaspekte des Algorithmus zu verstehen.
Ich fand den 'Regeln'-Ansatz früherer Antworten nicht hilfreich, um die zugrunde liegenden Gründe zu verstehen , deshalb habe ich alles unten geschrieben und mich ausschließlich auf die Pragmatik konzentriert. Wenn Sie Probleme haben, anderen Erklärungen zu folgen, genau wie ich, wird meine ergänzende Erklärung möglicherweise dazu führen, dass Sie darauf klicken.
Ich habe meine C # -Implementierung hier veröffentlicht: https://github.com/baratgabor/SuffixTree
Bitte beachten Sie, dass ich kein Experte auf diesem Gebiet bin. Daher können die folgenden Abschnitte Ungenauigkeiten (oder Schlimmeres) enthalten. Wenn Sie auf etwas stoßen, können Sie es jederzeit bearbeiten.
Voraussetzungen
Der Ausgangspunkt der folgenden Erklärung setzt voraus, dass Sie mit dem Inhalt und der Verwendung von Suffixbäumen sowie mit den Merkmalen des Ukkonen-Algorithmus vertraut sind, z. B. wie Sie den Suffixbaum Zeichen für Zeichen von Anfang bis Ende erweitern. Grundsätzlich gehe ich davon aus, dass Sie einige der anderen Erklärungen bereits gelesen haben.
(Allerdings musste ich eine grundlegende Erzählung für den Fluss hinzufügen, damit sich der Anfang tatsächlich überflüssig anfühlt.)
Der interessanteste Teil ist die Erklärung des Unterschieds zwischen der Verwendung von Suffix-Links und dem erneuten Scannen von der Wurzel aus . Dies gab mir viele Fehler und Kopfschmerzen bei meiner Implementierung.
Offene Blattknoten und ihre Einschränkungen
Ich bin sicher, Sie wissen bereits, dass der grundlegendste Trick darin besteht, zu erkennen, dass wir das Ende der Suffixe einfach offen lassen können, dh auf die aktuelle Länge der Zeichenfolge verweisen, anstatt das Ende auf einen statischen Wert zu setzen. Auf diese Weise werden diese Zeichen beim Hinzufügen zusätzlicher Zeichen implizit allen Suffix-Labels hinzugefügt, ohne dass alle Zeichen besucht und aktualisiert werden müssen.
Dieses offene Ende von Suffixen funktioniert jedoch aus offensichtlichen Gründen nur für Knoten, die das Ende der Zeichenfolge darstellen, dh für die Blattknoten in der Baumstruktur. Die Verzweigungsoperationen, die wir für den Baum ausführen (das Hinzufügen neuer Verzweigungsknoten und Blattknoten), werden nicht automatisch überall dort weitergegeben, wo sie benötigt werden.
Es ist wahrscheinlich elementar und erfordert keine Erwähnung, dass wiederholte Teilzeichenfolgen nicht explizit im Baum erscheinen, da der Baum diese bereits enthält, da sie Wiederholungen sind. Wenn der sich wiederholende Teilstring jedoch auf ein sich nicht wiederholendes Zeichen trifft, müssen wir an diesem Punkt eine Verzweigung erstellen, um die Divergenz von diesem Punkt an darzustellen.
Zum Beispiel muss im Fall der Zeichenfolge 'ABCXABCY' (siehe unten) eine Verzweigung zu X und Y zu drei verschiedenen Suffixen hinzugefügt werden, ABC , BC und C ; Andernfalls wäre es kein gültiger Suffixbaum, und wir könnten nicht alle Teilzeichenfolgen der Zeichenfolge finden, indem wir Zeichen von der Wurzel abwärts abgleichen.
Noch einmal, um zu betonen, dass jede Operation, die wir für ein Suffix im Baum ausführen, auch durch die aufeinanderfolgenden Suffixe (z. B. ABC> BC> C) wiedergegeben werden muss, da sie sonst einfach keine gültigen Suffixe mehr sind.
Aber selbst wenn wir akzeptieren, dass wir diese manuellen Updates durchführen müssen, woher wissen wir, wie viele Suffixe aktualisiert werden müssen? Da wir, wenn wir das wiederholte Zeichen A (und den Rest der wiederholten Zeichen nacheinander) hinzufügen , noch keine Ahnung haben, wann / wo wir das Suffix in zwei Zweige aufteilen müssen. Die Notwendigkeit der Teilung wird nur festgestellt, wenn wir auf das erste sich nicht wiederholende Zeichen stoßen, in diesem Fall Y (anstelle des X , das bereits im Baum vorhanden ist).
Was wir tun können, ist, die längste wiederholte Zeichenfolge zu finden, die wir können, und zu zählen, wie viele ihrer Suffixe wir später aktualisieren müssen. Dafür steht "Rest" .
Das Konzept von "Rest" und "erneutes Scannen"
Die Variable gibt an
remainder
, wie viele wiederholte Zeichen implizit ohne Verzweigung hinzugefügt wurden. dh wie viele Suffixe müssen wir besuchen, um den Verzweigungsvorgang zu wiederholen, sobald wir das erste Zeichen gefunden haben, mit dem wir nicht übereinstimmen können. Dies entspricht im Wesentlichen der Anzahl der Zeichen, die wir von ihrer Wurzel aus im Baum haben.Wenn wir also beim vorherigen Beispiel der Zeichenfolge ABCXABCY bleiben , stimmen wir den wiederholten ABC- Teil "implizit" ab und erhöhen ihn
remainder
jedes Mal, was zu einem Rest von 3 führt. Dann stoßen wir auf das sich nicht wiederholende Zeichen "Y" . Hier teilen wir das zuvor hinzugefügte ABCX in ABC -> X und ABC -> Y auf . Dann dekrementieren wirremainder
von 3 auf 2, weil wir uns bereits um die ABC- Verzweigung gekümmert haben . Jetzt wiederholen wir den Vorgang, indem wir die letzten 2 Zeichen - BC - von der Wurzel abgleichen , um den Punkt zu erreichen, an dem wir teilen müssen, und wir teilen BCX auch in BC-> X und BC -> Y . Wieder dekrementieren wirremainder
auf 1 und wiederholen die Operation; bis dasremainder
0 ist. Zuletzt müssen wir das aktuelle Zeichen ( Y ) selbst ebenfalls zur Wurzel hinzufügen .Diese Operation, die den aufeinanderfolgenden Suffixen von der Wurzel folgt, um einfach den Punkt zu erreichen, an dem wir eine Operation ausführen müssen, wird im Ukkonen-Algorithmus als "erneutes Scannen" bezeichnet. Dies ist normalerweise der teuerste Teil des Algorithmus. Stellen Sie sich eine längere Zeichenfolge vor, in der Sie lange Teilzeichenfolgen über viele Dutzend Knoten hinweg erneut scannen müssen (wir werden dies später besprechen), möglicherweise tausende Male.
Als Lösung führen wir sogenannte Suffix-Links ein .
Das Konzept der "Suffix-Links"
Suffix-Links verweisen im Grunde genommen auf die Positionen, an die wir normalerweise erneut scannen müssten. Anstelle des teuren Rescan-Vorgangs können wir einfach zur verknüpften Position springen, unsere Arbeit erledigen, zur nächsten verknüpften Position springen und wiederholen - bis dahin Es sind keine Positionen mehr zu aktualisieren.
Eine große Frage ist natürlich, wie man diese Links hinzufügt. Die vorhandene Antwort lautet, dass wir die Verknüpfungen hinzufügen können, wenn wir neue Verzweigungsknoten einfügen, wobei wir die Tatsache nutzen, dass in jeder Erweiterung des Baums die Verzweigungsknoten natürlich nacheinander in der genauen Reihenfolge erstellt werden, in der wir sie miteinander verknüpfen müssten . Wir müssen jedoch vom zuletzt erstellten Zweigknoten (dem längsten Suffix) mit dem zuvor erstellten verknüpfen, sodass wir den zuletzt erstellten Knoten zwischenspeichern, diesen mit dem nächsten erstellten Knoten verknüpfen und den neu erstellten zwischenspeichern müssen.
Eine Konsequenz ist, dass wir tatsächlich oft keine Suffix-Links haben, denen wir folgen müssen, weil der angegebene Verzweigungsknoten gerade erstellt wurde. In diesen Fällen müssen wir immer noch auf das oben erwähnte "erneute Scannen" von der Wurzel zurückgreifen . Aus diesem Grund werden Sie nach dem Einfügen angewiesen, entweder den Suffix-Link zu verwenden oder zum Stammverzeichnis zu springen.
(Wenn Sie übergeordnete Zeiger in den Knoten speichern, können Sie alternativ versuchen, den übergeordneten Zeigern zu folgen, zu überprüfen, ob sie einen Link haben, und diesen verwenden. Ich habe festgestellt, dass dies sehr selten erwähnt wird, die Verwendung des Suffix-Links jedoch nicht Set in Steinen. Es gibt mehrere mögliche Ansätze, und wenn man den zugrundeliegenden Mechanismus verstehen , können Sie ein implementieren , die Ihren Bedürfnissen am besten passt.)
Das Konzept des "aktiven Punktes"
Bisher haben wir mehrere effiziente Werkzeuge zum Erstellen des Baums besprochen und uns vage auf das Überqueren mehrerer Kanten und Knoten bezogen, aber die entsprechenden Konsequenzen und Komplexitäten noch nicht untersucht.
Das zuvor erläuterte Konzept des "Restes" ist nützlich, um zu verfolgen, wo wir uns im Baum befinden, aber wir müssen erkennen, dass es nicht genügend Informationen speichert.
Erstens befinden wir uns immer an einer bestimmten Kante eines Knotens, sodass wir die Kanteninformationen speichern müssen. Wir werden dies "aktive Kante" nennen .
Zweitens haben wir auch nach dem Hinzufügen der Kanteninformationen noch keine Möglichkeit, eine Position zu identifizieren, die weiter unten im Baum liegt und nicht direkt mit dem Wurzelknoten verbunden ist. Also müssen wir auch den Knoten speichern. Nennen wir diesen "aktiven Knoten" .
Schließlich können wir feststellen, dass der "Rest" nicht ausreicht, um eine Position an einer Kante zu identifizieren, die nicht direkt mit der Wurzel verbunden ist, da "Rest" die Länge der gesamten Route ist. und wir wollen uns wahrscheinlich nicht darum kümmern, die Länge der vorherigen Kanten zu merken und zu subtrahieren. Wir brauchen also eine Darstellung, die im Wesentlichen der Rest an der aktuellen Kante ist . Dies nennen wir "aktive Länge" .
Dies führt zu dem, was wir "aktiven Punkt" nennen - einem Paket von drei Variablen, die alle Informationen enthalten, die wir über unsere Position im Baum benötigen:
Active Point = (Active Node, Active Edge, Active Length)
Auf dem folgenden Bild können Sie sehen, wie die übereinstimmende Route von ABCABD aus 2 Zeichen am Rand AB (von der Wurzel ) plus 4 Zeichen am Rand CABDABCABD (vom Knoten 4) besteht - was zu einem "Rest" von 6 Zeichen führt. Unsere aktuelle Position kann also als aktiver Knoten 4, aktive Kante C, aktive Länge 4 identifiziert werden .
Eine weitere wichtige Rolle des "aktiven Punkts" besteht darin, dass er eine Abstraktionsschicht für unseren Algorithmus bereitstellt. Dies bedeutet, dass Teile unseres Algorithmus ihre Arbeit am "aktiven Punkt" ausführen können, unabhängig davon, ob sich dieser aktive Punkt in der Wurzel oder irgendwo anders befindet . Dies macht es einfach, die Verwendung von Suffix-Links in unserem Algorithmus sauber und unkompliziert zu implementieren.
Unterschiede zwischen dem erneuten Scannen und der Verwendung von Suffix-Links
Der schwierige Teil, der meiner Erfahrung nach viele Fehler und Kopfschmerzen verursachen kann und in den meisten Quellen nur unzureichend erklärt wird, ist der Unterschied in der Verarbeitung der Suffix-Link-Fälle gegenüber den Rescan-Fällen.
Betrachten Sie das folgende Beispiel für die Zeichenfolge 'AAAABAAAABAAC' :
Sie können oben beobachten, wie der 'Rest' von 7 der Gesamtsumme der Zeichen von root entspricht, während die 'aktive Länge' von 4 der Summe von übereinstimmenden Zeichen von der aktiven Kante des aktiven Knotens entspricht.
Nach dem Ausführen einer Verzweigungsoperation am aktiven Punkt enthält unser aktiver Knoten möglicherweise eine Suffix-Verknüpfung oder nicht.
Wenn ein Suffix-Link vorhanden ist: Wir müssen nur den Teil 'aktive Länge' verarbeiten . Der 'Rest' ist irrelevant, da der Knoten, zu dem wir über die Suffix-Verknüpfung springen, den impliziten 'Rest' bereits implizit codiert , einfach weil er sich in dem Baum befindet, in dem er sich befindet.
Wenn kein Suffix-Link vorhanden ist: Wir müssen von Null / Wurzel erneut scannen , was bedeutet, dass das gesamte Suffix von Anfang an verarbeitet wird. Zu diesem Zweck müssen wir den gesamten "Rest" als Grundlage für das erneute Scannen verwenden.
Beispielvergleich der Verarbeitung mit und ohne Suffix-Link
Überlegen Sie, was im nächsten Schritt des obigen Beispiels passiert. Vergleichen wir, wie Sie dasselbe Ergebnis erzielen - dh zum nächsten zu verarbeitenden Suffix wechseln - mit und ohne Suffixverknüpfung.
Verwenden des Suffix-Links
Beachten Sie, dass wir automatisch "am richtigen Ort" sind, wenn wir einen Suffix-Link verwenden. Dies ist häufig nicht unbedingt der Fall, da die "aktive Länge " mit der neuen Position "inkompatibel" sein kann.
Da die 'aktive Länge' im obigen Fall 4 beträgt, arbeiten wir mit dem Suffix ' ABAA' , beginnend mit dem verknüpften Knoten 4. Nachdem wir jedoch die Kante gefunden haben, die dem ersten Zeichen des Suffix ( 'A') entspricht. ) stellen wir fest, dass unsere 'aktive Länge' diese Kante um 3 Zeichen überschreitet. Also springen wir über die volle Kante zum nächsten Knoten und verringern die 'aktive Länge' um die Zeichen, die wir mit dem Sprung verbraucht haben.
Nachdem wir die nächste Kante 'B' gefunden haben , die dem dekrementierten Suffix 'BAA ' entspricht, stellen wir schließlich fest, dass die Kantenlänge größer ist als die verbleibende 'aktive Länge' von 3, was bedeutet, dass wir die richtige Stelle gefunden haben.
Bitte beachten Sie, dass dieser Vorgang normalerweise nicht als "erneutes Scannen" bezeichnet wird, obwohl er für mich das direkte Äquivalent zum erneuten Scannen ist, nur mit einer verkürzten Länge und einem nicht-root-Startpunkt.
Verwenden von 'Rescan'
Beachten Sie, dass wir, wenn wir eine herkömmliche 'Rescan'-Operation verwenden (hier so tun, als hätten wir keinen Suffix-Link), am oberen Rand des Baums, an der Wurzel, beginnen und uns wieder nach unten an die richtige Stelle arbeiten müssen. entlang der gesamten Länge des aktuellen Suffixes folgen.
Die Länge dieses Suffixes ist der 'Rest', den wir zuvor besprochen haben. Wir müssen den gesamten Rest verbrauchen, bis er Null erreicht. Dies kann (und oft auch) das Springen durch mehrere Knoten beinhalten, wobei bei jedem Sprung der Rest um die Länge der Kante verringert wird, durch die wir gesprungen sind. Dann erreichen wir endlich eine Kante, die länger ist als unser verbleibender "Rest" ; Hier setzen wir die aktive Kante auf die angegebene Kante, setzen 'aktive Länge' auf den verbleibenden 'Rest ' und fertig.
Beachten Sie jedoch, dass die tatsächliche 'Rest'- Variable beibehalten und erst nach jedem Einfügen eines Knotens dekrementiert werden muss. Was ich oben beschrieben habe, ging also davon aus, dass eine separate Variable verwendet wurde, die auf "Rest" initialisiert wurde .
Hinweise zu Suffix-Links und Rescans
1) Beachten Sie, dass beide Methoden zum gleichen Ergebnis führen. Das Suffix-Link-Jumping ist jedoch in den meisten Fällen erheblich schneller. Das ist die ganze Begründung hinter Suffix-Links.
2) Die tatsächlichen algorithmischen Implementierungen müssen sich nicht unterscheiden. Wie oben erwähnt, ist die 'aktive Länge' selbst bei Verwendung der Suffix-Verknüpfung häufig nicht mit der verknüpften Position kompatibel, da dieser Zweig des Baums möglicherweise zusätzliche Verzweigungen enthält. Im Wesentlichen müssen Sie also nur "aktive Länge" anstelle von "Rest" verwenden und dieselbe Rescan-Logik ausführen, bis Sie eine Kante finden, die kürzer als Ihre verbleibende Suffixlänge ist.
3) Eine wichtige Bemerkung zur Leistung ist, dass nicht jedes einzelne Zeichen während des erneuten Scannens überprüft werden muss. Aufgrund der Art und Weise, wie ein gültiger Suffixbaum erstellt wird, können wir davon ausgehen, dass die Zeichen übereinstimmen. Sie zählen also meistens die Längen, und die einzige Notwendigkeit für die Überprüfung der Zeichenäquivalenz entsteht, wenn wir zu einer neuen Kante springen, da Kanten durch ihr erstes Zeichen identifiziert werden (das im Kontext eines bestimmten Knotens immer eindeutig ist). Dies bedeutet, dass sich die Logik des erneuten Scannens von der Logik für die vollständige Zeichenfolgenanpassung unterscheidet (dh die Suche nach einer Teilzeichenfolge im Baum).
4) Die hier beschriebene ursprüngliche Suffixverknüpfung ist nur einer der möglichen Ansätze . Zum Beispiel NJ Larsson et al. bezeichnet diesen Ansatz als knotenorientiertes Top-Down und vergleicht ihn mit knotenorientiertem Bottom-Up und zwei kantenorientierten Sorten. Die verschiedenen Ansätze haben unterschiedliche typische und Worst-Case-Leistungen, Anforderungen, Einschränkungen usw., aber es scheint im Allgemeinen, dass kantenorientierte Ansätze eine allgemeine Verbesserung gegenüber dem Original darstellen.
quelle
@jogojapan du hast tolle Erklärungen und Visualisierungen mitgebracht. Aber wie @makagonov erwähnte, fehlen einige Regeln zum Setzen von Suffix-Links. Es ist gut sichtbar, wenn Sie Schritt für Schritt auf http://brenden.github.io/ukkonen-animation/ durch das Wort "aabaaabb" gehen. Wenn Sie von Schritt 10 zu Schritt 11 gehen, gibt es keine Suffix-Verbindung von Knoten 5 zu Knoten 2, aber der aktive Punkt bewegt sich plötzlich dorthin.
@makagonov Da ich in der Java-Welt lebe, habe ich auch versucht, Ihrer Implementierung zu folgen, um den ST-Build-Workflow zu verstehen, aber es war schwierig für mich aus folgenden Gründen:
Am Ende hatte ich eine solche Implementierung in Java, die hoffentlich alle Schritte klarer widerspiegelt und die Lernzeit für andere Java-Leute verkürzt:
quelle
Meine Intuition ist wie folgt:
Nach k Iterationen der Hauptschleife haben Sie einen Suffixbaum erstellt, der alle Suffixe der vollständigen Zeichenfolge enthält, die mit den ersten k Zeichen beginnen.
Zu Beginn bedeutet dies, dass der Suffixbaum einen einzelnen Stammknoten enthält, der die gesamte Zeichenfolge darstellt (dies ist das einzige Suffix, das bei 0 beginnt).
Nach len (string) Iterationen haben Sie einen Suffixbaum, der alle Suffixe enthält.
Während der Schleife ist der Schlüssel der aktive Punkt. Ich vermute, dass dies den tiefsten Punkt im Suffixbaum darstellt, der einem richtigen Suffix der ersten k Zeichen der Zeichenfolge entspricht. (Ich denke, richtig bedeutet, dass das Suffix nicht die gesamte Zeichenfolge sein kann.)
Angenommen, Sie haben Zeichen 'abcabc' gesehen. Der aktive Punkt würde den Punkt im Baum darstellen, der dem Suffix 'abc' entspricht.
Der aktive Punkt wird durch (Ursprung, erster, letzter) dargestellt. Dies bedeutet, dass Sie sich derzeit an der Stelle im Baum befinden, zu der Sie gelangen, indem Sie am Knotenursprung beginnen und dann die Zeichen in der Zeichenfolge [first: last] eingeben.
Wenn Sie ein neues Zeichen hinzufügen, prüfen Sie, ob sich der aktive Punkt noch im vorhandenen Baum befindet. Wenn ja, sind Sie fertig. Andernfalls müssen Sie am aktiven Punkt einen neuen Knoten zum Suffixbaum hinzufügen, auf die nächst kürzere Übereinstimmung zurückgreifen und erneut prüfen.
Anmerkung 1: Die Suffixzeiger geben einen Link zur nächst kürzeren Übereinstimmung für jeden Knoten.
Hinweis 2: Wenn Sie einen neuen Knoten und einen Fallback hinzufügen, fügen Sie einen neuen Suffixzeiger für den neuen Knoten hinzu. Das Ziel für diesen Suffixzeiger ist der Knoten am verkürzten aktiven Punkt. Dieser Knoten ist entweder bereits vorhanden oder wird bei der nächsten Iteration dieser Fallback-Schleife erstellt.
Hinweis 3: Der Heiligsprechungsteil spart einfach Zeit bei der Überprüfung des aktiven Punktes. Angenommen, Sie haben immer origin = 0 verwendet und nur zuerst und zuletzt geändert. Um den aktiven Punkt zu überprüfen, müssten Sie jedes Mal dem Suffixbaum entlang aller Zwischenknoten folgen. Es ist sinnvoll, das Ergebnis der Verfolgung dieses Pfads zwischenzuspeichern, indem nur die Entfernung vom letzten Knoten aufgezeichnet wird.
Können Sie ein Codebeispiel dafür geben, was Sie unter "Fixieren" von Begrenzungsvariablen verstehen?
Gesundheitswarnung: Ich fand diesen Algorithmus auch besonders schwer zu verstehen. Bitte beachten Sie, dass diese Intuition wahrscheinlich in allen wichtigen Details falsch ist ...
quelle
Hallo, ich habe versucht, die oben erläuterte Implementierung in Ruby zu implementieren. Bitte probieren Sie es aus. es scheint gut zu funktionieren.
Der einzige Unterschied in der Implementierung ist, dass ich versucht habe, das Kantenobjekt zu verwenden, anstatt nur Symbole zu verwenden.
Es ist auch unter https://gist.github.com/suchitpuri/9304856 vorhanden
quelle