Ukkonens Suffixbaum-Algorithmus im Klartext

1101

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:

https://gist.github.com/2373868


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 chalkvom 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

Nathan Ridley
quelle
2
Haben Sie sich die Beschreibung in Dan Gusfields Buch angesehen ? Ich fand das hilfreich.
Jogojapan
4
Das Wesentliche gibt die Lizenz nicht an - kann ich Ihren Code ändern und unter MIT erneut veröffentlichen (offensichtlich mit Zuschreibungen)?
Yurik
2
Ja, geh für dein Leben. Betrachten Sie es als gemeinfrei. Wie in einer anderen Antwort auf dieser Seite erwähnt, gibt es einen Fehler, der ohnehin behoben werden muss.
Nathan Ridley
1
Vielleicht hilft diese Implementierung anderen, gehe zu code.google.com/p/text-indexing
cos
2
"Betrachten Sie es als gemeinfrei" ist vielleicht überraschend eine sehr wenig hilfreiche Antwort. Der Grund ist, dass es für Sie tatsächlich unmöglich ist, die Arbeit öffentlich zugänglich zu machen. Daher unterstreicht Ihr Kommentar "Betrachten Sie es ..." die Tatsache, dass die Lizenz unklar ist, und gibt dem Leser Grund zu Zweifel, dass der Status der Arbeit für Sie tatsächlich klar ist . Wenn Sie möchten, dass Personen Ihren Code verwenden können, geben Sie bitte eine Lizenz dafür an, wählen Sie eine beliebige Lizenz aus (aber, sofern Sie kein Anwalt sind, wählen Sie eine bereits vorhandene Lizenz!)
James Youngman

Antworten:

2377

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.

  1. 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

  2. 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:

abc

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, aindem 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 danach a).

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 durch

  • Erweiterung des bestehenden aAngebots aufab
  • Einfügen einer neuen Kante für b

In unserer Darstellung sieht das so aus

Geben Sie hier die Bildbeschreibung ein

Und was es bedeutet ist:

Wir beobachten zwei Dinge:

  • Die Kantendarstellung für abist 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 .
  • Jede Kante belegt O (1) Leerzeichen, da sie nur aus zwei Zeigern im Text besteht, unabhängig davon, wie viele Zeichen sie darstellt.

Als nächstes erhöhen wir die Position erneut und aktualisieren den Baum, indem wir can jede vorhandene Kante ein anhängen und eine neue Kante für das neue Suffix einfügen c.

In unserer Darstellung sieht das so aus

Und was es bedeutet ist:

Wir beobachten:

  • Der Baum ist nach jedem Schritt der richtige Suffixbaum bis zur aktuellen Position
  • Es gibt so viele Schritte wie Zeichen im Text
  • Der Arbeitsaufwand in jedem Schritt beträgt O (1), da alle vorhandenen Kanten automatisch durch Inkrementieren aktualisiert werden #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:

abcabxabcd

Es beginnt mit abcwie im vorherigen Beispiel, wird dann abwiederholt und gefolgt von xund wird dann abcwiederholt, gefolgt von d.

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 aan 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:

  • Der aktive Punkt , der ein Tripel ist (active_node,active_edge,active_length)
  • Das remainderist eine Ganzzahl, die angibt, wie viele neue Suffixe eingefügt werden müssen

Die genaue Bedeutung dieser beiden wird bald klar, aber jetzt sagen wir einfach:

  • In dem einfachen abcBeispiel war der aktive Punkt immer (root,'\0x',0), dh active_nodeder Wurzelknoten, active_edgewurde als Nullzeichen angegeben '\0x'und active_lengthwar 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.
  • Das remainderwurde 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 aan der Wurzel einfügen , stellen wir fest, dass bereits eine ausgehende Kante beginnt a, insbesondere mit : abca. Folgendes tun wir in einem solchen Fall:

  • Wir fügen keine neue Kante [4,#]am Wurzelknoten ein. Stattdessen bemerken wir einfach, dass das Suffix abereits 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.
  • Wir setzen den aktiven Punkt auf (root,'a',1). Das bedeutet, dass sich der aktive Punkt jetzt irgendwo in der Mitte der ausgehenden Kante des Wurzelknotens befindet a, die speziell nach Position 1 an dieser Kante beginnt . Wir bemerken, dass die Kante einfach durch ihr erstes Zeichen angegeben wird a. 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).
  • Wir erhöhen auch 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 remainderes 2 ist , müssen wir zwei letzte Suffixe der aktuellen Position einfügen: abund b. Dies ist im Grunde, weil:

  • Das aSuffix aus dem vorherigen Schritt wurde nie richtig eingefügt. So ist es geblieben , und seit wir einen Schritt vorangekommen sind, ist es jetzt von abis gewachsen ab.
  • Und wir müssen die neue Endkante einfügen b.

