ウッコネンの単純な英語のサフィックスツリーアルゴリズム


1102

この時点で少し分厚い感じがします。私はサフィックスツリーの構築に完全に頭を悩ませるために何日も費やしましたが、私には数学的な背景がないため、数学記号を過度に使用し始めると、多くの説明がわからなくなります。私が見つけた優れた説明に最も近いのは、サフィックスツリーを使用した高速文字列検索ですが、彼はさまざまな点につながっており、アルゴリズムのいくつかの側面は不明のままです。

このStack Overflowでのアルゴリズムのステップバイステップの説明は、私以外の多くの人にとって非常に貴重だと思います。

参考までに、ここにアルゴリズムに関するウッコネンの論文があります。http//www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf

これまでの私の基本的な理解:

  • 特定の文字列Tの各プレフィックスPを反復処理する必要があります
  • 接頭辞Pの各接尾辞Sを反復処理し、それをツリーに追加する必要があります
  • 接尾辞Sをツリーに追加するには、Sの各文字を反復処理する必要があります。反復は、Sの同じ文字セットCで始まる既存のブランチを歩いて行き、エッジを子孫ノードに分割する可能性があります。サフィックス内の別の文字に到達する、または一致するエッジがない場合は下に移動する。Cに対して下に移動する一致するエッジが見つからない場合、Cに対して新しいリーフエッジが作成されます。

ほとんどの説明で指摘されているように、基本的なアルゴリズムはO(n 2)のように見えます。これは、すべてのプレフィックスをステップスルーする必要があるため、各プレフィックスの各サフィックスをステップスルーする必要があるためです。ウッコネンのアルゴリズムは、彼が使用しているサフィックスポインターテクニックのために、明らかにユニークですが、それは私が理解するのに苦労しいるものだと思います。

私も理解できません:

  • 「アクティブポイント」が割り当てられ、使用され、変更される時期と方法
  • アルゴリズムの正規化の側面で何が起こっているか
  • 私が見た実装が使用している境界変数を「修正」する必要がある理由

完成したC#ソースコードを以下に示します。正しく機能するだけでなく、自動正規化をサポートし、出力の見栄えの良いテキストグラフをレンダリングします。ソースコードとサンプル出力は次の場所にあります。

https://gist.github.com/2373868


2017年11月4日更新

長年の後に、私はサフィックスツリーの新しい使用法を見つけ、JavaScriptにアルゴリズムを実装しました。要旨は以下の通りです。バグがないはずです。npm install chalk同じ場所からそれをjsファイルにダンプし、node.jsで実行してカラフルな出力を表示します。同じGistには、デバッグコードのない、簡略化されたバージョンがあります。

https://gist.github.com/axefrog/c347bf0f5e0723cbd09b1aaed6ec6fc6



4
要旨でライセンスが指定されていません-コードを変更してMITで再公開できますか(当然のことですが)。
Yurik

2
うん、あなたの人生のために行きます。それをパブリックドメインと考えてください。このページの別の回答で述べたように、とにかく修正が必要なバグがあります。
Nathan Ridley、

1
多分この実装は他の人を助けるでしょう、code.google.com / p / text
cos

2
「それをパブリックドメインと見なす」は、おそらく驚くべきことに、非常に役に立たない答えです。その理由は、作品をパブリックドメインに置くことは実際には不可能だからです。したがって、「検討してください...」というコメントは、ライセンスが不明確であるという事実を強調し、読者に作品のステータスが実際に明確であるかどうかを疑う理由を与えます。他の人がコードを使用できるようにしたい場合は、コードのライセンスを指定し、好きなライセンスを選択してください(ただし、弁護士でない限り、既存のライセンスを選択してください)
James Youngman

回答:


2379

以下は、最初に文字列が単純である(つまり、繰り返される文字が含まれていない)場合の動作を示し、次に完全なアルゴリズムに拡張することによって、Ukkonenアルゴリズムを説明する試みです。

まず、いくつかの予備的な声明。

  1. 私たちが構築しているものは、基本的には検索トライのようなものです。したがって、ルートノードがあり、そのエッジから出て新しいノードにつながり、さらにそれらから出るエッジが続きます。

  2. ただし、検索トライとは異なり、エッジラベルは単一の文字ではありません。代わりに、各エッジには整数のペアを使用してラベルが付けられます [from,to]。これらはテキストへのポインタです。この意味で、各エッジは任意の長さの文字列ラベルを保持しますが、O(1)スペース(2つのポインター)のみを使用します。

基本的な原則

最初に、特に単純な文字列(繰り返し文字のない文字列)のサフィックスツリーを作成する方法を示します。

abc

アルゴリズムは、左から右へと段階的に機能します文字列の文字ごとに1つのステップがあります。各ステップには複数の個別の操作が含まれる場合がありますが、操作の総数がO(n)であることがわかります(最後の最後の観察を参照)。

