単玔な接尟蟞ツリヌ

暹朚 サフィックスツリヌは、非構造化デヌタ配列での無数の耇雑な怜玢タスクを予想倖に効果的に解決できる匷力な構造です。 残念ながら、よく知られおいるサフィックスツリヌ構築アルゎリズム䞻にEsko Ukkonenによっお提案されたアルゎリズムは、理解するのが非垞に耇雑であり、実装に時間がかかりたす。 比范的最近になっお、2011幎にDany BreslauerずGiuseppe Italianoの努力により、比范的簡単な構築方法が発明されたした。これは、実際に、1973幎に接尟蟞朚を発明したPeter Weinerのアルゎリズムの簡略版です。 接尟蟞ツリヌが䜕であるかわからない堎合、たたは垞にそれを恐れおいる堎合、これはそれを研究し、同時に比范的単玔な構築方法を習埗するチャンスです。



サフィックスツリヌの説明に進む前に、甚語を定矩したす。 アルゎリズムの入力は、n文字s [0]、s [1]、...、s [n-1]で構成される文字列sです。 各文字はバむトです。 もちろん、ここで説明するすべおは、ビット、2バむトUnicode文字、DNAシヌケンスの2ビット文字など、他のシヌケンスでも機胜したす。 さらに、s [n-1]は文字0ず等しいず仮定したすが、これはsの他のどこにもありたせん。 この最埌の制限は、物語を単玔化するためだけのものであり、実際、それを取り陀くだけで十分です。 s [i..j] = s [i] s [i + 1] ... s [j]ずいう圢匏の行iずjは敎数は、通垞どおり、郚分文字列ず呌ばれたす。 s [i..n-1]ずいう圢匏の郚分文字列iは敎数は、サフィックスず呌ばれたす。



だから、䞻人公。 文字列sの接尟蟞ツリヌは、頂点の数による最小ツリヌであり、各接尟蟞s [i..n-1]がルヌトから特定のシヌトぞ、たたはその逆で読み取れるように、各゚ッゞに空でないサブストリングsがマヌクされたす。 、ルヌトからリヌフぞのパスで読み取られる各行はサフィックスsです。 この定矩を凊理する最も簡単な方法は、たずえば珟時点では、pos、len、およびtoに泚意しないでくださいです。

サフィックスツリヌ



接尟蟞ツリヌにある頂点は2n + 1以䞋です。 サフィックスを連続しお挿入しおツリヌを構築する堎合、これを確認できたす。次のサフィックスを挿入するず、新しいリヌフずこのリヌフがアタッチされる頂点が衚瀺されたす。 s [n-1]は特殊文字であるため、接尟蟞ツリヌには正確にn個の葉がありたす。 頂点に敎数で番号を付けたす。 頂点vの父をパヌ[v]に保持したす。 vからファザヌvたでの゚ッゞのラベルは、数倀のペアずしお栌玍されたす。ラベルの長さlen [v]ず、このラベルの盎埌の䜍眮pos [v]は、sにありたす。 ラベルが文字列tの堎合、t = s [pos [v] -len [v] .. pos [v] -1]。 頂点vにk個の子があり、t 1 、t 2 、...、t kがvから子たでの゚ッゞ䞊のラベルであるずしたす。 行t 1 、t 2 、...、t kの最初の文字はペアごずに異なるこずに泚意しおください。぀たり、子vぞの参照は[v]ぞのマップに栌玍でき、察応する子孫のラベルの最初の文字を衚瀺したす。 コヌドを短くし、「すごい、なんおコンパクトなコヌドだ」ず考えるために、構造を蚘述するオブゞェクト指向のアプロヌチを避けたす。 したがっお、接尟蟞ツリヌは次のように衚されたすpos、len、および䞊蚘の図に泚意を払うこずができたす。

int sz; //    int pos[0..sz-1], len[0..sz-1], par[0..sz-1]; //par[v] –   v std::map<char, int> to[0..sz-1]; //to[v] –     v
      
      





最初の重芁なプロパティは、サフィックスツリヌがOnメモリを占有するこずです。



ルヌトから頂点vぞのパスに曞かれた行は、strvで瀺されたす。 strvは掚論にのみ䜿甚され、どこにも明瀺的に保存されたせん。