In der Praxis bedeutet dies, dass wir zum aktiven Punkt gehen (der hinter adie abcabKante zeigt) und das aktuelle Endzeichen einfügen b. Aber: Es stellt sich wieder heraus, dass bauch schon an derselben Kante vorhanden ist.

Also ändern wir den Baum nicht. Wir einfach:

  • Aktualisieren Sie den aktiven Punkt auf (root,'a',2)(den gleichen Knoten und die gleiche Kante wie zuvor, aber jetzt zeigen wir auf hinter den b)
  • Erhöhen Sie die Zahl remainderauf 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 abund bim aktuellen Schritt, aber da abbereits gefunden, haben wir den aktiven Punkt aktualisiert und nicht einmal versucht einzufügen b. Warum? Denn wenn abes im Baum ist, muss jedes Suffix (einschließlich b) 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 remainder3 ist , müssen wir einfügen abx, bxund x. Der aktive Punkt sagt uns, wo abendet, also müssen wir nur dorthin springen und das einfügen x. In der Tat xist es noch nicht da, also teilen wir die abcabxKante 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 abxund remainderauf 2 dekrementiert . Jetzt müssen wir das nächste verbleibende Suffix einfügen bx. 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 die active_nodeWurzel ist (wir lernen Regel 3 für andere Fälle weiter unten). Hier ist Regel 1:

Nach einem Einfügen von root,

  • active_node bleibt Wurzel
  • active_edge wird auf das erste Zeichen des neuen Suffix gesetzt, das eingefügt werden muss, d. h b
  • active_length wird um 1 reduziert

Daher zeigt das neue Aktivpunkt-Tripel (root,'b',1)an, dass die nächste Einfügung am bcabxRand hinter 1 Zeichen, dh hinter, erfolgen muss b. Wir können die Einfügemarke in O (1) -Zeit identifizieren und prüfen, ob sie xbereits vorhanden ist oder nicht. Wenn es vorhanden wäre, würden wir den aktuellen Schritt beenden und alles so lassen, wie es ist. Ist x aber nicht vorhanden, so fügen wir es durch Teilen der Kante ein:

Auch dies dauerte O (1) Zeit und wir aktualisieren remainderauf 1 und der aktive Punkt auf (root,'x',0)Regel 1.

Aber wir müssen noch etwas tun. Wir nennen das Regel 2:

Wenn wir eine Kante teilen und einen neuen Knoten einfügen und dies nicht der erste Knoten ist, der im aktuellen Schritt erstellt wurde, verbinden wir den zuvor eingefügten Knoten und den neuen Knoten über einen speziellen Zeiger, einen Suffix-Link . Wir werden später sehen, warum das nützlich ist. Folgendes erhalten wir: Der Suffix-Link wird als gepunktete Kante dargestellt:

Wir müssen noch das letzte Suffix des aktuellen Schritts einfügen x. Da die active_lengthKomponente des aktiven Knotens auf 0 gefallen ist, erfolgt die endgültige Einfügung direkt an der Wurzel. Da es am Wurzelknoten keine ausgehende Kante gibt x, 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 Zeichen awie 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 an b, und wie zuvor gesehen bedeutet dies nur, dass wir den aktiven Punkt aktualisieren (root,'a',2)und erhöhen, remainderohne etwas anderes zu tun, da dies bbereits 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). Hier node1beziehe ich mich auf den internen Knoten, an dem die abKante 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 #Update cautomatisch 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 ihn remainderund tun nichts anderes.

Nun in Schritt #= 10 , remainder4, und so wir zunächst eingelegt werden muss abcd(die von 3 Schritten vor bleibt) durch Einfügen dan der aktiven Stelle.

Der Versuch, dam 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:

Nachdem active_nodewir eine Kante von einem Knoten getrennt haben, der nicht der Stammknoten ist, folgen wir dem Suffix-Link, der von diesem Knoten ausgeht, falls vorhanden, und setzen ihn active_nodeauf den Knoten zurück, auf den er zeigt. Wenn es keinen Suffix-Link gibt, setzen wir den active_nodeauf den Stamm. active_edge und active_lengthbleiben unverändert.

Der aktive Punkt ist also jetzt (node2,'c',1)und node2wird unten rot markiert:

Da das Einfügen von abcdabgeschlossen ist, dekrementieren wir remainderauf 3 und betrachten das nächste verbleibende Suffix des aktuellen Schritts bcd. Regel 3 hat den aktiven Punkt genau auf den richtigen Knoten und die richtige Kante gesetzt, sodass das Einfügen bcddurch einfaches Einfügen des endgültigen Zeichens dam 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 abmit dem Knoten bei b(seinem Suffix) und dem Knoten bei abcverknüpft ist bc.

Der aktuelle Schritt ist noch nicht abgeschlossen. remainderist jetzt 2 und wir müssen Regel 3 folgen, um den aktiven Punkt wieder zurückzusetzen. Da der Strom active_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: cabxabcdhinter dem ersten Zeichen, dh hinter c. 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 remainderkann auf 1 gesetzt werden und da das active_noderoot 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 einzelne d 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.

  • remaindergibt 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 remainderden 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, dass remainder> 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. Erzwingenremainder0 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 remainderEinfügungen ein, die jeweils O (1) Zeit in Anspruch nehmen. Da remainderangibt, 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_lengthKomponente mit dem neuen nicht gut funktioniert active_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 hinter fdem defgRand 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). Das dVerlassen des grünen Knotens ist jedoch deso, 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_lengthkönnte das so groß sein wie remainder, 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 Schritt remainderim 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_lengthreduziert wird. Wenn wir der Kette der Suffix-Glieder folgen, nehmen wir die verbleibenden Einfügungen vor, active_lengthkönnen nur abnehmen, und die Anzahl der aktiven Punktanpassungen, die wir unterwegs vornehmen können, kann nicht größer sein als active_lengthzu einem bestimmten Zeitpunkt. Da active_lengthniemals größer sein remainderkann und remainder nicht nur in jedem einzelnen Schritt O (n) ist, sondern die Gesamtsumme der jemals remainderim Verlauf des gesamten Prozesses vorgenommenen Inkremente auch O (n) ist, beträgt die Anzahl der aktiven Punktanpassungen auch begrenzt durch O (n).

jogojapan
quelle
74
Tut mir leid, dass das etwas länger gedauert hat, als ich gehofft hatte. Und mir ist klar, dass es eine Reihe von trivialen Dingen erklärt, die wir alle kennen, während die schwierigen Teile möglicherweise noch nicht ganz klar sind. Lassen Sie es uns gemeinsam in Form bringen.
Jogojapan
68
Und ich sollte hinzufügen, dass dies nicht auf der Beschreibung in Dan Gusfields Buch basiert. Es ist ein neuer Versuch, den Algorithmus zu beschreiben, indem zuerst eine Zeichenfolge ohne Wiederholungen betrachtet und dann diskutiert wird, wie Wiederholungen behandelt werden. Ich hoffte, dass das intuitiver sein würde.
Jogojapan
8
Dank @jogojapan konnte ich dank Ihrer Erklärung ein voll funktionsfähiges Beispiel schreiben. Ich habe die Quelle veröffentlicht, damit sie hoffentlich von jemand anderem verwendet werden kann: gist.github.com/2373868
Nathan Ridley
4
@ NathanRidley Ja (das letzte Bit nennt Ukkonen übrigens kanonisieren). Eine Möglichkeit, dies auszulösen, besteht darin, sicherzustellen, dass ein Teilstring dreimal vorkommt und in einer Zeichenfolge endet, die in einem anderen Kontext noch einmal vorkommt. ZB abcdefabxybcdmnabcdex. Der erste Teil von abcdwird in abxy(dies erzeugt einen internen Knoten nach ab) und erneut in wiederholt abcdexund endet in bcd, was nicht nur im bcdexKontext, sondern auch im bcdmnKontext erscheint. Nach dem abcdexEinfügen folgen wir dem Suffix-Link zum Einfügen bcdex, und dort wird canonicize
aktiviert
6
Ok, mein Code wurde komplett neu geschrieben und funktioniert jetzt in allen Fällen korrekt, einschließlich der automatischen Heiligsprechung. Außerdem hat er eine viel schönere Textgrafikausgabe. gist.github.com/2373868
Nathan Ridley
132

Ich 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

  1. aktiver Punkt - ein Tripel (active_node; active_edge; active_length), das zeigt, von wo aus wir mit dem Einfügen eines neuen Suffixes beginnen müssen.
  2. Rest - Zeigt die Anzahl der Suffixe an, die explizit hinzugefügt werden müssen . Wenn unser Wort beispielsweise "abcaabca" lautet und "rest = 3", bedeutet dies, dass wir drei letzte Suffixe verarbeiten müssen: bca , ca und a .

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 pointund remainder).

Beobachtung 2