したがって、から開始して、最初にaルートノード(左側)からリーフにエッジを作成し、ラベルを付けて1文字だけを挿入し ます[0,#]。で、現在のエンド。Iは、記号を使用#意味する現在の終了位置1(右の後です、a)。

したがって、次のような初期ツリーがあります。

そしてそれが意味することはこれです:

次に、位置2(の直後b)に進みます。各ステップの目標は、現在の位置までのすべてのサフィックス を挿入することです。これを行う

  • 既存のaエッジを拡張するab
  • 新しいエッジを1つ挿入する b

私たちの表現ではこれは次のようになります

ここに画像の説明を入力してください

そしてそれが意味することは:

2つのことを観察します。

  • 以下のためのエッジ表現は、abある同じ、それが最初の木であることが使用されます:[0,#]。現在の位置#を1から2に更新したため、その意味は自動的に変更されました。
  • 各エッジは、それが表す文字数に関係なく、テキストへの2つのポインタのみで構成されるため、O(1)スペースを消費します。

次に、位置を再度インクリメントし、c既存のすべてのエッジにa を追加して新しいサフィックスに新しいエッジを1つ挿入することにより、ツリーを更新しますc

私たちの表現ではこれは次のようになります

そしてそれが意味することは:

私たちは観察します:

  • ツリーは、 各ステップの後の現在の位置までの正しいサフィックスツリーです。
  • テキストの文字数と同じ数のステップがあります
  • 各ステップの作業量はO(1)です。これは、既存のすべてのエッジがを増分#することによって自動的に更新され、最後の文字の1つの新しいエッジの挿入はO(1)時間で実行できるためです。したがって、長さがnの文字列の場合、必要な時間はO(n)のみです。

最初の拡張:単純な繰り返し

もちろん、これは文字列に繰り返しが含まれていないためだけにうまく機能します。次に、より現実的な文字列を見てみましょう。

abcabxabcd

abc前の例と同じように始まり、abが繰り返され、が続き、が繰り返されxabcが続きdます。

ステップ1から3:最初の3つのステップの後に、前の例のツリーがあります。

ステップ4:#位置4に移動します。これにより、既存のすべてのエッジが暗黙的に更新されます。

そして、現在のステップの最後のサフィックスをaルートに挿入する必要があります。

これを行う前に、(に加えて)さらに 2つの変数を導入します。#もちろん、これらは常に存在しますが、これまでのところ使用していません。

  • アクティブポイント三重であり、 (active_node,active_edge,active_length)
  • remainder我々は挿入する必要がありますどのように多くの新しい接尾辞を表す整数です、

これら2つの正確な意味はすぐに明らかになりますが、今のところは言っておきましょう。

  • シンプルではabc例として、アクティブポイントは常にして (root,'\0x',0)、つまりはactive_nodeルートノードだった、active_edgeヌル文字として指定されました'\0x'、そしてactive_lengthゼロでした。この結果、すべてのステップで挿入した1つの新しいエッジが、新しく作成されたエッジとしてルートノードに挿入されました。この情報を表すためにトリプルが必要な理由はすぐにわかります。
  • remainder常に各ステップの開始時に1に設定しました。これの意味は、各ステップの最後に積極的に挿入しなければならないサフィックスの数が1(常に最後の文字のみ)であることでした。

今、これは変わるでしょう。現在の最後の文字aをルートに挿入するとa、特にで始まる出力エッジが既にあることがわかりますabca。このような場合の処理​​を次に示します。

  • 私たちはしていない新鮮なエッジを挿入し[4,#]、ルートノードで。代わりに、サフィックスaが既にツリーにあることに気づきます。それは長いエッジの真ん中で終わりますが、私たちはそれで気になりません。私たちは物事をそのままの状態にしています。
  • アクティブポイントをに設定(root,'a',1)ます。つまり、アクティブポイントは、ルートノードの出力エッジの中央、つまりで始まりa、特にそのエッジの位置1の後にあることを意味します。エッジは最初の文字だけで指定されていることがわかりますa。特定の文字で始まるエッジは1つだけなので、これで十分です(説明全体を読んだ後、これが正しいことを確認してください)。
  • また、をインクリメントしますremainder。そのため、次のステップの開始時には2になります。

観察:挿入する必要がある最後のサフィックスがツリーにすでに存在することが判明した場合、ツリー自体はまったく変更さません(アクティブポイントとのみを更新しますremainder)。その場合、ツリーは現在の位置までのサフィックスツリーの正確な表現ではなくなりますが、すべてのサフィックスが含まれます(最後のサフィックスa暗黙的に含まれているため)。したがって、変数の更新(すべて固定長であるため、これはO(1)です)を除いて、この手順では 何も行われませんでした

ステップ5:現在の位置#を5に更新します。これにより、ツリーが次のように自動的に更新されます。

またremainderis 2 なので、現在の位置の最後の2つのサフィックスを挿入する必要があります:abb。これは基本的に次の理由によります:

  • a前のステップのサフィックスが正しく挿入されたことはありません。したがって、それは残っおり、1つのステップを進めて以来、現在はからaに成長していabます。
  • そして、新しい最終エッジを挿入する必要がありますb

実際には、これはアクティブポイント(a現在はabcabエッジの後ろのポイント)に移動し、現在の最終文字を挿入することを意味しbます。しかし、再び、b同じエッジにすでに存在していることがわかります。

したがって、ここでもツリーを変更しません。私たちは単に:

  • アクティブな点を更新します(root,'a',2)(以前と同じノードとエッジですが、今はの後ろを指していますb
  • remainder前のステップの最終エッジをまだ適切に挿入しておらず、現在の最終エッジも挿入していないため、を3に増やします。

明確にするために、現在のステップでabとを挿入する必要がありbましたが、ab既に見つかったため、アクティブポイントを更新し、を挿入することさえしませんでしたb。どうして?abがツリー内にある場合、その すべてのサフィックス(を含むb)もツリー内にある必要があるためです。おそらく暗黙的にだけかもしれませんが、これまでにツリーを構築した方法のため、そこにある必要があります。

ステップ6に進みます#。ツリーは自動的に次のように更新されます。

のでremainder3である、我々は挿入する必要がabxbxx。アクティブポイントはどこでab終了するかを示しているので、そこにジャンプしてを挿入するだけで済みxます。実際、xまだそこにはないので、abcabxエッジを分割して内部ノードを挿入します。

エッジ表現は依然としてテキストへのポインタであるため、内部ノードの分割と挿入はO(1)時間で実行できます。

それで、2に対処しabxremainder2にデクリメントしました。次に、残りの次のサフィックスを挿入する必要がありbxます。ただし、その前に、アクティブポイントを更新する必要があります。エッジを分割して挿入した後のこのルールは、以下のルール1と呼ばれ、active_nodeがルートである場合は常に適用されます (他のケースについては、以下でルール3を学習します)。これがルール1です。

ルートから挿入した後、

  • active_node ルートのまま
  • active_edge 挿入する必要がある新しいサフィックスの最初の文字、つまり b
  • active_length 1減る

したがって、新しいアクティブポイントトリプル(root,'b',1)は、次の挿入がbcabx1文字の後ろ、つまり後ろで行われる必要があることを示しますb。O(1)時間で挿入ポイントを識別し、xすでに存在するかどうかを確認できます。存在する場合は、現在のステップを終了し、すべてをそのままにします。しかしx 、存在しないため、エッジを分割して挿入します。

繰り返しますが、これにはO(1)の時間がかかり、1に更新remainder(root,'x',0)、ルール1の状態としてアクティブポイントを更新します。

しかし、やらなければならないことがもう1つあります。これをルール2と呼びます

エッジを分割して新しいノードを挿入し、それが現在のステップで作成された最初のノードでない場合は、以前に挿入されたノードと新しいノードを特別なポインターである接尾辞リンクを介して接続します。なぜそれが役立つのかは後で説明します。これが私たちが得るものです、サフィックスリンクは点線のエッジとして表されます:

現在のステップの最後のサフィックスを挿入する必要があり xます。以来、active_lengthアクティブノードの成分が0まで低下した、最終的なインサートは直接ルートで行われます。で始まるルートノードには発信エッジがないためx、新しいエッジを挿入します。

ご覧のとおり、現在のステップでは、残りのすべての挿入が行われました。

= 7を設定してステップ7に進みます。これにより、常に、すべてのリーフエッジに#次の文字が自動的に追加さ aれます。次に、新しい最後の文字をアクティブポイント(ルート)に挿入しようとしましたが、すでにそこにあることがわかります。したがって、何も挿入せずに現在のステップを終了し、アクティブポイントをに更新し(root,'a',1)ます。

ではステップ8#= 8、我々は追加しbて、前に見たように、この唯一の手段は、我々は積極的にポイントの更新(root,'a',2)と増分remainderので、他に何もせずにb、既に存在しています。ただし、(O(1)時間で)アクティブポイントがエッジの終わりにあることがわかります。これをにリセットして反映し (node1,'\0x',0)ます。ここではnode1abエッジが終了する内部ノードを参照するために使用します。

次に、ステップ#= 9で、「c」を挿入する必要があります。これは、最終的なトリックを理解するのに役立ちます。

2番目の拡張:サフィックスリンクの使用

いつものように、#更新はcリーフエッジに自動的に追加され、アクティブポイントに移動して「c」を挿入できるかどうかを確認します。'c'がそのエッジに既に存在することが判明したため、アクティブポイントをに設定し (node1,'c',1)、インクリメントしてremainder、他には何もしません。

今では、ステップ#= 10remainder4であり、インサートに我々最初必要があるので、 abcd挿入することにより(3つのステップ前から残っている)dアクティブポイントで。

dアクティブなポイントに挿入しようとすると、O(1)時間でエッジが分割されます。

active_node、分割が開始された、赤、上記に記されています。これが最後のルール、ルール3です。

active_nodeルートノードではないからエッジを分割した後、そのノードから出るサフィックスリンクがあればそれをたどり、が指すノードにをリセットactive_nodeします。サフィックスリンクがない場合はactive_node、ルートに設定します。active_edge そして、active_length変わりません。

したがって、アクティブポイントはになり(node2,'c',1)node2下に赤でマークされます。

の挿入abcdが完了したので、remainder3 にデクリメントし、現在のステップの次の残りのサフィックスを検討し bcdます。ルール3では、アクティブポイントを正しいノードとエッジに設定しているので、アクティブポイントにbcd最後の文字dを挿入するだけで挿入できます 。

これを行うと、別のエッジ分割が発生します。ルール2のため、以前に挿入されたノードから新しいノードへのサフィックスリンクを作成する必要があります。

観察:サフィックスリンクを使用すると、アクティブポイントをリセットできるため、O(1)で次の残りの挿入を行うことができます。実際にノードラベルで確認するために、上のグラフを見てabのノードにリンクされているb(その接尾辞)、およびのノードabcにリンクされています bc

現在のステップはまだ完了していません。remainderこれで2になり、アクティブポイントを再度リセットするには、ルール3に従う必要があります。現在active_node(上記の赤)にはサフィックスリンクがないため、ルートにリセットします。現在アクティブなポイントは(root,'c',1)です。

したがって、次の挿入は、最初の文字の後ろ、つまりの後ろで、ラベルがccabxabcdで始まるルートノードの1つの発信エッジで行われcます。これにより、別の分割が発生します。

これには新しい内部ノードの作成が含まれるため、ルール2に従い、以前に作成された内部ノードから新しいサフィックスリンクを設定します。

(私はこれらの小さなグラフにGraphviz Dotを使用しています。新しいサフィックスリンクはドットが既存のエッジを再配置する原因となったので、上に挿入された唯一のものが新しいサフィックスリンクであることを確認するために注意深くチェックしてください。)

これにより、remainderを1に設定できます。これactive_nodeはルートであるため、ルール1を使用してアクティブポイントをに更新し(root,'d',0)ます。つまり、現在のステップの最後の挿入は、d ルートにシングルを挿入することです。

これが最後のステップで、これで完了です。ただし、いくつかの最終的な観察があります。

  • 各ステップで#1ポジション進みます。これにより、すべてのリーフノードがO(1)時間で自動的に更新されます。

  • ただし、a)前のステップから残っているサフィックス、およびb)現在のステップの最後の1文字を処理しません。

  • remainder追加の挿入をいくつ行う必要があるかを示します。これらの挿入は、現在の位置で終了する文字列の最後のサフィックスに1対1で対応し#ます。次々と考えてインサートを作ります。重要:各挿入はO(1)時間で行われます。これは、アクティブポイントがどこに行くべきかを正確に指示し、アクティブポイントに1文字だけ追加する必要があるためです。どうして?他の文字は暗黙的に含まれているためです (そうでない場合、アクティブポイントはその場所にありません)。

  • このような挿入が行われるたびremainderに、サフィックスリンクがある場合はそれを減らして移動します。そうでない場合は、ルートに移動します(ルール3)。すでにルートにいる場合は、ルール1を使用してアクティブポイントを変更します。いずれの場合も、O(1)時間しかかかりません。

  • これらの挿入の1つで、挿入したい文字が既にそこにあることがわかった場合、remainder> 0 であっても、何もせずに現在のステップを終了します。その理由は、残っている挿入は、先ほど作成した挿入のサフィックスになるためです。したがって、それらはすべて現在のツリーで暗黙的です。remainder> 0であるという事実は、残りのサフィックスを後で処理することを確実にします。

  • アルゴリズムの最後でremainder> 0の場合はどうなりますか?これは、テキストの最後がどこか以前に発生した部分文字列である場合に当てはまります。その場合、これまでにない文字列の最後に1文字追加する必要があります。文献では、通常、ドル記号$がその記号として使用されています。なぜそれが重要なのですか?->完成したサフィックスツリーを後で使用してサフィックスを検索する場合、リーフで終了する場合にのみ一致を受け入れる必要があります。そうしないと、ツリーに暗黙的に含まれる多くの文字列がメイン文字列の実際のサフィックスではないため、多くの疑似一致が発生します。強制remainder末尾が0になることは、基本的にすべてのサフィックスがリーフノードで終了することを保証する方法です。ただし、ツリーを使用してメイン文字列のサフィックスだけでなく一般的な部分文字列を検索する場合は下のOPのコメントで示唆されているように、この最後の手順は実際には必要ありません。

  • では、アルゴリズム全体の複雑さはどのくらいですか?テキストの長さがn文字の場合、明らかにnステップ(またはドル記号を追加した場合はn + 1)になります。各ステップで(変数の更新以外に)何もしないか、またはremainderO(1)時間をかけて挿入を行います。以来remainder、私たちは前の手順で何もしなかった、と私たちは今、作ること、すべての挿入のために減少される回数を示し、私たちが何かをする回数の合計は、ちょうどn(またはN + 1)です。したがって、全体の複雑度はO(n)です。

  • ただし、適切に説明しなかった小さなことが1つあります。サフィックスリンクをたどり、アクティブポイントを更新した後、そのactive_lengthコンポーネントが新しいでうまく機能しない場合がありactive_nodeます。たとえば、次のような状況を考えます。

(破線はツリーの残りの部分を示します。点線はサフィックスリンクです。)

次に、アクティブポイントをと(red,'d',3)するfと、defgエッジの後ろの場所を指します。次に、必要な更新を行ったと想定し、サフィックスリンクをたどって、ルール3に従ってアクティブポイントを更新します。新しいアクティブポイントは(green,'d',3)です。ただし、d緑色のノードから出ている-edgeはdeなので、2文字しかありません。正しいアクティブポイントを見つけるには、そのエッジを青いノードまでたどり、にリセットする必要があります(blue,'f',1)

特に悪いケースでは、active_lengthはと同じくらい大きく remainderなる可能性があり、nと同じくらい大きくなる可能性があります。そして、正しいアクティブポイントを見つけるために、1つの内部ノードをジャンプするだけでなく、最悪の場合は最大nまでジャンプする必要があることもよくあります。各ステップでは一般にO(n)であり、サフィックスリンクをたどった後のアクティブノードへの後調整もO(n)になる可能性があるため、アルゴリズムには隠されたO(n 2)の複雑さがあるということremainderですか?

いいえ。理由は、実際にアクティブポイントを調整する必要がある場合(たとえば、上記のように緑から青に)、独自のサフィックスリンクを持つ新しいノードに移動し、active_length削減されるためです。サフィックスリンクのチェーンをたどると、残りの挿入が行われ、active_length減少するだけであり、途中で行うことができるアクティブポイント調整の数はactive_length、任意の時点より大きくなることはできません。ためには、 active_lengthより大きくなることはありませんremainder、とremainder だけでなく、すべての単一工程でO(n)があるが、これまでに行われた増分の総和remainderプロセス全体にわたってはO(n)があまりにも、活性点調整の数でありますまた、O(n)によって制限されます。


74
申し訳ありませんが、これは思ったより少し長くなりました。そして、それは私たち全員が知っている多くの些細なことを説明していますが、難しい部分はまだ完全に明確ではないかもしれません。一緒に形にしましょう。
jogojapan 2012年

69
これは、ダンガスフィールドの本にある説明に基づいていないことを付け加えておきます。これは、最初に繰り返しのない文字列を検討し、次に繰り返しがどのように処理されるかを説明することで、アルゴリズムを説明する新しい試みです。それがもっと直感的になることを望みました。
jogojapan 2012年

8
@jogojapanのおかげで、あなたの説明のおかげで完全に機能する例を書くことができました。私はソースを公開したので、うまくいけば誰か他の人がそれを利用できるようになるかもしれません:gist.github.com/2373868
Nathan Ridley

4
@NathanRidleyはい(ちなみに、最後の部分はウッコネンが正規化と呼んでいるものです)。これをトリガーする1つの方法は、3回出現し、さらに別のコンテキストでもう一度出現する文字列で終了する部分文字列があることを確認することです。例えばabcdefabxybcdmnabcdex。の最初の部分はabcdで繰り返されabxy(これによりの後に内部ノードが作成されますab)、再びで繰り返されabcdex、で終了しますbcd。これは、bcdexコンテキストだけでなくコンテキストにも表示されbcdmnます。後にabcdex挿入され、我々は挿入する接尾辞リンクをたどるbcdex、とcanonicizeはそこでキックします。
jogojapan

6
私のコードは完全に書き直され、自動正規化を含むすべてのケースで正しく動作するようになりました。さらに、より優れたテキストグラフ出力が得られます。gist.github.com/2373868
ネイサンリドリー

132

jogojapanの回答で示されたアプローチでサフィックスツリーを実装しようとしましたが、ルールに使用されている表現のために、一部のケースでは機能しませんでした。さらに、このアプローチを使用して絶対に正しいサフィックスツリーを実装できた人は誰もいないと述べました。以下に、jogojapanの回答の「概要」を書き、ルールをいくつか変更します。重要なサフィックスリンクの作成を忘れた場合についても説明します。

使用される追加の変数

  1. アクティブポイント -トリプル(active_node; active_edge; active_length)。新しいサフィックスの挿入を開始する必要がある場所を示します。
  2. 残り - 明示的に追加する必要があるサフィックスの数を示します。たとえば、単語が「abcaabca」で、残りが3の場合、最後の3つのサフィックスbcacaおよびaを処理する必要があることを意味ます。

内部ノードの概念を使用してみましょう- ルートリーフを除くすべてのノードは内部ノードです。

観察1

挿入する必要がある最後のサフィックスがすでにツリーに存在することが判明した場合、ツリー自体はまったく変更されません(active pointおよびのみを更新しますremainder)。

観察2

ある時点active_lengthで現在のエッジの長さ(edge_length)に等しいかそれ以上の場合は、が完全にになるactive pointまで下に移動します。edge_lengthactive_length

次に、ルールを再定義します。

ルール1

アクティブノードからの挿入後= rootの場合、アクティブ長は0より大きい場合、次のようになります。

  1. アクティブノードは変更されません
  2. アクティブな長さが減少します
  3. アクティブエッジが右にシフトされます(挿入する必要がある次のサフィックスの最初の文字に)

ルール2

私たちは新しい作成する場合は、内部ノードを ORからインサータを作る内部ノードと、これが初めてではないそのような 内部ノード現在の段階で、我々は前のリンクSUCHとノードTHIS介して1を接尾辞リンク

この定義はRule 2jogojapanとは異なります。ここでは、新しく作成された内部ノードだけでなく、挿入を行う内部ノードも考慮に入れているためです。

ルール3

ルートノードではないアクティブノードから挿入した後、サフィックスリンクをたどり、アクティブノードをそれが指すノードに設定する必要があります。サフィックスリンクがない場合は、アクティブノードルートノードに設定します。どちらの方法でも、アクティブなエッジアクティブな長さは変わりません。

この定義でRule 3は、(分割ノードだけでなく)葉ノードの挿入も考慮します。

そして最後に、観察3:

ツリーに追加するシンボルがすでに端にある場合、に従ってObservation 1active pointおよびのみを更新しremainder、ツリーは変更しないままにします。しかしサフィックスリンクが必要とマークされた内部ノードがある場合、そのノードをサフィックスリンクを介して現在のノードに接続する必要があります。active node

このような場合にサフィックスリンクを追加し、追加しない場合のcdddcdcのサフィックスツリーの例を見てみましょう。

  1. サフィックスリンクを介してノードを接続しない場合:

    • 最後の文字cを追加する前に:

    • 最後の文字cを追加した後:

  2. 私たちは、場合DO接尾辞リンクを介してノードを接続します。

    • 最後の文字cを追加する前に:

    • 最後の文字cを追加した後:

大きな違いはないようです。2番目のケースでは、さらに2つのサフィックスリンクがあります。しかし、これらのサフィックスリンクは正しく、そのうちの1つ(青色のノードから赤色のノードまで)は、アクティブポイントを使用するアプローチにとって非常に重要です。問題は、ここにサフィックスリンクを配置しないと、後でツリーに新しい文字を追加するときに、に起因するツリーへのノードの追加が省略される可能性があるということです。接尾辞リンク、次にルートに置く必要があります。Rule 3active_node

ツリーに最後の文字を追加したとき、青色のノードから挿入を行う前に、赤色のノードが既に存在しいました(エッジに「c」が付けられています)。青いノードからの挿入があったため、サフィックスリンクが必要であることを示します。次に、アクティブポイントアプローチに依存して、active nodeは赤いノードに設定されました。ただし、文字「c」はすでにエッジにあるため、赤いノードからの挿入は行いません。それは青いノードがサフィックスリンクなしで残されなければならないことを意味しますか?いいえ、サフィックスリンクを介して青いノードを赤いノードに接続する必要があります。なぜそれが正しいのですか?そのため、アクティブポイントアプローチにより、適切な場所、つまり短いサフィックスの挿入を処理する必要がある次の場所に到達することが保証されます。

最後に、これがサフィックスツリーの私の実装です。

  1. ジャワ
  2. C ++

この「概要」とjogojapanの詳細な回答を組み合わせると、誰かが自分のサフィックスツリーを実装するのに役立つことを願っています。


3
本当にありがとう、あなたの努力に+1を。きっとあなたは正しいと思います。詳細をすぐに考える時間はありませんが。後で確認し、可能であれば私の回答も変更します。
jogojapan 2013年

本当にありがとうございました。しかし、観察3について具体的に説明していただけますか?たとえば、新しいサフィックスリンクを導入する2つのステップの図を示します。ノードはアクティブノードにリンクされていますか?(実際には2番目のノードを挿入しないので)
dyesdyes 14年

@makagonov文字列 "cdddcdc"のサフィックスツリーを作成するのを手伝ってくれませんか(これは最初の手順で)混乱しています。
tariq zafar 2014年

3
ルール3に関しては、ルートのサフィックスリンクをルート自体に設定し、(デフォルトでは)すべてのノードのサフィックスリンクをルートに設定するのが賢明な方法です。したがって、条件付けを回避し、サフィックスリンクをたどることができます。
sqd

1
aabaacaadサフィックスリンクを追加すると、トリプルの更新時間を短縮できることを示すケースの1つです。jogojapanのポストの最後の2つの段落の結論は間違っています。この投稿で言及しているサフィックスリンクを追加しない場合、平均時間の複雑さはO(nlong(n))以上になります。正しいものを見つけるために木を歩くのに余分な時間がかかるからactive_nodeです。
IvanaGyro

10

@jogojapanによってよく説明されたチュートリアルをありがとう、私はアルゴリズムをPythonで実装しました。

@jogojapanによって言及されたいくつかのマイナーな問題は、私が予想しよりも洗練されていることが判明し、非常に注意深く処理する必要があります。私の実装を十分に堅牢にするために数日かかりました(おそらく)。問題と解決策を以下に示します。

  1. 終わるRemainder > 0それはこのような状況でも発生することが判明した展開ステップの間に、アルゴリズム全体のだけではなく、終わり。これが発生した場合、残り、actnode、actedge、actlengthを変更せずに、現在の展開ステップを終了し、元の文字列の次の文字が現在のパス上にあるか、またはない。

  2. ノードの跳躍:サフィックスリンクをたどると、アクティブポイントが更新され、そのactive_lengthコンポーネントが新しいactive_nodeでうまく機能しないことがわかります。我々はする必要が前進スプリットに適切な場所に、または葉を挿入します。このプロセスは、かもしれないという単純明快 actlengthと、すべての方法を変更するactedgeキープ、あなたが戻って移動する必要があるとき、移動中にあるため、ルートノードactedgeactlengthは可能性が間違っているので、それらの移動のを。その情報を保持するには、追加の変数が必要です。

    ここに画像の説明を入力してください

他の2つの問題は、@ managonovによって何らかの形で指摘されています

  1. スプリットは縮退する可能性があるエッジをスプリットしようとすると、ノード上でスプリット操作が正しく行われることがあります。その場合、そのノードに新しいリーフを追加するだけでよく、それを標準のエッジ分割操作と見なします。つまり、サフィックスリンクがある場合は、それに応じて維持する必要があります。

  2. 非表示のサフィックスリンク問題1問題2によって発生する別の特別なケースがあります。時々、分割のためにいくつかのノードを飛び越えて正しいポイントに移動する必要があります。残りの文字列とパスラベルを比較して移動すると、正しいポイントを超える場合があります。その場合、もしあれば、意図せずにサフィックスリンクが無視されます。これは前進するときに正しいポイントを覚えておくことで回避できます。分割ノードが既に存在する場合、または問題1が展開ステップ中に発生する場合は、サフィックスリンクを維持する必要があります。

最後に、Pythonでの私の実装は次のとおりです。

ヒント: 上記のコードには、デバッグ中に非常に重要な単純なツリー印刷機能が含まれています。時間を大幅に節約でき、特殊なケースを探すのに便利です。


10

私の答えが冗長に思われる場合はお詫びしますが、私は最近ウコネンのアルゴリズムを実装し、何日もそれと苦労していました。アルゴリズムのいくつかの中核的な側面の理由と方法を理解するために、このテーマに関する複数の論文を読む必要がありました。

以前の回答の「ルール」アプローチは根本的な理由を理解するのに役立たないと感じたので、以下のすべてを実用主義のみに焦点を当てて書きました。私と同じように、以下の説明に苦労している場合は、私の補足説明で「クリック」できるでしょう。

ここにC#実装を公開しました:https : //github.com/baratgabor/SuffixTree

私はこの問題の専門家ではないため、以下のセクションには不正確な(またはさらに悪い)内容が含まれている可能性があることに注意してください。何か発生した場合は、自由に編集してください。

前提条件

以下の説明の開始点は、サフィックスツリーの内容と使用法、およびUkkonenのアルゴリズムの特性(例:サフィックスツリーを文字ごとに最初から最後まで拡張する方法)に精通していることを前提としています。基本的には、すでに他の説明を読んでいると思います。

(ただし、フローの基本的なナラティブを追加する必要があったので、最初は確かに冗長に感じるかもしれません。)

最も興味深い部分は、サフィックスリンクの使用とルートからの再スキャンの違い説明です。これが私の実装に多くのバグと頭痛の種を与えたものです。

オープンエンドリーフノードとその制限

最も基本的な「トリック」は、接尾辞の末尾を「オープン」のままにできること、つまり末尾を静的な値に設定するのではなく、文字列の現在の長さを参照できることを理解することです。この方法で追加の文字を追加すると、それらの文字はすべてのサフィックスラベルに暗黙的に追加され、すべてにアクセスして更新する必要はありません。

ただし、明らかな理由により、このサフィックスのオープンエンディングは、文字列の終わりを表すノード、つまりツリー構造のリーフノードに対してのみ機能します。ツリーで実行するブランチ操作(新しいブランチノードとリーフノードの追加)は、必要なすべての場所に自動的に反映されるわけではありません。

繰り返しの部分文字列は繰り返しであるため、ツリーにはすでに含まれているため、繰り返された部分文字列は明示的にツリーに表示されないことは、おそらく基本的であり、言及する必要はありません。ただし、反復しない文字に遭遇して反復するサブストリングが終了する場合は、そのポイントから分岐を作成して、そのポイント以降の分岐を表す必要があります。

たとえば、文字列'ABCXABCY'(以下を参照)の場合、XYへの分岐を3つの異なるサフィックスABCBCCに追加する必要があります。そうしないと、有効なサフィックスツリーにならず、ルートから下に向かって文字を照合して、文字列のすべての部分文字列を見つけることができませんでした。

もう一度、強調するために- どの我々はそうでない場合、彼らは単に有効な接尾辞であることをやめ、(例えばABC> BC> C)だけでなく、その連続した接尾辞によって反射されるように、ツリーのニーズにサフィックス上で実行操作を。

サフィックスでの分岐の繰り返し

しかし、これらの手動更新を行う必要があることを受け入れたとしても、更新する必要があるサフィックスの数をどのようにして知ることができますか?繰り返し文字A(および残りの繰り返し文字を続けて)を追加するとき、いつどこでサフィックスを2つのブランチに分割する必要があるのか​​、まだわかりません。分割の必要性は、最初の繰り返しのない文字、この場合はY(ツリーに既に存在するXではなく)に遭遇したときにのみ確認されます。

私たちにできることは、できる限り長く繰り返される文字列に一致させ、後で更新する必要があるその接尾辞の数を数えることです。これが「残り」の意味です。

「残り」と「再スキャン」のコンセプト

変数remainderは、分岐せずに暗黙的に追加した繰り返し文字の数を示します。つまり、一致しない最初の文字を見つけた後、分岐操作を繰り返すためにアクセスする必要がある接尾辞の数。これは基本的に、ツリーのルートから「深い」文字数に相当します。

したがって、文字列ABCXABCYの前の例をそのまま使用して、繰り返されるABC部分を「暗黙的に」照合し、remainder毎回インクリメントします。その結果、残りは3になります。次に、繰り返されない文字「Y」に遭遇します。ここで、以前に追加したABCXABC- > XABC- > Yに分割します。次にremainderABC分岐をすでに処理したため、3から2にデクリメントします。次に、ルートから最後の2文字(BC)を突き合わせて操作を繰り返し、分割する必要があるポイントに到達します。BCXBCに分割します。- > XBC - > Y。この場合もremainder、1にデクリメントして操作を繰り返します。まで、remainder最後には0である、我々は(現在の文字を追加する必要がありYをルートに、同様に自分自身を)。

ルートからの連続したサフィックスに続くこの操作は、操作を実行する必要があるポイントに到達するために、Ukkonenのアルゴリズムで「再スキャン」と呼ばれるものであり、通常、これはアルゴリズムの最も高価な部分です。何十回ものノード(これについては後で説明します)にわたって、長いサブストリングを「再スキャン」する必要がある長いストリングを想像してみてください。

解決策として、「接尾辞リンク」と呼ばれるものを紹介します

「接尾辞リンク」の概念

接尾辞リンクは基本的に、通常「再スキャン」する必要がある位置を指しているため、費用のかかる再スキャン操作の代わりに、リンクされた位置にジャンプして作業を行い、次のリンクされた位置にジャンプして繰り返す–そこまで更新するポジションはもうありません。

もちろん、1つの大きな問題は、これらのリンクを追加する方法です。既存の答えは、新しいブランチノードを挿入するときにリンクを追加できることです。これは、ツリーの各拡張で、ブランチノードを、リンクする必要がある正確な順序で自然に次々に作成されるという事実を利用しています。 。ただし、最後に作成したブランチノード(最長のサフィックス)から前に作成したブランチノードにリンクする必要があるため、最後に作成したブランチノードをキャッシュし、次に作成するブランチノードにリンクして、新しく作成したブランチノードをキャッシュする必要があります。

結果の1つは、指定されたブランチノードが作成されたばかりであるため、実際には多くの場合、サフィックスリンクをたどらないことです。これらの場合でも、ルートから前述の「再スキャン」にフォールバックする必要があります。これが、挿入後、サフィックスリンクを使用するか、ルートにジャンプするように指示される理由です。

(または、親ポインターをノードに保存している場合は、親を追跡し、リンクがあるかどうかを確認して、それを使用することができます。これがほとんど言及されていないことがわかりましたが、サフィックスリンクの使用法はありません石に設定します。考えられるアプローチは複数あります基盤となるメカニズムを理解していれば、ニーズに最も適したメカニズムを実装できます。)

「アクティブポイント」のコンセプト

これまでに、ツリーを構築するための複数の効率的なツールについて説明し、複数のエッジとノードのトラバースについて漠然と言及しましたが、対応する結果と複雑さについてはまだ調べていません。

前に説明した「剰余」の概念は、ツリーのどこにいるかを追跡するのに役立ちますが、十分な情報が格納されていないことに気づく必要があります。

まず、常にノードの特定のエッジに常駐するため、エッジ情報を保存する必要があります。これを「アクティブエッジ」と呼びます。

次に、エッジ情報を追加した後でも、ツリーのさらに下の位置を特定する方法がなく、ルートノードに直接接続されていません。したがって、ノードも保存する必要があります。これを「アクティブノード」と呼びます

最後に、「残り」はルート全体の長さであるため、「残り」はルートに直接接続されていないエッジ上の位置を特定するには不十分であることがわかります。また、前のエッジの長さを覚えたり差し引いたりする必要はないでしょう。したがって、本質的に現在のエッジの残りである表現が必要です。これを「アクティブ長」と呼びます。

これは、「アクティブポイント」と呼ばれるものにつながります。これは、ツリー内の位置に関して維持する必要があるすべての情報を含む3つの変数のパッケージです。

Active Point = (Active Node, Active Edge, Active Length)

ABCABDの一致したルートが、エッジABルートから)の2文字とエッジCABDABCABD(ノード4から)の4文字で構成され、結果として6文字の「残り」になる様子を次の画像で確認できます。したがって、現在の位置はアクティブノード4、アクティブエッジC、アクティブ長さ4として識別できます。

残りとアクティブポイント

「アクティブポイント」のもう1つの重要な役割は、アルゴリズムの抽象化レイヤーを提供することです。つまり、アクティブポイントがルートにあるか他の場所にあるかに関係なく、アルゴリズムの一部が「アクティブポイント」で作業を行うことができます。 。これにより、アルゴリズムにサフィックスリンクの使用を簡単でわかりやすい方法で実装できます。

再スキャンとサフィックスリンクの使用の違い

さて、トリッキーな部分は、私の経験では、多くのバグや頭痛を引き起こす可能性があり、ほとんどのソースで十分に説明されていないことですが、サフィックスリンクケースと再スキャンケースの処理の違いです。

文字列'AAAABAAAABAAC'の次の例を考えます。

複数のエッジの残り

上記の7の「残り」がルートからの文字の合計にどのように対応するかを観察できますが、4の「アクティブな長さ」は、アクティブなノードのアクティブなエッジからの一致した文字の合計に対応します。

これで、アクティブポイントで分岐操作を実行した後、アクティブノードにサフィックスリンクが含まれる場合と含まれない場合があります。

サフィックスリンクが存在する場合:「アクティブな長さ」の部分のみを処理する必要があります。「残りは」ので、無関係である我々はサフィックスリンクを介してに移動ノードが既に暗黙的に正しい「余り」をコード単にそれが木であることのおかげで、。

サフィックスリンクが存在しない場合:ゼロ/ルートから「再スキャン」する必要があります。これは、サフィックス全体を最初から処理することを意味します。このためには、「残り」全体を再スキャンの基礎として使用する必要があります。

サフィックスリンクがある場合とない場合の処理​​の比較例

上記の例の次のステップで何が起こるかを考えます。同じ結果を達成する方法を比較してみましょう。つまり、サフィックスリンクがある場合とない場合で、次のサフィックスに移動して処理します。

「接尾辞リンク」の使用

接尾辞リンクを介して連続する接尾辞に到達する

サフィックスリンクを使用すると、自動的に「適切な場所」にいることに注意してください。これは、「アクティブな長さ」が新しい位置と「互換性がない」可能性があるという事実のために、厳密には正しくないことがよくあります。

上記の場合、「アクティブな長さ」は4 なので、リンクされたノード4からサフィックス「ABAA」を処理しています。ただし、サフィックスの最初の文字(「A」)に対応するエッジを見つけた後)、「アクティブな長さ」がこのエッジから3文字オーバーフローていることがわかります。したがって、エッジ全体を次のノードにジャンプし、ジャンプで使用した文字数だけ「アクティブな長さ」をデクリメントします。