サフィックスツリヌの䟋



サフィックスツリヌに慣れるず同時に、その䜿甚方法を理解するために、いく぀かの䟋を怜蚎しおください。



sの郚分文字列を怜玢したす



おそらく最初に思い浮かぶのは、サフィックスツリヌを䜿甚しお郚分文字列を怜玢するこずです。 文字列uが文字列sの郚分文字列であるこずに気付くのは非垞に簡単です。これは、uが接尟蟞ツリヌのルヌトから読み取るこずができる堎合のみです䞀郚のiずjに察しおu = s [i..j]であり、したがっお接尟蟞s [i .n-1]はuで始たりたす。



文字列sの異なる郚分文字列の数



同じ理由で、各郚分文字列sが接尟蟞ツリヌのある蟺のラベル䞊の特定の䜍眮に察応するこずを確立できたす。 したがっお、異なる郚分文字列の数はそのような䜍眮の数であり、ルヌトを陀くすべおの頂点vの合蚈len [v]に等しくなりたす。



デヌタ圧瞮LZ77



䟋はもっず耇雑です。 LZ77google Lempel、Zivを圧瞮するずいう考え方は簡単で、次の擬䌌コヌドで説明できたす。

 for (int i = 0; i < n; i = j+1) //  s[0..n-1] if ( s[i]     s[0..i-1]) { j = i,      s[i]; //      } else { j = max{j    d < i ,  s[i..j] = s[d..d+ji]}; d =  0,1,
,i-1 ,  s[d..d+ji] = s[i..j];       (j-i+1, id) //      } }
      
      





たずえば、文字列「aababababaaab」は「a1,1b7,23,10」ずしお゚ンコヌドされたすabababa行の「重耇」に泚意しおください。 もちろん、実装の詳现は倧きく異なる可胜性がありたすが、基本的な考え方は倚くの圧瞮アルゎリズムで䜿甚されおいたす。 サフィックスツリヌを䜿甚するず、Onで同様の方法でsを圧瞮できたす。 これを行うために、start [v]がs [p..p + | strv| -1] = strvの最小pず等しくなるように、ツリヌの各頂点vをフィヌルドstart [v]で補完したす。 v| strvの長さです。 葉の堎合、この倀は簡単に蚈算できるこずは明らかです。 残りの頂点に぀いおは、開始フィヌルドはツリヌの1回の深さによっお蚈算されたす。 start [v] = min {start [v 1 ]、start [v 2 ]、...、start [v k ]}、v 1 、v 2 、...、v kはvの子です。 ここで、圧瞮アルゎリズムでjの次の倀を蚈算するには、ツリヌの珟圚の頂点vが開始する[v] <i;になるたで、ルヌトから文字列s [i..n-1]を読み取るだけで十分です。 dは、最埌のそのような倀start [v]に等しいものを遞択できたす。



サフィックスツリヌの構築



単玔化されたWeinerアルゎリズムはUkkonenアルゎリズムおよび埓来のWeinerアルゎリズムよりも単玔であるにもかかわらず、それでもなお非自明でないアルゎリズムであり、それを理解するにはある皋床の努力が必芁であるこずを事前に譊告したす。



䞀般的な蚈画。 プレフィックスリンク



アルゎリズムは空のツリヌから始たり、サフィックスs [n-1..n-1]、s [n-2..n-1]、...、s [0..n-1]を順番に远加したす念のため、議論䞭の実装では、extend関数は、サフィックスが最短から始たる長さの昇順で正確に远加された堎合にのみ正しく機胜するこず、぀たり、たずえば、コヌドforint i = n-3; i> = 0; i--extend i「バグを含む

 for (int i = n-1; i >= 0; i--) extend(i); //    s[i..n-1]
      
      





したがっお、ルヌプのk番目の繰り返しで、文字列s [n-k + 1..n-1]の接尟蟞ツリヌがあり、接尟蟞s [nk..n-1]がツリヌに远加されたす。 新しい最長のサフィックスを远加するには、1぀の新しいリヌフず、堎合によっおは叀い゚ッゞを「分割」する1぀の新しい頂点を挿入する必芁があるこずは明らかです。 䞻な問題は、新しいシヌトが添付されるツリヌ内の䜍眮を芋぀けるこずです。 この問題を解決するために、サフィックスツリヌにプレフィックスリンクが远加されおいたす 。



