web-dev-qa-db-ja.com

Manacherのアルゴリズム(線形時間で最長の回文部分文字列を見つけるアルゴリズム)

Manacherのアルゴリズムを消化しようとして約6〜8時間費やした後、タオルを投げる準備ができました。しかし、私がやる前に、暗闇の中で最後のショットがあります:誰もそれを説明できますか?コードは気にしません。 [〜#〜]アルゴリズム[〜#〜]を誰かに説明してほしい。

ここは、アルゴリズムを説明する上で他の人が楽しんでいるように見える場所のようです: http://www.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html

「abba」などの文字列を#a#b#b#a#に変換したいと思う理由がわかりました。たとえば、前述のWebサイトの著者は、アルゴリズムの重要な部分は次のように述べています。

                      if P[ i' ] ≤ R – i,
                      then P[ i ] ← P[ i' ]
                      else P[ i ] ≥ P[ i' ]. (Which we have to expand past 
                      the right Edge (R) to find P[ i ])

これは間違っているようです。P[i '] = 7で、P [i]がR-i以下でない場合、P [i]は5に等しいとある時点で言うからです。

アルゴリズムに慣れていない場合は、さらにいくつかのリンクがあります: http://tristan-interview.blogspot.com/2011/11/longest-palindrome-substring-manachers.html (I 'これを試してみましたが、用語はひどく混乱しています。まず、定義されていないものがあります。また、変数が多すぎます。どの変数が何を参照しているかを思い出すためにチェックリストが必要です。

もう一つは: http://www.akalin.cx/longest-palindrome-linear-time (幸運)

アルゴリズムの基本的な要点は、線形時間で最長の回文を見つけることです。最小から中程度の労力でO(n ^ 2)で実行できます。このアルゴリズムは、O(n)に到達するのに非常に「賢い」ことになっています。

68
user678392

リンクの説明でロジックが完全に正しくないことに同意します。以下に詳細を示します。

Manacherのアルゴリズムは、iを中心とする回文がどの程度伸びているかを含むテーブルP [i]を埋めます。 P [5] = 3の場合、位置5の両側の3文字は回文の一部です。アルゴリズムは、長いパリンドロームを見つけた場合、左側のPの値を見て、パリンドロームの右側のPの値をすばやく埋めることができるという事実を利用します。同じ。

あなたが話していたケースを説明することから始め、必要に応じてこの答えを拡張します。

Rは、Cを中心とした回文の右側のインデックスを示します。これは、指定した場所の状態です。

C=11
R=20
i=15
i'=7
P[i']=7
R-i=5

ロジックは次のとおりです。

if P[i']<=R-i:  // not true
else: // P[i] is at least 5, but may be greater

リンクの擬似コードは、テストが失敗した場合、P [i]がP [i ']以上である必要があることを示していますが、R-i以上である必要があると考えており、説明はそれを裏付けています。

P [i ']はRiよりも大きいため、i'を中心とするパリンドロームはCを中心とするパリンドロームを超えます。iを中心とするパリンドロームは、少なくともその点まで対称であるため、しかし、それ以上は明示的に検索する必要があります。

P [i ']がRiより大きくない場合、i'を中心とする最大のパリンドロームはCを中心とする最大のパリンドローム内にあるため、P [i]がP [i]を超えることはないことがわかっていました。 ']。もしそうなら、矛盾が生じます。つまり、iを中心とするパリンドロームをP [i ']を超えて拡張できることを意味しますが、可能であれば、対称性のためにi'を中心とするパリンドロームを拡張することもできますが、できるだけ大きくすることになっています。

このケースは前に説明されています:

C=11
R=20
i=13
i'=9
P[i']=1
R-i=7

この場合、P [i '] <= R-iです。 Cを中心とするパリンドロームの端から7文字離れているため、iの少なくとも7文字はi 'の7文字と同じであることがわかります。 i 'の周囲に1文字の回文があったため、iの周囲にも1文字の回文があります。

j_random_hackerは、ロジックを次のようにする必要があることに気付きました。

if P[i']<R-i then
  P[i]=P[i']
else if P[i']>R-i then
  P[i]=R-i
else P[i]=R-i + expansion

P [i '] <R-iの場合、P [i] == P [i']であることがわかります。これは、Cを中心とする回文構造の内部にいるためです。

P [i ']> R-iの場合、P [i] == R-iであることがわかります。それ以外の場合、Cを中心とする回文はRを超えて拡張しているためです。

したがって、拡張は、P [i '] == R-iである特別な場合にのみ必要であるため、P [i]での回文がより長くなるかどうかはわかりません。

これは、実際のコードでP [i] = min(P [i ']、R-i)を設定し、常に展開することで処理されます。この方法では、拡張が不要な場合、拡張にかかる時間が一定であるため、時間の複雑さは増加しません。

38
Vaughn Cato

このサイトのアルゴリズムは特定のポイントに理解できるようです http://www.akalin.cx/longest-palindrome-linear-time

この特定のアプローチを理解するには、紙の問題を解決し、可能なセンターごとに回文をチェックしないように実装できるトリックをキャッチするのが最善です。

最初に自分自身に答えてください-あなたが与えられた長さのパリンドロームを見つけたら、5としましょう-次のステップとして、あなたはこのパリンドロームの最後にジャンプすることはできませんか?

長さ8の回文を作成し、長さが8を超える別の回文を配置しようとすると、その中心は最初の回文の右側にあります。試してみてください:長さ8のパリンドローム-WOWILIKEEKIL-Like + ekiL = 8今ではほとんどの場合、2つのEを中心として数字8を長さとして書き、最後のLの後にジャンプして探すことができます大きな回文の中心。

このアプローチは正しくありません。大きな回文の中心はekiL内にある可能性があり、最後のLの後にジャンプすると見逃してしまいます。

LIKE + EKILを見つけたら、これらのアルゴリズムが使用する配列に8を配置します。これは次のようになります。

[0,1,0,3,0,1,0,1,0,3,0,1,0,1,0,1,8]

for

[#、W、#、O、#、W、#、I、#、L、#、I、#、K、#、E、#]

秘Theは、おそらく8の後の次の7(8-1)の数字が左側と同じであることを既に知っているということです。そのため、次のステップは、8の左から8の右に7の数字を自動的にコピーすることですまだ決まっていないことに注意してください。配列は次のようになります

[0,1,0,3,0,1,0,1,0,3,0,1,0,1,0,1,8,1,0,1,0,1,0,3](私たちは8歳です

for

[#、W、#、O、#、W、#、I、#、L、#、I、#、K、#、E、#、E、#、K、#、I、#、L]

このようなジャンプが現在のソリューションを破壊し、何に気付くことができるかを例に挙げましょう。

WOWILIKEEKIL-EKIL内のどこかにセンターを配置して、より大きなパリンドロームを作成してみましょう。しかし、それは不可能です-Word EKILを回文を含むものに変更する必要があります。何? OOOOOh-それはトリックです。現在のパリンドロームの右側に中心がある大きなパリンドロームを持つ唯一の可能性は、パリンドロームの右側(および左側)に既にあるということです。

WOWILIKEEKILに基づいて作成してみましょう。大きなパリンドロームの中心としてEKILをたとえばEKIKに変更する必要があります。LIKEをKIKEに変更することも忘れないでください。トリッキーな回文の最初の文字は次のとおりです。

WOWIKIKEEKIK

前に言ったように-最後に私がKIKEEKIKよりも大きなパリンドロームの中心になりましょう:

WOWIKIKEEKIKEEKIKIW

配列を古いpallindromに合わせて、追加情報を平均化する方法を見つけましょう。

for

[_ W _ O _ W _ I _ K _ I _ K _ E _ E _ K _ I _ K _ E _ E _ K _ I _ K _ I _ W]

[0,1,0,3,0,1,0,1,0,3,0,3,0,1,0,1,8

次のI-3番目が最も長いパリンドロームになることはわかっていますが、少し忘れてみましょう。配列の数字を8の左から右にコピーします(8つの数字)

[0,1,0,3,0,1,0,1,0,3,0,3,0,1,0,1,8,1,0,1,0,3,0,3]

ループでは、Eと8の間にあります。K(現在の最大のパリンドロームの最後の文字)にジャンプできないI(最大のパリンドロームの中間)の特別な点は何ですか?特別なことは、それが配列の現在のサイズを超えていることです...どのように? 3の右に3スペース移動すると、配列から外れます。それは、それが最大のパリンドロームの真ん中になり、ジャンプできる最も遠いのはこの手紙Iであることを意味します。

この答えの長さで申し訳ありません-アルゴリズムを説明したかったので、あなたを保証できます-@OmnipotentEntityが正しかった-あなたに説明した後、私はそれをさらによく理解します:)

12

これまでのところ、次のリンクで最良の説明の1つを見つけました。

http://tarokuriyama.com/projects/palindrome2.php

また、質問で言及された最初のリンクで使用された同じ文字列の例(babcbabcbaccba)の視覚化もあります。

このリンクとは別に、私もコードを見つけました

http://algs4.cs.princeton.edu/53substring/Manacher.Java.html

このアルゴリズムの核心を理解しようと懸命に努力している他の人に役立つことを願っています。

11
scv

完全な記事: http://www.zrzahid.com/longest-palindromic-substring-in-linear-time-manachers-algorithm/

まず、いくつかの興味深い特性を見つけるために、パリンドロームを注意深く観察します。たとえば、S1 = "abaaba"およびS2 = "abcba"は両方とも回文ですが、それらの間の非自明な(長さや文字ではない)違いは何ですか? S1は、i = 2とi = 3の間の不可視の空間(存在しない空間!)を中心とした回文です。一方、S2はi = 2(つまりc)の文字を中心にしています。奇数/偶数の長さに関係なく、回文の中心を優雅に処理するために、文字の間に特殊文字$を挿入して回文を変換します。次に、S1 = "abba"およびS2 = "abcba"は、i = 6およびT2 = "$ a $ b $ c $ bを中心としたT1 =" $ a $ b $ a $ a $ b $ a $ "に変換されます。 i = 5を中心とした$ a $」。これで、センターが存在し、長さが2 * n + 1で一貫していることがわかります(n =元の文字列の長さ)。例えば、

 i 'ci 
 ------------------------------------- ---------------- 
 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 
 ------------------------------------------- ---------- 
 T1 = | $ | | $ | b | $ | | $ | | $ | b | $ | | $ | 
 ------------------------------------------- ---------- 

次に、中心cの周りの(変換された)パリンドロームTの対称性から、0 <= k <= cでT [c-k] = T [c + k]であることを観察します。つまり、位置c-kとc + kは互いに鏡像です。別の言い方をすれば、中心cの右側にあるインデックスiの場合、ミラーインデックスi 'はc-i' = i-c => i '= 2 * c-iとなるようにcの左側にあります。あれは、

回文部分文字列の中心cの右側にある各位置iについて、iのミラー位置はi '= 2 * c-iであり、逆も同様です。

P [i。]がiを中心とするパリンドロームの長さと等しくなるように、配列P [0..2 * n]を定義しましょう。実際には、長さは元の文字列の文字数によって(特殊文字$を無視して)測定されることに注意してください。また、minとmaxを、それぞれcを中心とするパリンドローム部分文字列の左端と右端の境界とする。したがって、min = c-P [c]およびmax = c + P [c]です。たとえば、パリンドロームS = "abaaba"の場合、変換されたパリンドロームT、ミラー中心c = 6、長さ配列P [0..12]、min = cP [c] = 6-6 = 0、max = c + P [c] = 6 + 6 = 12と2つのサンプルミラーリングされたインデックスiおよびi 'を次の図に示します。

 min i 'ci max 
 ----------------------------------- ------------------ 
 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 
 ------------------------------------------- ---------- 
 T = | $ | | $ | b | $ | | $ | | $ | b | $ | | $ | 
 ------------------------------------------- ---------- 
 P = | 0 | 1 | 0 | 3 | 0 | 5 | 6 | 1 | 0 | 3 | 0 | 1 | 0 | 
 ------------------------------------------- ---------- 

このような長さの配列Pを使用すると、Pの最大要素を調べることにより、最長のパリンドローム部分文字列の長さを見つけることができます。つまり、

P [i]は、変換された文字列Tのiを中心とするパリンドローム部分文字列の長さです。元の文字列Sのi/2の中心。したがって、最長のパリンドローム部分文字列は、長さP [iの部分文字列になります最大]インデックス(i最大-P [i最大])/ 2そのようなi最大 Pの最大要素のインデックスです。

非パリンドロームの例の文字列S = "babaabca"について、以下に同様の図を描いてみましょう。

 min c max 
 | ---------------- | ----------------- | 
 ---------------------------------------------- ---------------------- 
 idx = | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 
 ------------------------------------------- -------------------------- 
 T = | $ | b | $ | | $ | b | $ | | $ | | $ | b | $ | c | $ | | $ | 
 ------------------------------------------- -------------------------- 
 P = | 0 | 1 | 0 | 3 | 0 | 3 | 0 | 1 | 4 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 
 ------------------------------------------- -------------------------- 

問題は、Pを効率的に計算する方法です。対称プロパティは、左側のミラーインデックスi 'で以前に計算されたP [i']を使用してP [i]を計算するために使用できる以下のケースを示唆しているため、多くの計算をスキップします。まず最初に、参照回文(最初の回文)があるとしましょう。

  1. 中心が最初の回文の右側にある3番目の回文は、2番目の回文が最初の回文の範囲内にある場合、左側のミラーの中心に固定されている2番目の回文とまったく同じ長さになります。少なくとも1文字。
    たとえば、c = 8を中心とし、min = 4およびmax = 12を境界とする最初のパリンドロームの次の図では、i = 9を中心とする3番目のパリンドロームの長さ(ミラーインデックスi '= 2 *を使用) ci = 7)は、P [i] = P [i '] = 1です。これは、i'を中心とする2番目の回文が最初の回文の境界内にあるためです。同様に、P [10] = P [6] = 0。
     
     
     | ---- 3rd ---- | 
     | ---- 2nd ---- | 
     | ----------- 1st Palindrome --------- | 
     min i 'ci max 
     | ----- ------- | --- | --- | ------------- | 
     --------------- -------------------------------------------------- --- 
     idx = | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 
     ------------------------------------------- -------------------------- 
     T = | $ | b | $ | | $ | b | $ | | $ | | $ | b | $ | c | $ | | $ | 
     ------------------------------------------- -------------------------- 
     P = | 0 | 1 | 0 | 3 | 0 | 3 | 0 | 1 | 4 | ? | ? | ? | ? | ? | ? | ? | ? | 
     -------------------------------------------- ------------------------- 
    
    今、このケースを確認する方法は?対称プロパティのため、セグメントの長さ[min..i ']はセグメントの長さ[i..max]に等しいことに注意してください。また、2番目のパリンドロームは、左の境界、1番目のパリンドロームの最小値の内側にある場合、2番目のパリンドロームは完全に1番目のパリンドローム内にあることに注意してください。あれは、
     
     i'-P [i ']> = min 
     => P [i']-i '<-min(否定)
     => P [i '] <i'-min 
     => P [i'] <max-i [(max-i)=(i'-min)対称性のため]。
    
    ケース1のすべての事実を組み合わせる
    P [i] = P [i ']、iff(max-i)> P [i']
  2. 2番目のパリンドロームが最初のパリンドロームの左境界を満たしているか、それを超えて延びている場合、3番目のパリンドロームは、少なくとも自身の中心から最初のパリンドロームの右外側の文字までの長さを持っています。この長さは、2番目の回文の中心から最初の回文の左端の文字まで同じです。
    たとえば、次の図では、i = 5を中心とする2番目の回文は、最初の回文の左境界を超えて延びています。したがって、この場合、P [i] = P [i ']とは言えません。しかし、i = 11を中心とする3番目の回文の長さP [i]は、少なくともその中心i = 11からcを中心とする最初の回文の右境界max = 12までの長さです。つまり、P [i]> = 1です。これは、3番目の回文がmaxを超えて拡張される可能性があることを意味します。maxを超える次の即時文字がミラー化された文字と正確に一致する場合にのみ、たとえば、この場合、P [13]!= P [9]であり、拡張できません。したがって、P [i] = 1。
                                                        
     | ------- 2回目の回文------ | | ---- 3rd ---- | ----? 
     | ----------- 1st Palindrome --------- | 
     min i 'ci max 
     | ---- | ----------- | ----------- | ----- | 
     --------------- -------------------------------------------------- --- 
     idx = | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 
     ------------------------------------------- -------------------------- 
     T = | $ | b | $ | | $ | b | $ | | $ | | $ | b | $ | c | $ | | $ | 
     ------------------------------------------- -------------------------- 
     P = | 0 | 1 | 0 | 3 | 0 | 3 | 0 | 1 | 4 | 1 | 0 | ? | ? | ? | ? | ? | ? | 
     -------------------------------------------- ------------------------- 
    
    では、このケースを確認する方法は?これは、ケース1のチェックに失敗しただけです。つまり、2番目の回文は、最初の回文の左端を越えて拡張されます。
     
     i'-P [i '] <min 
     => P [i']-i '> = -min [negate] 
     => P [i ']> = i'-min 
     => P [i']> = max-i [(max-i)=(i'-min)対称性のため]。 
    
    つまり、P [i]は少なくとも(max-i)iff(max-i)P [i]> =(max-i)、iff(max-i)です。 maxを超えて拡張する場合、新しい回文の中心と境界を更新する必要があります。
    Iを中心とするパリンドロームがmaxを超えて拡大する場合、新しい(拡張された)パリンドロームがあるため、c = iに新しい中心があります。 maxを新しい回文の右端の境界に更新します。
    ケース1とケース2のすべての事実を組み合わせると、非常に美しい小さな式を思いつくことができます。
     
    ケース1:P [i] = P [i ']、iff(max-i)> P [i'] 
    ケース2:P [i]> = (max-i)、iff(max-i)= min(P [i ']、max-i)。 
    
    つまり、3番目の回文がmaxを超えて拡張可能でない場合、P [i] = min(P [i ']、max-i)です。そうでなければ、c = iと新しいmax = i + P [i]の中心に新しい3番目の回文があります。
  3. 最初の回文も2回目の回文も、中心が最初の回文の右側の外側にある4回目の回文の回文長を決定するのに役立つ情報を提供しません。
    つまり、i> maxの場合、P [i]を先制的に決定することはできません。あれは、
    Max-i <0の場合、P [i] = 0
    すべてのケースを組み合わせて、式を結論付けます:
    P [i] = max> i? min(P [i ']、max-i):0。maxを超えて展開できる場合、c = iの新しい中心に関して、maxを超える文字をミラーリングされた文字と一致させることによって展開します。最後に、不一致がある場合、新しいmax = i + P [i]を更新します。

参照: wikiページのアルゴリズムの説明

5
user3674869

Animation Graphicsを使用して、このアルゴリズムに関するビデオを作成しました。うまくいけば、あなたがそれを理解するのに役立つでしょう! https://www.youtube.com/watch?v=kbUiR5YWUpQ

1

この資料は、私が理解するのに非常に役立ちます。 http://solutionleetcode.blogspot.com/2013/07/leetcode-longest-palindromic-substring.html

Tを、各文字を中心とした最長のパリンドローム部分文字列の長さとして定義します。

重要なことは、より小さなパリンドロームがより長いパリンドローム内に完全に埋め込まれている場合、T [i]はより長いパリンドローム内でも対称であるべきです。

そうでない場合は、対称の左部分から誘導するのではなく、ゼロからT [i]を計算する必要があります。

1
Lin Jian

文字列内で最長の回文を見つけるための高速Javascriptソリューション:

const lpal = str => {
  let lpal = ""; // to store longest palindrome encountered
  let pal = ""; // to store new palindromes found
  let left; // to iterate through left side indices of the character considered to be center of palindrome
  let right; // to iterate through left side indices of the character considered to be center of palindrome
  let j; // to iterate through all characters and considering each to be center of palindrome
  for (let i=0; i<str.length; i++) { // run through all characters considering them center of palindrome
    pal = str[i]; // initializing current palindrome
    j = i; // setting j as index at the center of palindorme
    left = j-1; // taking left index of j
    right = j+1; // taking right index of j
    while (left >= 0 && right < str.length) { // while left and right indices exist
      if(str[left] === str[right]) { //
        pal = str[left] + pal + str[right];
      } else {
        break;
      }
      left--;
      right++;
    }
    if(pal.length > lpal.length) {
      lpal = pal;
    }
    pal = str[i];
    j = i;
    left = j-1;
    right = j+1;
    if(str[j] === str[right]) {
      pal = pal + str[right];
      right++;
      while (left >= 0 && right < str.length) {
        if(str[left] === str[right]) {
          pal = str[left] + pal + str[right];
        } else {
          break;
        }
        left--;
        right++;
      }
      if(pal.length > lpal.length) {
        lpal = pal;
      }
    }
  }
  return lpal;
}

入力例

console.log(lpal("gerngehgbrgregbeuhgurhuygbhsbjsrhfesasdfffdsajkjsrngkjbsrjgrsbjvhbvhbvhsbrfhrsbfsuhbvsuhbvhvbksbrkvkjb"));

出力

asdfffdsa
0
class Palindrome
{
    private int center;
    private int radius;

    public Palindrome(int center, int radius)
    {
        if (radius < 0 || radius > center)
            throw new Exception("Invalid palindrome.");

        this.center = center;
        this.radius = radius;
    }

    public int GetMirror(int index)
    {
        int i = 2 * center - index;

        if (i < 0)
            return 0;

        return i;
    }

    public int GetCenter()
    {
        return center;
    }

    public int GetLength()
    {
        return 2 * radius;
    }

    public int GetRight()
    {
        return center + radius;
    }

    public int GetLeft()
    {
        return center - radius;
    }

    public void Expand()
    {
        ++radius;
    }

    public bool LargerThan(Palindrome other)
    {
        if (other == null)
            return false;

        return (radius > other.radius);
    }

}

private static string GetFormatted(string original)
{
    if (original == null)
        return null;
    else if (original.Length == 0)
        return "";

    StringBuilder builder = new StringBuilder("#");
    foreach (char c in original)
    {
        builder.Append(c);
        builder.Append('#');
    }

    return builder.ToString();
}

private static string GetUnFormatted(string formatted)
{
    if (formatted == null)
        return null;
    else if (formatted.Length == 0)
        return "";

    StringBuilder builder = new StringBuilder();
    foreach (char c in formatted)
    {
        if (c != '#')
            builder.Append(c);
    }

    return builder.ToString();
}

public static string FindLargestPalindrome(string str)
{
    string formatted = GetFormatted(str);

    if (formatted == null || formatted.Length == 0)
        return formatted;

    int[] radius = new int[formatted.Length];

    try
    {
        Palindrome current = new Palindrome(0, 0);
        for (int i = 0; i < formatted.Length; ++i)
        {
            radius[i] = (current.GetRight() > i) ?
                Math.Min(current.GetRight() - i, radius[current.GetMirror(i)]) : 0;

            current = new Palindrome(i, radius[i]);

            while (current.GetLeft() - 1 >= 0 && current.GetRight() + 1 < formatted.Length &&
                formatted[current.GetLeft() - 1] == formatted[current.GetRight() + 1])
            {
                current.Expand();
                ++radius[i];
            }
        }

        Palindrome largest = new Palindrome(0, 0);
        for (int i = 0; i < radius.Length; ++i)
        {
            current = new Palindrome(i, radius[i]);
            if (current.LargerThan(largest))
                largest = current;
        }

        return GetUnFormatted(formatted.Substring(largest.GetLeft(), largest.GetLength()));
    }
    catch (Exception ex) 
    {
        Console.WriteLine(ex);
    }

    return null;
}
0