次に、減少したサフィックス「BAA に対応する次のエッジ「B」を見つけた後、エッジの長さが残りの「アクティブな長さ」 3 より大きいことを最後に確認します。これは、適切な場所を見つけたことを意味します。

この操作は通常「再スキャン」とは呼ばれていないようですが、私にとっては、再スキャンの直接的な同等物であるように見えますが、長さが短く、ルートが開始点ではありません。

「再スキャン」の使用

再スキャンによる連続したサフィックスへの到達

従来の「再スキャン」操作を使用する場合(ここではサフィックスリンクがないように見せかけています)、ツリーの最上部のルートから開始し、適切な場所に戻る必要があることに注意してください。現在のサフィックスの全長に沿って続きます。

このサフィックスの長さは、前に説明した「残り」です。残りがゼロになるまで、残り全体を消費する必要があります。これには、複数のノードのジャンプが含まれる場合があり(多くの場合そうです)、ジャンプごとに、ジャンプしたエッジの長さだけ残りが減少します。そして最後に、残りの「残り」よりも長いエッジに到達します。ここで、アクティブエッジを指定されたエッジに設定し、「アクティブ長」を残りの「残り」に設定して、完了です。

ただし、実際の「残り」の変数は保持する必要があり、各ノードの挿入後にのみ減分されることに注意してください。したがって、上記で説明したものは、'remainder'に初期化された個別の変数を使用することを想定しています。