各頂点vに察しお、プレフィックスリンクlink [v]-文字を頂点番号にマッピングするマップを次のように関連付けたす。シンボルの堎合、リンク[v] [a]は、str w= a strvそしおこの堎合、リンク[v] [a] = w。 叀兞的なWeinerアルゎリズムに粟通しおいる堎合、プレフィックスリンクは「明瀺的な」叀兞的なプレフィックスリンクに察応しおいるこずに気づきたした。「暗黙的な」リンクはたったく必芁ないこずがわかりたした。Ukkonenアルゎリズムに粟通しおいる堎合は、プレフィックスリンクは「サフィックスリンク」ず逆になりたす。図では、プレフィックスリンクは赀で瀺され、長方圢にはリンクに察応するシンボルが含たれおいたすダミヌ頂点0の意味に぀いおは以䞋を参照。

プレフィックスリンク



したがっお、远加の構造がありたす。

 std::map<char, int> link[0..sz-1]; // 
      
      





定矩により、各頂点に぀ながるプレフィックスリンクは1぀だけであるため、リンクはOnメモリを占有するず結論付けたす。



たず、ここで玹介する実装の技術的なニュアンスに぀いお説明したす。 ツリヌのルヌトは頂点1、0はすべおの文字aに察しおリンク[0] [a] = 1であるようなダミヌ頂点です。 さらに、par [1] = 0およびlen [1] = 1です。ダミヌの頂点には特別なセマンティックロヌドはありたせんが、いく぀かの特別なケヌスを同じ方法で凊理できたす。 これに぀いおは以䞋で詳しく説明したすが、今のずころは泚意を払っおはいけたせん。 新しいサフィックスを挿入するアルゎリズムの説明に進みたす。



アルゎリズム



補題の図 補題。 iを0からn-2たでの数倀ずしたす。 文字列s [k..n-1]のサフィックスツリヌを考えたす。ここで、k <= iです。 wがs [i..n-1]に察応するルヌトからリヌフぞのパス䞊の非ルヌト頂点である堎合、ルヌトからs [i + 1..n-1]に察応するリヌフぞのパス䞊には、次のような頂点vがありたす。 s [i] strv= strwおよびlink [v] [s [i]] = w図を参照。



それを蚌明したしょう。 strw= s [i] tを瀺したす。 接尟蟞ツリヌの定矩により、wはリヌフであるか、wには少なくずも2぀の子孫がありたす。 wが葉の堎合、vは文字列s [i + 1..n-1]に察応する葉です。 wを葉にしないでください。 次に、[w] [a]が1぀の子wに぀ながり、[w] [b]が別の子wに぀ながるような、いく぀かの異なる蚘号aずbがありたす。 s [i] taずs [i] tbはsの郚分文字列です少なくずも1぀は䜍眮iで始たりたせん。 したがっお、taずtbも郚分文字列であり、ツリヌ内にstrv= tずなる頂点vが存圚する必芁がありたす。 vがs [i + 1..n-1]に察応するシヌトぞのパス䞊にあり、プレフィックスリンクの定矩により、リンク[v] [s [i]] = wであるこずは明らかです。



したがっお、文字列s [i + 1..n-1]のサフィックスツリヌがあり、文字列s [i..n-1]のシヌトを远加し、それに応じおプレフィックスリンクを曎新するずしたす。 䞋の写真をご芧ください。 s [i + 1..n-1]に察応する叀いシヌトを瀺したす。 新しいシヌトの「ドッキングポむント」は頂点w 'であり、ただ䜜成されおいない可胜性がありたす。 wによっお、w 'のすぐ䞊にある頂点を瀺したすwが既にツリヌにある堎合は、w = w'で、この堎合は新しい葉を远加するだけで十分です。 簡単にするために、最初にwがルヌトでない堎合を考えたす。 補題により、リンク[v] [s [i]] = wになるように、叀いパスからルヌトぞのパスに頂点vがありたす。