Wenn irgendwann active_lengthgrößer oder gleich der Länge der aktuellen Kante ( edge_length) ist, bewegen wir uns nach active pointunten, bis sie edge_lengthstreng größer als ist active_length.

Definieren wir nun die Regeln neu:

Regel 1

Wenn nach einem Einfügen vom aktiven Knoten = root die aktive Länge größer als 0 ist, dann:

  1. aktiver Knoten wird nicht geändert
  2. Die aktive Länge wird dekrementiert
  3. Die aktive Kante wird nach rechts verschoben (zum ersten Zeichen des nächsten Suffix, das eingefügt werden muss).

Regel 2

Wenn wir einen neuen internen Knoten erstellen ODER einen Inserter von einem internen Knoten erstellen und dies im aktuellen Schritt nicht der erste SUCH- interne Knoten ist , verknüpfen wir den vorherigen SUCH- Knoten über einen Suffix-Link mit DIESEM .

Diese Definition von Rule 2unterscheidet 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

Nach einem Einsatz aus dem aktiven Knoten , der nicht der ist Wurzelknoten, müssen wir das Suffix Verbindung und stellen die folgen aktiven Knoten zum Knoten es auf Punkte an . Wenn es keine Verbindung ein Suffix ist, den aktiven Knoten zum Wurzelknoten. In beiden Fällen bleiben die aktive Kante und die aktive Länge unverändert.

In dieser Definition von betrachten Rule 3wir 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äß nur active pointund remainderlassen den Baum unverändert. ABER wenn es einen internen Knoten gibt, der als Suffix-Link erforderlich markiert ist , müssen wir diesen Knoten active 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:

  1. Wenn wir die Knoten NICHT über eine Suffix-Verbindung verbinden:

    • vor dem Hinzufügen des letzten Buchstabens c :

    • nach dem Hinzufügen des letzten Buchstabens c :

  2. Wenn wir DO die Knoten durch einen Suffix Link verbinden:

    • vor dem Hinzufügen des letzten Buchstabens c :

    • nach dem Hinzufügen des letzten Buchstabens c :

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 3active_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 nodeauf 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:

  1. Java
  2. C ++

Ich hoffe, dass diese "Übersicht" in Kombination mit der detaillierten Antwort von jogojapan jemandem hilft, seinen eigenen Suffixbaum zu implementieren.

Makagonov
quelle
3
Vielen Dank und +1 für Ihre Bemühungen. Ich bin sicher, Sie haben Recht ... obwohl ich nicht die Zeit habe, sofort über die Details nachzudenken. Ich werde es später überprüfen und möglicherweise auch meine Antwort ändern.
Jogojapan
Vielen Dank, es hat wirklich geholfen. Könnten Sie jedoch genauer auf Beobachtung 3 eingehen? Geben Sie beispielsweise die Diagramme der beiden Schritte an, in denen der neue Suffix-Link eingeführt wird. Ist der Knoten mit dem aktiven Knoten verbunden? (da wir den 2. Knoten nicht wirklich einfügen)
Dyesdyes
@makagonov Hey, kannst du mir helfen, einen Suffixbaum für deine Zeichenfolge "cdddcdc" zu erstellen? Ich bin etwas verwirrt dabei (die ersten Schritte).
Tariq Zafar
3
Wie bei Regel 3 besteht eine clevere Möglichkeit darin, die Suffix-Verknüpfung von root auf root selbst und (standardmäßig) die Suffix-Verknüpfung jedes Knotens auf root festzulegen. So können wir die Konditionierung vermeiden und einfach dem Suffix-Link folgen.
sqd
1
aabaacaadDies 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 finden active_node.
IvanaGyro
10

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:

  1. Ende mitRemainder > 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.

  2. 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.

    Geben Sie hier die Bildbeschreibung ein

Auf die beiden anderen Probleme hat @managonov irgendwie hingewiesen

  1. 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.

  2. 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:

Tipps: Der obige Code enthält eine naive Baumdruckfunktion , die beim Debuggen sehr wichtig ist . Das hat mir viel Zeit gespart und ist praktisch, um Sonderfälle zu finden.

mutux
quelle
10

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.

Wiederholte Verzweigung in Suffixen

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 remainderjedes 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 wir remaindervon 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 wir remainderauf 1 und wiederholen die Operation; bis das remainder0 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 .

Rest und aktiver Punkt

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' :

Rest über mehrere Kanten

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

Erreichen aufeinanderfolgender Suffixe über 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'

Erreichen aufeinanderfolgender Suffixe durch erneutes Scannen

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.

MrValueType
quelle
8