サフィックスリンクと再スキャンに関する注意

1)どちらの方法でも同じ結果になることに注意してください。ただし、ほとんどの場合、サフィックスリンクジャンプは大幅に高速です。これが、サフィックスリンクの根拠です。

2)実際のアルゴリズムの実装は異なる必要はありません。上で述べたように、サフィックスリンクを使用する場合でも、ツリーのそのブランチに追加のブランチが含まれている可能性があるため、「アクティブな長さ」はリンクされた位置と互換性がないことがよくあります。したがって、基本的には、「残りではなく「アクティブな長さ」を使用し、残りのサフィックスの長さよりも短いエッジが見つかるまで同じ再スキャンロジックを実行する必要があります。

3)パフォーマンスに関する重要な注意点の1つは、再スキャン中にすべての文字をチェックする必要がないことです。有効なサフィックスツリーの構築方法により、文字が一致すると想定できます。したがって、ほとんどの長さを数えます。エッジは最初の文字(特定のノードのコンテキストでは常に一意です)で識別されるため、新しいエッジにジャンプするときに文字等価チェックの必要性が発生します。これは、「再スキャン」ロジックが完全な文字列照合ロジック(つまり、ツリー内のサブストリングの検索)とは異なることを意味します。

4)ここで説明されている元のサフィックスリンクは、可能なアプローチの1つにすぎません。たとえば、NJ Larssonら。このアプローチにNode-Oriented Top-Downと名前を付け、Node-Oriented Bottom-Upおよび2つのEdge-Orientedと比較します。さまざまなアプローチには、さまざまな典型的および最悪のケースのパフォーマンス、要件、制限などがありますが、一般的に、エッジ指向アプローチは元のアプローチに対する全体的な改善であると思われます。


