This Little Miracle-Knuth-Morris-PrattCLCアルゎリズム

Knuth-Morris-Prattアルゎリズムは 、ストリング内のサブストリングパタヌンを怜玢するために䜿甚されたす。 それはもっず簡単かもしれないようです。線に沿っお移動し、文字をパタヌンず順次比范したす。 䞀臎したせんでした。比范の開始点を1ステップ移動しお、再床比范したす。 パタヌンを芋぀けるか、行の終わりに達するたで、ずいうように。 関数は次のようなものです。
//      //        -1,    int find(char *, char *) { // i-     // j-     for (int i=0;[i];++i) { for (int j=0;;++j) { if (![j]) return i; //   if([i+j]!=[j]) break; } //   ,    } //   return -1; }
      
      



簡単な怜玢ケヌス 'needle'-サンプル 'stackistogiststigstigigstogstog needle stack stack needle stack'-怜玢文字列 良いのは、内偎のサむクルがすぐに䞭断されるずきです単玔な堎合のように、たずえば、ステップ1〜3で。 ただし、サンプルず文字列に頻繁にネストされた郚分が繰り返し含たれおいる堎合䞊蚘の難しいケヌスのように、内郚ルヌプはサンプルの終わり近くで䞭断し、怜玢時間はO<サンプル長> * <ストリング長>ずしお評䟡されたす。 ストリングの長さが10䞇で、サンプルの長さが100の堎合、O1000䞇が埗られたす。 もちろん、100の長さのサンプルを芋぀けるこずは非垞にたれですが、オリンピアヌドタスクでは「怜玢する」こずは䞀般的なこずです。したがっお、単玔な怜玢は倚くの堎合適切ではありたせん。 、そしおその堎で、あなたが単語を入力するずきブラりザがこれをどれほど速くするかに気づいおいたすか KMPアルゎリズムは、文字列ずその速床O<サンプル長> + <文字列長>内のすべおのサンプルを怜出するため、倧きなテキスト/サンプルたたは匱いプロセッサ䜎予算の携垯電話などでは競合したせん。 なぜ「小さい」のですか ILCのハむラむトはプレフィックス関数であり、非垞に小さいためです。 なぜ「奇跡」なのですか 完党に異なる問題を解決するように思われるため、この解決策は、玠晎らしいトリックの埌、文字列内のサンプルのすべおの出珟を芋぀ける問題の解決策になりたす。プレフィックス関数が䜕をどのように行うかを理解するために、耇雑な文字列を芋おみたしょう
a a b a a b a a a a b a a b でも a a b
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
0 1 0 1 2 3 4 5 2 2 3 4 5 6 7 8 9 3
その䞋の行は、行内のシンボルの番号䜍眮アルゎリズムの説明の郜合䞊、1で番号を怜蚎しおくださいであり、䞀番䞋の行はプレフィックス長の配列M、プレフィックス機胜を理解するためのキヌです。 Kは1〜6で、プレフィックス文字列文字列の最初のむンデックスで始たる郚分文字列ずサフィックス文字列の䜍眮7これは「私たちの」文字aの長さKの最埌の文字を持぀郚分文字列
K プレフィックス 接尟蟞
1 でも a ここではすべおが簡単です。プレフィックスは文字列の最初の文字で、サフィックスは私たちの文字です
2 ああ ba
3 aab あば
4 アヌバ アヌバ 最長䞀臎、接尟蟞ず接頭蟞がここず䞋に重なり始めたす怜玢文字列の断片のように
5 アヌバヌ バアバ
6 アヌバアブ アバアバ
泚長さ4の堎合、接尟蟞文字のシヌケンスずしおは接頭蟞ず䞀臎し、これは接尟蟞が接頭蟞ず䞀臎するKの最倧倀です。 プレフィックス長の配列の察応する䜍眮7に入力されるのはそれです。 K = 7の堎合、接頭蟞も接尟蟞ず䞀臎するこずに気付くでしょう。これは同じ行であるためです。 しかし、この些现なケヌスは私たちには適しおいない、なぜなら アルゎリズムを機胜させるには、サブストリングが必芁ですS-゜ヌスストリングを瀺したすSn-長さnのストリングSの先頭プレフィックス、S [n]-ストリングSの䜍眮nのシンボルS. M [n]配列内の倀SM [n]-䜍眮nの最倧長の接頭蟞および接尟蟞ず同じ行簡朔にするため、Pnで瀺したす。 文字列Pnは䞀皮の「仮想」であり、圢成されず、どこにも曞き蟌たれたせん。 これは、長さM [n]の元の文字列Sの開始フラグメント1です。 そしお、この最初のfragment1は、長さM [n]のfragment2ず䞀臎し文字のシヌケンスずしお、その最埌の文字は䜍眮nにありたす。 M [n] = 0の堎合、䞀臎はありたせん配列の䜍眮7は倀M [7] = 4で埋められ、最長行はP7=長さ4の ' aaba '自然で、䜍眮8に移動しお埋めたすM [8]。 1から7たでのすべおのプレフィックスずサフィックスの長さを愚かに数え、それらを比范し、最倧長を䜍眮8に曞き蟌むこずができたす。しかし、他の方法ILCの埌に進みたす。 長さkの最倧の長い行P8を芋぀けたしょう。これは、䜍眮8の接頭蟞ず接尟蟞です。最初のk-1文字からの行p7は、䜍眮k-1の接頭蟞ず接尟蟞です。 7番目のポゞションで最も長いずいう事実ではありたせん。 ただし、p7 = P7であるこずが刀明した堎合、P8はP7の1文字の拡匵です。 P7を1ポゞション拡匵できるかどうかを確認するには、サフィックスに远加された文字これは文字S [8] = a が次のプレフィックス文字ず䞀臎するかどうかを確認する必芁がありたす。 接頭蟞aの次の文字は、䜍眮M [7] + 1 = 5にありたすこれがなぜかを考えおください。 䞀臎した堎合そしお、この堎合は䞀臎した堎合、タスクは完了したす-M [8] = M [7] +1、および8番目の䜍眮にあるP8= P7+文字S [8] = a 。 P8= ' aabaa 'を取埗したす。 拡匵が成功するず、配列の次の倀を蚈算するために必芁な比范は1぀だけです。 ちなみに、配列に沿っお移動する堎合、倀は最倧1増加する可胜性がありたす。 䟿宜䞊、耇雑な行を繰り返しお、䞊䞋に移動する必芁がないようにしたす。
a a b a a b a a a a b a a b でも a a b
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
0 1 0 1 2 3 4 5 2 2 3 4 5 6 7 8 9 3
別のケヌス-P8を展開できたせんでした。 文字S [9] = aは、䜍眮M [8] + 1 = 6 bの文字列Sの文字ず䞀臎したせんでした。 サフィックスは簡単に展開されたす新しい文字が単に末尟に远加されるためが、問題のあるプレフィックスが付いおいるため、 接尟蟞に远加される文字は、次の接頭蟞文字ず䞀臎しない堎合がありたす。 接頭蟞Pkが収たらない堎合、同じ接尟蟞を持぀別の短い接頭蟞を芋぀ける必芁があり、それを展開しおみおください。 ただし、接頭蟞は短く、同じ接尟蟞ではS [M [M [k]]です。 配列Mを埋めるずき、各芁玠には同じ接尟蟞を持぀最長の接頭蟞の長さが含たれたす。 したがっお、SM [k]を展開できなかった堎合は、SM [M [k]]なども䞀臎するたで、たたは長さがれロになるたで次の文字Sを単玔に比范する必芁がありたす 1mの文字列S。 必芁なすべおの情報がすでに配列Mにあるため、適切なプレフィックスの怜玢サむクルは非垞に速く終了したす。この行では、P8はP7の1文字だけの拡匵であり、1回の比范が必芁です。 ただし、S [9] = aおよびS [M [8] + 1 = 6] = bであるため、P8をP9に展開できたせんでした。 長さM [8] = 5のプレフィックスP8が適合しなかったため、長さM [5] = 2のプレフィックスを詊したす。 たた、S [2 + 1] = bに適合したせん。 長さM [2] = 1のプレフィックスを詊しおみたす。 S [1 + 1] = a 。 したがっお、M [9] = 2、拡匵可胜なプレフィックスの長さより1぀倚くなりたす。 M [10]を埋めるには、2぀の比范が必芁です確認できたす。 ただし、11〜17の芁玠を入力するには、1぀の比范が必芁です。 その結果、配列倀の蚈算にはO文字列の長さのオヌダヌの時間がかかるため、塗り぀ぶしアルゎリズムを䜿甚するず、倚かれ少なかれ明確になりたす。

私たちはトリックに目を向けたす。

文字列aabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaのサンプルabaaaを芋぀けるには
a a b a a @ a a b a a b a a a a b a a b でも a a b
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
0 1 0 1 2 0 1 2 3 4 5 3 4 5 2 2 3 4 5 3 4 5 2 3
蚘号「@ 」はセパレヌタヌの圹割を果たしたすが、サンプルや怜玢行には芋圓たりたせんこのような蚘号だけを遞択する必芁がありたす。 配列の11、14、19、22の䜍眮を芋おみたしょう。 配列の倀は5です。これは、長さ5のサフィックス怜玢文字列のフラグメントが5぀のプレフィックス文字に䞀臎するこずを意味したす。 プレフィックスの5文字-これが怜玢のパタヌンです 怜玢アルゎリズムは次のずおりです-セパレヌタを䜿甚しおサンプルず怜玢文字列を接着し、接頭蟞関数の「接着」を枡し、サンプルの長さに等しい芁玠を配列で調べたす。 倀がセパレヌタ文字ず倀のためにサンプルの長さを超えないこずがわかりたすサンプルの長さず等しい倀は、元の怜玢文字列に察応する䜍眮にのみ衚瀺できたす。 接着された文字列の長さは<サンプル長> + <文字列長>であるため、蚘事の冒頭で述べたように、蚈算時間はO<サンプル長> + <文字列長>ず掚定されたす。 必芁なバッファプレフィックス関数のボリュヌムは<サンプル長> + <ラむン長>ですが、バッファ<サンプル長>が十分になるようにプレフィックス関数を倉曎できたす付録の倉曎䟋

プレフィックス機胜

そしお今、プレフィックス関数の䟋。 䞊蚘の説明ずの違いは、C蚀語の慣䟋に埓っお、むンデックスは0からカりントされるこずです。

C ++の䟋

この関数は、文字列プレフィックスの長さのベクトルを返し、文字列は文字列ずしお枡されたす長さを蚈算する必芁はありたせん
 vector<size_t> prefix_function (string s) { size_t n = s.length(); vector<size_t> pi(n); //  i-  (  i-1)           i. // p[0]=0 , p[1]=1,      for (size_t i=1; i<n; ++i) { // ,  -   size_t j = pi[i-1]; //   -,   while ((j > 0) && (s[i] != s[j])) //   , j = pi[j-1]; //    - if (s[i] == s[j]) ++j; //   ( ) - pi[i] = j; } return pi; }
      
      



Cの䟋

関数は䜕も返したせん。 最初のパラメヌタヌは文字列ぞのポむンタヌであり、2番目はプレフィックス長の入力された配列ぞのポむンタヌです。3番目のpは文字列ず配列の長さです。
 //     size_t,   int void prefix_function (char *s, int *pi, size_t n) { pi[0]=0; //  i-  (  i-1)           i. // p[0]=0 , p[1]=1,      for (size_t i=1; i<n; ++i) { int j = pi[i-1]; while ((j > 0) && (s[i] != s[j])) //   j = pi[j-1]; //     (   ) if (s[i] == s[j]) //  ++j; pi[i] = j; } }
      
      



このコヌドは、議論の結果ずしお远加されたした。 パタヌンず怜玢文字列は、異なるパラメヌタヌで枡されたす。 それらを「䞀緒に接着する」必芁はありたせん。 プレフィックス長の配列は、パタヌンに察しおのみ入力されたす。
 //   ,       int f(size_t i) { printf("%d\n",i); return 1; } // str  . // obr ,  . // pi      (    ). // int f(size_t i)   ,   , //       str . //   0,      1,   . void prefix_find (char *str, char *obr, size_t *pi, int (*f)(size_t)) { pi[0]=0; //  i-  (  i-1)   //          i. // p[0]=0 , p[1]=1,      size_t l; //    //       for (l=1; obr[l]; ++l) { size_t j = pi[l-1]; while ((j > 0) && (obr[l] != obr[j])) //   j = pi[j-1]; //     (   ) if (obr[l] == obr[j]) //  ++j; pi[l] = j; } size_t j=0; //   ,     //   .        i for (size_t i=0; str[i]; ++i) {
      
      



この堎所は、ILCの有効性の鍵です。 文字列の次の文字ずパタヌンが䞀臎しなかった堎合、パタヌンを1文字移動せず、最初から文字列ず比范したせん蚘事の冒頭のプリミティブ怜玢のように。 パタヌンを数文字シフトしお、str [i]ずobr [j]を1回比范したす。 䞀臎しない堎合、文字が䞀臎するか、パタヌンの先頭に到達するたで、パタヌンを再床シフトしたす。 T.O. 怜玢バヌに沿っお埌方に移動するのではなく、前方に移動したすただし停止したす。
  while ((j > 0) && (str[i] != obr[j])) //         .  , //   ,   j       //    j+1   (  j)  i+1  . j = pi[j-1]; //  j=0,         if (str[i] == obr[j]) //     ++j; //      1 if (j==l) if (!f(i-l+1)) //  ,    return; //    ,    0. } }
      
      



小さな奇跡に぀いおの私の話-ILCアルゎリズムは終わりたした。 もちろん、ILCに関するこの蚘事は最初のものではなく、最埌のものではありたせん。 habrahabrに関する2぀の蚘事を次に瀺したす。郚分文字列を怜玢したす。 Knuth – Morris –プラットアルゎリズムず郚分文字列 怜玢ず関連事項 。しかし、誰かがこの蚘事を自分に圹立぀ものず考え、誰か結局これが起こる可胜性があるがILCを適甚するこずを願っおいたす。 たずえ圌が䜜品の内郚メカニズムをよく理解しおいなかったずしおも理解するのに遅すぎるこずはありたせん。 KMPアルゎリズムは唯䞀の高速怜玢アルゎリズムではありたせんが、olympiadなどの問題に察しお 十分に高速であるず同時にシンプルです。 Boyer-Mooreアルゎリズムは耇雑さがILCに近く、特定の範囲のタスクサンプルに反埩フラグメントが含たれおいない堎合の方が高速です。 しかし、その埌、さたざたなタスクの範囲蚘事の䟋のように、サンプルず怜玢文字列に反埩シヌケンスが含たれる堎合では、ILCに比べお著しく劣りたす。 最埌に、どちらかを遞択するこずが奜みの問題であるタスクがありたすこれは議論されおいたせん。 堎合によっおはたたは地域でさえ、 Aho-KorasikアルゎリズムがILCずBoyer-Mooreの䞡方よりも効率的であるこずが刀明する堎合がありたす。 しかし、本圓にこのアルゎリズムを必芁ずする人は、䞀般的な博芧䌚、 IMHOを必芁ずしたせん。

ディスカッション補遺

O行の長さ+サンプルの長さではなくOサンプルの長さを䜿甚するように、プレフィックス関数を倉曎できたす。 この堎合、サンプルのプレフィックスの長さを保存するためだけにメモリを割り圓おる必芁があり、怜玢文字列のプレフィックスの長さは配列に保存されたせん。 珟圚の䜍眮の長さは倉数に保存され、サンプルの長さず比范されるずすぐに぀たり、サンプルが芋぀かるず、プレフィックス関数は芋぀かったフラグメントの凊理関数を呌び出したす。 倉曎されたprefix_find関数が蚘事に远加されたした。 実際、配列にメモリを割り圓おるこずで節玄する必芁はありたせんでしたが、誰かがそれを必芁ずする可胜性があるこずを陀倖したせん。Mayorovpはメモリを節玄する機䌚に泚意を喚起し、 メッセヌゞの䞭でプレフィックス関数のバリアントの実装ぞのリンクを䞎えたしたcoutで芋぀かったフラグメントの䜍眮。 コメントによれば、staticlabは倉数のタむプをsize_tに倉曎したしたこれは実際により正確です。蚘事を最埌たで読んだ人のために、接頭蟞ず接尟蟞のブロックがかなり耇雑な堎合にどのように線成されるかを瀺す図を瀺したす
アヌトワヌク
れロず1の文字列は同じです。最初の2぀の図ではむンデックスは0から、次では-1からです。実際、これは重芁ではありたせん。郚分文字列の長さが配列に曞き蟌たれ、文字列の文字に番号を付ける方法は重芁ではありたせん。最埌の図- 「バヌズアむ」は、文字列hahrhabhabrhabrhabrhabhabrhabhabrhabhabrhabrhabrhabhabrhabraのプレフィックスずサフィックスを、むンデックスず長さの配列なしで調べたす。







All Articles