新しいシヌトを挿入する



事実1. vからoldぞのパスには、v "'リンク[v' '] [s [i]]が定矩されおいるような頂点はありたせん。 それどころか、v ''がそのようなピヌクであるずしたす。 次に、頂点リンク[v ''] [s [i]]はw 'の祖先であり、同時にwの子孫でもありたすプレフィックスリンクの定矩による。 しかし、ドッキングポむントから最も近いピヌクずしおwを遞択したした 論争。



アルゎリズムでは、たずvずwを芋぀けたす。 同時に、vlen倉数の倀| strv| +1を蚈算し、パススタックに枡されたすべおの頂点を远加したす-それらは匕き続き䟿利です。 サむクルの最埌で、s [i + vlen]は、vからoldぞのパスの子孫vぞの゚ッゞの最初の文字に等しいこずに泚意しおください䞊の図を参照。

  int v, vlen = n - i, old = sz – 1;//   old –    for (v = old; !link[v].count(s[i]); v = par[v]) { vlen -= len[v]; path.push(v); } int w = link[v][s[i]];
      
      





事実2. to [w] [s [i + vlen]]が定矩されおいない堎合に限り、頂点w 'はツリヌに既に存圚したす。 この堎合、w = w '。 䞊の図ず、シンボルs [i + vlen]の意味に関する発蚀を少し黙想するず、このステヌトメントが明らかになりたす。



事実3. vから叀いパスぞのパスには、vがありたす。たずえば、wがただ存圚しおいなくおも、s [i] strv '= strw'です。 w 'ツリヌにすでに存圚する堎合、ステヌトメントは事実2およびv' = vから明らかです。 wがただ䜜成されおいないずしたす。 u = to [w] [s [i + vlen]]を衚したす。 ルヌトuを持぀サブツリヌから任意の葉を遞択したす。 この葉をjの接尟蟞s [j..n-1]に察応させたす。 ただ挿入されおいない接尟蟞s [i..n-1]ず接尟蟞s [j..n-1]は、「暗黙の」頂点w 'で分岐したす。 ただし、s [j + 1..n-1]およびs [i + 1..n-1]に察応するリヌフは、ツリヌで既に衚されおおり、頂点v 'で分岐しおいたす。 s [i] strv '= strw'であるこずは明らかです。぀たり、vはvからoldぞのパスにありたす。 この時点で、䞊の図を最埌に芋る䟡倀がありたす。 アルゎリズムの䞻芁郚分に進みたす。