8

@jogojapanあなたは素晴らしい説明と視覚化をもたらしました。しかし、@ makagonovが言及したように、サフィックスリンクの設定に関するいくつかのルールがありません。http://brenden.github.io/ukkonen-animation/を「aabaaabb」という単語で一歩一歩進んでいくと、見栄えがよくなります。手順10から手順11に進むと、ノード5からノード2へのサフィックスリンクはありませんが、アクティブポイントが突然そこに移動します。

@makagonov私はJavaの世界に住んでいるので、STビルドのワークフローを把握するために実装を追跡することも試みましたが、次の理由で私にとって困難でした。

  • エッジとノードを組み合わせる
  • 参照の代わりにインデックスポインターを使用する
  • ステートメントを中断します。
  • 継続ステートメント;

そのため、Javaでのこのような実装になりました。これにより、すべてのステップがより明確に反映され、他のJavaの人々の学習時間が短縮されます。

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);
        }
    });
  }
}

6

私の直感は次のとおりです。

メインループをk回繰り返した後、最初のk文字で始まる完全な文字列のすべてのサフィックスを含むサフィックスツリーを作成しました。

最初に、これはサフィックスツリーに文字列全体を表す単一のルートノードが含まれることを意味します(これは0で始まる唯一のサフィックスです)。