@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:

  • Kanten mit Knoten kombinieren
  • Verwenden von Indexzeigern anstelle von Referenzen
  • bricht Aussagen;
  • Aussagen fortsetzen;

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:

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class ST {

  public class Node {
    private final int id;
    private final Map<Character, Edge> edges;
    private Node slink;

    public Node(final int id) {
        this.id = id;
        this.edges = new HashMap<>();
    }

    public void setSlink(final Node slink) {
        this.slink = slink;
    }

    public Map<Character, Edge> getEdges() {
        return this.edges;
    }

    public Node getSlink() {
        return this.slink;
    }

    public String toString(final String word) {
        return new StringBuilder()
                .append("{")
                .append("\"id\"")
                .append(":")
                .append(this.id)
                .append(",")
                .append("\"slink\"")
                .append(":")
                .append(this.slink != null ? this.slink.id : null)
                .append(",")
                .append("\"edges\"")
                .append(":")
                .append(edgesToString(word))
                .append("}")
                .toString();
    }

    private StringBuilder edgesToString(final String word) {
        final StringBuilder edgesStringBuilder = new StringBuilder();
        edgesStringBuilder.append("{");
        for(final Map.Entry<Character, Edge> entry : this.edges.entrySet()) {
            edgesStringBuilder.append("\"")
                    .append(entry.getKey())
                    .append("\"")
                    .append(":")
                    .append(entry.getValue().toString(word))
                    .append(",");
        }
        if(!this.edges.isEmpty()) {
            edgesStringBuilder.deleteCharAt(edgesStringBuilder.length() - 1);
        }
        edgesStringBuilder.append("}");
        return edgesStringBuilder;
    }

    public boolean contains(final String word, final String suffix) {
        return !suffix.isEmpty()
                && this.edges.containsKey(suffix.charAt(0))
                && this.edges.get(suffix.charAt(0)).contains(word, suffix);
    }
  }

  public class Edge {
    private final int from;
    private final int to;
    private final Node next;

    public Edge(final int from, final int to, final Node next) {
        this.from = from;
        this.to = to;
        this.next = next;
    }

    public int getFrom() {
        return this.from;
    }

    public int getTo() {
        return this.to;
    }

    public Node getNext() {
        return this.next;
    }

    public int getLength() {
        return this.to - this.from;
    }

    public String toString(final String word) {
        return new StringBuilder()
                .append("{")
                .append("\"content\"")
                .append(":")
                .append("\"")
                .append(word.substring(this.from, this.to))
                .append("\"")
                .append(",")
                .append("\"next\"")
                .append(":")
                .append(this.next != null ? this.next.toString(word) : null)
                .append("}")
                .toString();
    }

    public boolean contains(final String word, final String suffix) {
        if(this.next == null) {
            return word.substring(this.from, this.to).equals(suffix);
        }
        return suffix.startsWith(word.substring(this.from,
                this.to)) && this.next.contains(word, suffix.substring(this.to - this.from));
    }
  }

  public class ActivePoint {
    private final Node activeNode;
    private final Character activeEdgeFirstCharacter;
    private final int activeLength;

    public ActivePoint(final Node activeNode,
                       final Character activeEdgeFirstCharacter,
                       final int activeLength) {
        this.activeNode = activeNode;
        this.activeEdgeFirstCharacter = activeEdgeFirstCharacter;
        this.activeLength = activeLength;
    }

    private Edge getActiveEdge() {
        return this.activeNode.getEdges().get(this.activeEdgeFirstCharacter);
    }

    public boolean pointsToActiveNode() {
        return this.activeLength == 0;
    }

    public boolean activeNodeIs(final Node node) {
        return this.activeNode == node;
    }

    public boolean activeNodeHasEdgeStartingWith(final char character) {
        return this.activeNode.getEdges().containsKey(character);
    }

    public boolean activeNodeHasSlink() {
        return this.activeNode.getSlink() != null;
    }

    public boolean pointsToOnActiveEdge(final String word, final char character) {
        return word.charAt(this.getActiveEdge().getFrom() + this.activeLength) == character;
    }

    public boolean pointsToTheEndOfActiveEdge() {
        return this.getActiveEdge().getLength() == this.activeLength;
    }

    public boolean pointsAfterTheEndOfActiveEdge() {
        return this.getActiveEdge().getLength() < this.activeLength;
    }

    public ActivePoint moveToEdgeStartingWithAndByOne(final char character) {
        return new ActivePoint(this.activeNode, character, 1);
    }

    public ActivePoint moveToNextNodeOfActiveEdge() {
        return new ActivePoint(this.getActiveEdge().getNext(), null, 0);
    }

    public ActivePoint moveToSlink() {
        return new ActivePoint(this.activeNode.getSlink(),
                this.activeEdgeFirstCharacter,
                this.activeLength);
    }

    public ActivePoint moveTo(final Node node) {
        return new ActivePoint(node, this.activeEdgeFirstCharacter, this.activeLength);
    }

    public ActivePoint moveByOneCharacter() {
        return new ActivePoint(this.activeNode,
                this.activeEdgeFirstCharacter,
                this.activeLength + 1);
    }

    public ActivePoint moveToEdgeStartingWithAndByActiveLengthMinusOne(final Node node,
                                                                       final char character) {
        return new ActivePoint(node, character, this.activeLength - 1);
    }

    public ActivePoint moveToNextNodeOfActiveEdge(final String word, final int index) {
        return new ActivePoint(this.getActiveEdge().getNext(),
                word.charAt(index - this.activeLength + this.getActiveEdge().getLength()),
                this.activeLength - this.getActiveEdge().getLength());
    }

    public void addEdgeToActiveNode(final char character, final Edge edge) {
        this.activeNode.getEdges().put(character, edge);
    }

    public void splitActiveEdge(final String word,
                                final Node nodeToAdd,
                                final int index,
                                final char character) {
        final Edge activeEdgeToSplit = this.getActiveEdge();
        final Edge splittedEdge = new Edge(activeEdgeToSplit.getFrom(),
                activeEdgeToSplit.getFrom() + this.activeLength,
                nodeToAdd);
        nodeToAdd.getEdges().put(word.charAt(activeEdgeToSplit.getFrom() + this.activeLength),
                new Edge(activeEdgeToSplit.getFrom() + this.activeLength,
                        activeEdgeToSplit.getTo(),
                        activeEdgeToSplit.getNext()));
        nodeToAdd.getEdges().put(character, new Edge(index, word.length(), null));
        this.activeNode.getEdges().put(this.activeEdgeFirstCharacter, splittedEdge);
    }

    public Node setSlinkTo(final Node previouslyAddedNodeOrAddedEdgeNode,
                           final Node node) {
        if(previouslyAddedNodeOrAddedEdgeNode != null) {
            previouslyAddedNodeOrAddedEdgeNode.setSlink(node);
        }
        return node;
    }

    public Node setSlinkToActiveNode(final Node previouslyAddedNodeOrAddedEdgeNode) {
        return setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, this.activeNode);
    }
  }

  private static int idGenerator;

  private final String word;
  private final Node root;
  private ActivePoint activePoint;
  private int remainder;

  public ST(final String word) {
    this.word = word;
    this.root = new Node(idGenerator++);
    this.activePoint = new ActivePoint(this.root, null, 0);
    this.remainder = 0;
    build();
  }

  private void build() {
    for(int i = 0; i < this.word.length(); i++) {
        add(i, this.word.charAt(i));
    }
  }

  private void add(final int index, final char character) {
    this.remainder++;
    boolean characterFoundInTheTree = false;
    Node previouslyAddedNodeOrAddedEdgeNode = null;
    while(!characterFoundInTheTree && this.remainder > 0) {
        if(this.activePoint.pointsToActiveNode()) {
            if(this.activePoint.activeNodeHasEdgeStartingWith(character)) {
                activeNodeHasEdgeStartingWithCharacter(character, previouslyAddedNodeOrAddedEdgeNode);
                characterFoundInTheTree = true;
            }
            else {
                if(this.activePoint.activeNodeIs(this.root)) {
                    rootNodeHasNotEdgeStartingWithCharacter(index, character);
                }
                else {
                    previouslyAddedNodeOrAddedEdgeNode = internalNodeHasNotEdgeStartingWithCharacter(index,
                            character, previouslyAddedNodeOrAddedEdgeNode);
                }
            }
        }
        else {
            if(this.activePoint.pointsToOnActiveEdge(this.word, character)) {
                activeEdgeHasCharacter();
                characterFoundInTheTree = true;
            }
            else {
                if(this.activePoint.activeNodeIs(this.root)) {
                    previouslyAddedNodeOrAddedEdgeNode = edgeFromRootNodeHasNotCharacter(index,
                            character,
                            previouslyAddedNodeOrAddedEdgeNode);
                }
                else {
                    previouslyAddedNodeOrAddedEdgeNode = edgeFromInternalNodeHasNotCharacter(index,
                            character,
                            previouslyAddedNodeOrAddedEdgeNode);
                }
            }
        }
    }
  }

  private void activeNodeHasEdgeStartingWithCharacter(final char character,
                                                    final Node previouslyAddedNodeOrAddedEdgeNode) {
    this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
    this.activePoint = this.activePoint.moveToEdgeStartingWithAndByOne(character);
    if(this.activePoint.pointsToTheEndOfActiveEdge()) {
        this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
    }
  }

  private void rootNodeHasNotEdgeStartingWithCharacter(final int index, final char character) {
    this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
    this.activePoint = this.activePoint.moveTo(this.root);
    this.remainder--;
    assert this.remainder == 0;
  }

  private Node internalNodeHasNotEdgeStartingWithCharacter(final int index,
                                                         final char character,
                                                         Node previouslyAddedNodeOrAddedEdgeNode) {
    this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
    if(this.activePoint.activeNodeHasSlink()) {
        this.activePoint = this.activePoint.moveToSlink();
    }
    else {
        this.activePoint = this.activePoint.moveTo(this.root);
    }
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private void activeEdgeHasCharacter() {
    this.activePoint = this.activePoint.moveByOneCharacter();
    if(this.activePoint.pointsToTheEndOfActiveEdge()) {
        this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
    }
  }

  private Node edgeFromRootNodeHasNotCharacter(final int index,
                                             final char character,
                                             Node previouslyAddedNodeOrAddedEdgeNode) {
    final Node newNode = new Node(idGenerator++);
    this.activePoint.splitActiveEdge(this.word, newNode, index, character);
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
    this.activePoint = this.activePoint.moveToEdgeStartingWithAndByActiveLengthMinusOne(this.root,
            this.word.charAt(index - this.remainder + 2));
    this.activePoint = walkDown(index);
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private Node edgeFromInternalNodeHasNotCharacter(final int index,
                                                 final char character,
                                                 Node previouslyAddedNodeOrAddedEdgeNode) {
    final Node newNode = new Node(idGenerator++);
    this.activePoint.splitActiveEdge(this.word, newNode, index, character);
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
    if(this.activePoint.activeNodeHasSlink()) {
        this.activePoint = this.activePoint.moveToSlink();
    }
    else {
        this.activePoint = this.activePoint.moveTo(this.root);
    }
    this.activePoint = walkDown(index);
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private ActivePoint walkDown(final int index) {
    while(!this.activePoint.pointsToActiveNode()
            && (this.activePoint.pointsToTheEndOfActiveEdge() || this.activePoint.pointsAfterTheEndOfActiveEdge())) {
        if(this.activePoint.pointsAfterTheEndOfActiveEdge()) {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge(this.word, index);
        }
        else {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
        }
    }
    return this.activePoint;
  }

  public String toString(final String word) {
    return this.root.toString(word);
  }

  public boolean contains(final String suffix) {
    return this.root.contains(this.word, suffix);
  }

  public static void main(final String[] args) {
    final String[] words = {
            "abcabcabc$",
            "abc$",
            "abcabxabcd$",
            "abcabxabda$",
            "abcabxad$",
            "aabaaabb$",
            "aababcabcd$",
            "ababcabcd$",
            "abccba$",
            "mississipi$",
            "abacabadabacabae$",
            "abcabcd$",
            "00132220$"
    };
    Arrays.stream(words).forEach(word -> {
        System.out.println("Building suffix tree for word: " + word);
        final ST suffixTree = new ST(word);
        System.out.println("Suffix tree: " + suffixTree.toString(word));
        for(int i = 0; i < word.length() - 1; i++) {
            assert suffixTree.contains(word.substring(i)) : word.substring(i);
        }
    });
  }
}
Kamil
quelle
6

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 ...

Peter de Rivaz
quelle
In einer der wissenschaftlichen Arbeiten wird "richtig" so definiert, dass das "richtige Suffix" einer Zeichenfolge nicht das erste Zeichen enthält. Manchmal nennt man einen ganzen Teilstring ein "Suffix", aber wenn man den Algorithmus definiert, werden die Begriffe "String" und "Teilstring" und "Suffix" großzügig herumgeworfen, und manchmal muss man sehr klar sein, was man mit "Suffix" meint Der Begriff "richtiges Suffix" schließt aus, das Ganze als Suffix zu bezeichnen. Ein Suffix-Teilstring eines Strings kann also ein beliebiger legitimer Teilstring sein und ein geeignetes Suffix haben, das nicht dasselbe Suffix ist. Weil Logik.
Blair Houghton
3

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

    require 'pry'


class Edge
    attr_accessor :data , :edges , :suffix_link
    def initialize data
        @data = data
        @edges = []
        @suffix_link = nil
    end

    def find_edge element
        self.edges.each do |edge|
            return edge if edge.data.start_with? element
        end
        return nil
    end
end

class SuffixTrees
    attr_accessor :root , :active_point , :remainder , :pending_prefixes , :last_split_edge , :remainder

    def initialize
        @root = Edge.new nil
        @active_point = { active_node: @root , active_edge: nil , active_length: 0}
        @remainder = 0
        @pending_prefixes = []
        @last_split_edge = nil
        @remainder = 1
    end

    def build string
        string.split("").each_with_index do |element , index|


            add_to_edges @root , element        

            update_pending_prefix element                           
            add_pending_elements_to_tree element
            active_length = @active_point[:active_length]

            # if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data[0..active_length-1] ==  @active_point[:active_edge].data[active_length..@active_point[:active_edge].data.length-1])
            #   @active_point[:active_edge].data = @active_point[:active_edge].data[0..active_length-1]
            #   @active_point[:active_edge].edges << Edge.new(@active_point[:active_edge].data)
            # end

            if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data.length == @active_point[:active_length]  )
                @active_point[:active_node] =  @active_point[:active_edge]
                @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0])
                @active_point[:active_length] = 0
            end
        end
    end

    def add_pending_elements_to_tree element

        to_be_deleted = []
        update_active_length = false
        # binding.pry
        if( @active_point[:active_node].find_edge(element[0]) != nil)
            @active_point[:active_length] = @active_point[:active_length] + 1               
            @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0]) if @active_point[:active_edge] == nil
            @remainder = @remainder + 1
            return
        end



        @pending_prefixes.each_with_index do |pending_prefix , index|

            # binding.pry           

            if @active_point[:active_edge] == nil and @active_point[:active_node].find_edge(element[0]) == nil

                @active_point[:active_node].edges << Edge.new(element)

            else

                @active_point[:active_edge] = node.find_edge(element[0]) if @active_point[:active_edge]  == nil

                data = @active_point[:active_edge].data
                data = data.split("")               

                location = @active_point[:active_length]


                # binding.pry
                if(data[0..location].join == pending_prefix or @active_point[:active_node].find_edge(element) != nil )                  


                else #tree split    
                    split_edge data , index , element
                end

            end
        end 
    end



    def update_pending_prefix element
        if @active_point[:active_edge] == nil
            @pending_prefixes = [element]
            return

        end

        @pending_prefixes = []

        length = @active_point[:active_edge].data.length
        data = @active_point[:active_edge].data
        @remainder.times do |ctr|
                @pending_prefixes << data[-(ctr+1)..data.length-1]
        end

        @pending_prefixes.reverse!

    end

    def split_edge data , index , element
        location = @active_point[:active_length]
        old_edges = []
        internal_node = (@active_point[:active_edge].edges != nil)

        if (internal_node)
            old_edges = @active_point[:active_edge].edges 
            @active_point[:active_edge].edges = []
        end

        @active_point[:active_edge].data = data[0..location-1].join                 
        @active_point[:active_edge].edges << Edge.new(data[location..data.size].join)


        if internal_node
            @active_point[:active_edge].edges << Edge.new(element)
        else
            @active_point[:active_edge].edges << Edge.new(data.last)        
        end

        if internal_node
            @active_point[:active_edge].edges[0].edges = old_edges
        end


        #setup the suffix link
        if @last_split_edge != nil and @last_split_edge.data.end_with?@active_point[:active_edge].data 

            @last_split_edge.suffix_link = @active_point[:active_edge] 
        end

        @last_split_edge = @active_point[:active_edge]

        update_active_point index

    end


    def update_active_point index
        if(@active_point[:active_node] == @root)
            @active_point[:active_length] = @active_point[:active_length] - 1
            @remainder = @remainder - 1
            @active_point[:active_edge] = @active_point[:active_node].find_edge(@pending_prefixes.first[index+1])
        else
            if @active_point[:active_node].suffix_link != nil
                @active_point[:active_node] = @active_point[:active_node].suffix_link               
            else
                @active_point[:active_node] = @root
            end 
            @active_point[:active_edge] = @active_point[:active_node].find_edge(@active_point[:active_edge].data[0])
            @remainder = @remainder - 1     
        end
    end

    def add_to_edges root , element     
        return if root == nil
        root.data = root.data + element if(root.data and root.edges.size == 0)
        root.edges.each do |edge|
            add_to_edges edge , element
        end
    end
end

suffix_tree = SuffixTrees.new
suffix_tree.build("abcabxabcd")
binding.pry
Suchit Puri
quelle