wが芋぀かったので、タスクは新しいシヌトのドッキングポむントを芋぀けるこずです。 実際、len [w ']ず頂点v'を探しおいたす。 w = w 'の堎合、シヌトをwにアタッチし、叀いシヌトからこのシヌトぞのプレフィックスリンクを描画するだけで十分です。 w= W 'の堎合の耇雑なケヌス、぀たり シンボルs [i + vlen]によるwからの゚ッゞがありたす。 再びu = to [w] [s [i + vlen]]を瀺したす。 s [i] strv '= strw'のような頂点v 'が芋぀かるたで、パススタックから頂点を順番に取埗したす。 適切なピヌクが芋぀かったこずをどのように刀断したすか v 1 、v 2 、...、v kを、v k = v 'のパススタックの最䞊郚の頂点ずしたす䞋図を参照。 v pからoldぞのパス䞊の子v pに続く゚ッゞの最初の文字をpで瀺したす。 0 = s [i + vlen]を定矩したす。 tをwからuたでの゚ッゞ䞊のラベルずする。 シンボルt [len [v 1 ] + len [v 2 ] + ... + len [v p -1]]は、t䞊のpに察応するシンボルず呌ばれたす。 w 'はwからuぞの゚ッゞ䞊の接尟蟞s [i..n-1]の分岐点であるため、シンボルa kは t䞊の察応するシンボルず等しくありたせん。 䞀方、同じ理由で、各p = 0,1、...、k-1に察しお、シンボルa pはt䞊の察応するシンボルず等しくなりたす。 図面を䜿甚するず、この掚論がより明確になりたす。



パスタックでv 'を怜玢



埗られた発蚀は、単玔化されたWeinerアルゎリズムの䞭栞です。 興味深いこずに、すべおの文字ではなく、゚ッゞの最初の文字のみをtの察応する文字ず比范すれば十分です。 本質的に、これは簡単な芳察です。これはブレスラりアヌずむタリアヌノが気づいたこずですが、䜕らかの理由で誰も気付いおいたせんでした。 vからto wぞのプレフィックスリンクず叀いシヌトから新しいシヌトぞのプレフィックスリンクを描画するこずを忘れないでください。 そのため、アルゎリズムは次のように新しいサフィックスs [i..n-1]を挿入したす。

  if (to[w].count(s[i + vlen])) { // w != w' int u = to[w][s[i + vlen]]; //sz –    w',    //.. w!=w',  s[pos[u] - len[u]] == s[i + vlen]      for (pos[sz] = pos[u] - len[u]; s[pos[sz]] == s[i + vlen]; pos[sz] += len[v]) { v = path.top(); path.pop(); //   v' vlen += len[v]; }    w  u  w'=sz; len[w'] = len[u]-(pos[u]-pos[sz]) link[v][s[i]] = sz; //   v'  w' w = sz++; // w = w'     } //    w  w' link[old][s[i]] = sz; //   old    sz   sz  w; len[sz] = n – (i + vlen) pos[sz++] = n; //pos     n
      
      





wがルヌトである堎合、特別なケヌスを怜蚎する必芁がありたす。 この状況は完党に䌌おいたすが、䞀郚の倀は1぀シフトされたす。 もちろん、このケヌスを個別に凊理するこずも可胜ですが、ダミヌの頂点を䜿甚する方が簡単であり、远加のコヌドを蚘述する必芁はありたせん。 この堎所で重芁な圹割を果たしおいるのは、すべおの文字aに぀いお、par [1] = 0、len [1] = 1、link [0] [a] = 1です。 したがっお、頂点vの怜玢サむクルは必ず頂点0で終了し、倀len [1] = 1は必芁なシフトを1぀提䟛したす。 詳现を理解するのは難しくありたせん。これは挔習ずしお残しおおきたす。 これに関する架空のピヌクの秘密の意味が明らかにされるこずを願っおいたす。 すべおを組み合わせるこずで、次の゜リュヌションが埗られたす。

 void attach(int child, int parent, char c, int child_len) //  { to[parent][c] = child; len[child] = child_len; par[child] = parent; } void extend(int i) //  ;   i=n-1,n-2,...,0 { int v, vlen = n - i, old = sz - 1; for (v = old; !link[v].count(s[i]); v = par[v]) { vlen -= len[v]; path.push(v); } //   vlen = |str(v)|+1 int w = link[v][s[i]]; if (to[w].count(s[i + vlen])) { // w != w' int u = to[w][s[i + vlen]]; for (pos[sz] = pos[u]-len[u]; s[pos[sz]]==s[i + vlen]; pos[sz] += len[v]) { v = path.top(); path.pop(); //   v' vlen += len[v]; } attach(sz, w, s[pos[u] - len[u]], len[u] - (pos[u] - pos[sz])); // w'(=sz)  w attach(u, sz, s[pos[sz]], pos[u] - pos[sz]); // u  w'(=sz) w = link[v][s[i]] = sz++; //    v'  w';  w = w'     } //    w   w' link[old][s[i]] = sz; //       attach(sz, w, s[i + vlen], n - (i + vlen)); //   sz   w' pos[sz++] = n; //pos     n } void init() { len[1] = 1; pos[1] = -1; par[1] = 0; //,  par[1] = 0  len[1] = 1 (!) for (int c = 0; c < 256; c++) link[0][c] = 1;//  0         }
      
      







アルゎリズムの実行時間の掚定



結論ずしお、説明した構造党䜓がOn操䜜を実行する理由を理解したす。 頂点vの高さは、ルヌトからvたでのパス䞊の頂点の数です。 アルゎリズムのステップは、ツリヌに新しいサフィックスを远加するこずであるこずを思い出させおください。 i番目のステップで最も長い接尟蟞に察応する頂点の高さをh iで瀺したす。 k iを、ステップiでoldからvに移動した頂点の数ずしたす。 h iの倀はどのように倉化したすか 補題の図をもう䞀床芋るず、反射時にh i <h i-1 -k i-1 + 2であるこずがわかりたす。i番目のステップで実行される操䜜の数はk iに比䟋したす。圌らが気づいたこず、Oh i + 1 -h i + 2。 このこずから、アルゎリズムが合蚈でO2n +h 1 -h 2 +h 2 -h 3 + ... +h n-1 -h n = On操䜜を実行するこずは明らかです。



小さな最適化のメモ。 マップぞのアクセス時間を考えるず、WeinerアルゎリズムおよびUkkonenの時間はOn log mです。ここで、mはストリング内の異なる文字の数です。 アルファベットが非垞に小さい堎合は、マップの代わりに配列を䜿甚しお、Weinerを真に線圢にするこずをお勧めしたす。



公平に蚀うず、簡略化されたWeinerアルゎリズムおよび叀兞的なアルゎリズムなどがUkkonenよりも玄1.5倍から2倍のメモリを消費するこずは泚目に倀したす。 テストでは、単玔化されたWeinerの動䜜が玄1.2倍遅いこずも瀺されたしたここでは、Ukkonenよりわずかに劣っおいたす。 それにもかかわらず、これらの欠点はすべお、Weinerの実装の容易さず倚数の萜ずし穎の欠劂によっお郚分的に盞殺されおいたす。



時系列のリンク



P.ワむナヌ。 「線圢パタヌンマッチングアルゎリズム」1973は、Weinerの最初の蚘事で、接尟蟞ツリヌを玹介し、線圢アルゎリズムを瀺しおいたす。



E.マクレむト。 「スペヌス節玄型サフィックスツリヌ構築アルゎリズム」1976は、より軜量なサフィックスツリヌ構築アルゎリズムです。



E.うっこねん。 「接尟蟞ツリヌのオンラむン構築」1995-McCrateアルゎリズムの修正。 最も人気のある最新のアルゎリズム。



D.ブレスラりアヌ、G。むタリアヌノ。 「フリンゞマヌクされた祖先問題によるリアルタむムのサフィックスツリヌの構築」20132011幎の暫定版-この蚘事の簡略化されたWeinerアルゎリズムの説明は1段萜10ペヌゞの泚釈であり、他のすべおは別の関連する問題に圓おられおいたす。



PSアルゎリズムのテストずコヌド改善の提案に協力しおくれたMisha RubinchikずOleg Merkuryevに感謝したす。



PPS結論ずしお、私は匕甚したす
シンプルで完党な実装
 //      ,   ""     . //         (    ). #include <string> #include <map> const int MAXLEN = 600000; std::string s; int pos[MAXLEN], len[MAXLEN], par[MAXLEN]; std::map<char,int> to[MAXLEN], link[MAXLEN]; int sz = 2; int path[MAXLEN]; void attach(int child, int parent, char c, int child_len) { to[parent][c] = child; len[child] = child_len; par[child] = parent; } void extend(int i) { int v, vlen = s.size() - i, old = sz - 1, pstk = 0; for (v = old; !link[v].count(s[i]); v = par[v]) { vlen -= len[v]; path[pstk++] = v; } int w = link[v][s[i]]; if (to[w].count(s[i + vlen])) { int u = to[w][s[i + vlen]]; for (pos[sz] = pos[u] - len[u]; s[pos[sz]] == s[i + vlen]; pos[sz] += len[v]) { v = path[--pstk]; vlen += len[v]; } attach(sz, w, s[pos[u] - len[u]], len[u] - (pos[u] - pos[sz])); attach(u, sz, s[pos[sz]], pos[u] - pos[sz]); w = link[v][s[i]] = sz++; } link[old][s[i]] = sz; attach(sz, w, s[i + vlen], s.size() - (i + vlen)); pos[sz++] = s.size(); } int main() { len[1] = 1; pos[1] = 0; par[1] = 0; for (int c = 0; c < 256; c++) link[0][c] = 1; s = "abababasdsdfasdf"; for (int i = s.size() - 1; i >= 0; i--) extend(i); }
      
      








All Articles