len(string)の反復後、すべてのサフィックスを含むサフィックスツリーが作成されます。

ループの間、キーはアクティブポイントです。私の推測では、これは、文字列の最初のk文字の適切なサフィックスに対応するサフィックスツリーの最も深いポイントを表していると思います。(適切とは、サフィックスを文字列全体にすることはできないことを意味します。)

たとえば、「abcabc」という文字を見たとします。アクティブポイントは、サフィックス「abc」に対応するツリー内のポイントを表します。

アクティブポイントは(origin、first、last)で表されます。これは、現在、ノードの起点から開始し、次にstring [first:last]の文字をフィードすることによって到達するツリーのポイントにいることを意味します

新しいキャラクターを追加すると、アクティブなポイントがまだ既存のツリーにあるかどうかがわかります。もしそうなら、あなたは完了です。そうでない場合は、アクティブノードのサフィックスツリーに新しいノードを追加し、次に短い一致にフォールバックして、再度確認する必要があります。

注1:サフィックスポインターは、各ノードの次に短い一致へのリンクを提供します。

注2:新しいノードとフォールバックを追加すると、新しいノードの新しいサフィックスポインターが追加されます。このサフィックスポインタの宛先は、短縮されたアクティブポイントのノードになります。このノードは既に存在するか、このフォールバックループの次の反復で作成されます。

注3:正規化部分は、アクティブポイントをチェックする時間を節約するだけです。たとえば、常にorigin = 0を使用し、最初と最後を変更したとします。アクティブポイントを確認するには、すべての中間ノードに沿って毎回サフィックスツリーをたどる必要があります。最後のノードからの距離だけを記録することにより、このパスをたどった結果をキャッシュすることは理にかなっています。

境界変数を「修正する」という意味のコード例を教えてください。

健康に関する警告:このアルゴリズムは特に理解しにくいので、この直感はすべての重要な詳細で正しくない可能性があることを認識してください...


学術論文の1つでは、文字列の「適切な接尾辞」に最初の文字が含まれていないことを意味する「適切な」と定義しています。時々、部分文字列全体を「接尾辞」と呼びますが、アルゴリズムを定義するとき、「文字列」と「部分文字列」と「接尾辞」という用語が自由に使われるため、「接尾辞」の意味を非常に明確にする必要がある場合があるため、 「適切なサフィックス」という用語は、全体をサフィックスと呼ぶことを除外します。したがって、文字列のサフィックスサブストリングは、任意の正当なサブストリングであり、同じサフィックスではない適切なサフィックスを持つことができます。論理だから。
ブレアホートン2016年

3

こんにちは私は上記で説明した実装をrubyで実装しようとしました。確認してください。正常に動作するようです。

実装の唯一の違いは、シンボルを使用するのではなく、エッジオブジェクトを使用しようとしたことです。

https://gist.github.com/suchitpuri/9304856にもあります

    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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.