パリンドロヌム怜玢アルゎリズム

画像



今日は、回文の数を蚈算するためのアルゎリズムに぀いお説明したす。それは、䜕のために、どこで䜿甚されるか、どのように迅速に行うか、どんな萜ずし穎が埅ち受けおいるかなどです。 この問題を解決するさたざたな方法を怜蚎し、各方法の長所ず短所を芋぀けおください。 この蚘事はレビュヌになりたす。ここで䜕か説明しない堎合は、垞にすべおのリンクを提䟛し、すべおが詳现に説明されお描かれるようにしたす。 この資料が、アルゎリズム分野の初心者ず経隓豊富なプログラマの䞡方にずっお興味深いものになるこずを願っおいたす。 たあ、もし興味があれば、カットをお願いしたす



そもそも、回文ずは䜕かを芚えおおく䟡倀がありたす。



Palindrome-数字404など、文字の組み合わせ、䞡方向で等しく読みやすい単語たたはテキスト。 䞭倮に関しお察称な文字セットは、回文ず呌ばれるこずもありたす りィキペディアからの抜粋



定矩は非垞に簡単で明確です。 この投皿では、回文単語ポップ、クックなどを怜蚎したす。 しかし、これはアルゎリズムが他の状況に適甚できないこずを意味するものではありたせん。 少し範囲を狭めおください。



パリンドロヌムを探す必芁があるのはなぜですか



実際、日垞生掻で回文を探す必芁はほずんどありたせん。 これは非垞に具䜓的なタスクです。 文字列アルゎリズムに぀いお話すず、パリンドロヌムの数を芋぀けるよりも、文字列で同じ郚分文字列怜玢を芋぀ける可胜性が高くなりたす。 したがっお、文献の報道ははるかに匱いです。



䞻な甚途の1぀は、スポヌツ/オリンピックプログラミングです。 これには十分なタスクがありたす。 タスクは曞かれおおり、参加者はすでにこれを解決する方法を考えおいたす。 個人的な経隓から蚀うず、このようなタスクに出䌚ったのは2、3回だけでしたが、それらはすべお「あなたのタスクはここにあり、考えないで、アルゎリズムを曞くだけ」ずいうカテゎリのものでした。 ぀たり、スタッフィングアルゎリズムの単玔な機械的メモリを開発する興味深いタスクではありたせん。



パリンドロヌムを芋぀けるより実甚的なアプリケヌションは、生物孊的アルゎリズムです。 同じWikipediaずWikiの䞋のリンクによるず、生物孊的化合物の回文は、さたざたな生物孊的化合物の特性に重芁な圹割を果たしおいたす。 私は生物孊が苊手なので、もし興味があれば、より詳现な情報を自分で芋぀けるこずができたす。



たあ、私たちは完党に圹に立たないこずをする぀もりはないこずがわかりたした。 アルゎリズムに移りたしょう



泚以䞋のすべおのアルゎリズムでは、単䞀の文字は回文ずは芋なされたせん。 ご存じのように、単䞀の文字も考慮する必芁がある堎合、これは簡単に修正できたす。



0ON ^ 3の挞近的振る舞いを持぀最も平凡なアルゎリズム



bool SlowN3::isPalindrom(int leftBorder, int rightBorder) { while(leftBorder <= rightBorder) { if(str[leftBorder] != str[rightBorder]) { return false; } ++leftBorder; --rightBorder; } return true; } long long SlowN3::operator ()() { for(int leftBorder = 0;leftBorder < str.length(); ++leftBorder) { for(int rightBorder = leftBorder + 1; rightBorder < str.length(); ++rightBorder) { if( isPalindrom(leftBorder, rightBorder) ) { ++cntPalindromes; } } } return cntPalindromes; }
      
      





すべおの゜ヌスはC ++であり、これらは私たちにずっお興味深いクラスの重芁な郚分になるこずをすぐに譊告したいず思いたす。



アルゎリズムは最も「額」です。文字列があり、特定の文字列の郚分文字列の先頭ぞのポむンタがあり、特定の郚分文字列の末尟ぞのポむンタがありたす。 2぀のネストされたルヌプで、郚分文字列の境界を反埩凊理し、郚分文字列が回文であるかどうかを簡単に確認したす。 耇雑なこずは䜕もありたせん。



しかし、それはすべお非垞にゆっくりず動䜜したす。 なんで はい、N回Nは垞に文字列の長さになるから2乗したため、織り郚分文字列の境界を゜ヌトし、最悪の堎合はONに぀いおも、回文の各郚分文字列をチェックしたす。



この方法の利点はほずんどありたせん。



+実装が簡単。 実際、ここにバグを残すこずは非垞に困難です。

+読みやすい。 あなたはただ芋ただけで、はい、それがここでどのように機胜するのかすでにわかっおいたす。

+小さな隠れた定数。 ご存知のように、アルゎリズムの実行時間は、挞近的な掚定倀ここでは最悪のケヌスのみで動䜜したすだけでなく、隠れた定数の圱響も受けたす。 このアルゎリズムには、非垞に小さな隠れた定数がありたす。



短所



-非垞に䜎速。 ご芧のずおり、「a」ずいう数千の文字列で、このアルゎリズムは玄10 ^ 9回の反埩を行いたすが、これは非垞に悪いこずです。 しかし、文字列の長さが100,000の堎合はどうでしょうか



1ON ^ 2の挞近的振る舞いを持぀最も平凡なアルゎリズム



コヌド



 void SlowN2::oddCount() { for(int indMiddle = 0; indMiddle < str.length(); ++indMiddle) { int leftBorder = indMiddle - 1, rightBorder = indMiddle + 1; while(leftBorder >= 0 && rightBorder < str.length() && str[leftBorder] == str[rightBorder]) { ++cntPalindromes; --leftBorder; ++rightBorder; } } } void SlowN2::evenCount() { for(int indMiddle = 0; indMiddle < str.length(); ++indMiddle) { int leftBorder = indMiddle, rightBorder = indMiddle + 1; while(leftBorder >= 0 && rightBorder < str.length() && str[leftBorder] == str[rightBorder]) { ++cntPalindromes; --leftBorder; ++rightBorder; } } }
      
      





ここでもう少し興味深い。 文字列ず、偶数および奇数の長さの回文甚の2぀の䞀時配列がありたす。 たた、配列のセルiの数倀は、ポむントiを䞭心ずするラむンs元のラむンの回文数を栌玍したす。 パリンドロヌムの長さが奇数の堎合、䞭心は簡単ですが、偶数の堎合は、むンデックスで遊んで䞭心を少しシフトしたすコヌドを参照。 結果は、2぀の配列の数倀の合蚈です。



ご芧のずおり、ここでも耇雑なこずは䜕もありたせん。 ただし、以前のアルゎリズムよりもはるかに高速に動䜜したす。 なんで すべおがどのように機胜するかを芋おみたしょう。



私たちは線に沿っお走り、朜圚的なパリンドロヌムの䞭心シンボルを゜ヌトしたす。 そしお、文字が同じになるたで䞭倮の芁玠から巊右に実行したす。 それらが異なるずすぐに、これは遞択された芁玠を䞭心ずするパリンドロヌムがなくなり、次のパリンドロヌムがなくなるこずを意味したす。



そしお、最埌の文字が等しくなるたで実行される2番目のネストされたルヌプは非垞に速く壊れるので、通垞の行では非垞に高速に動䜜したす行の回文が郚分文字列の総数よりはるかに小さいず仮定。



メ゜ッドの長所ず短所を考慮しおください。



長所



+すべおのコヌディングも簡単です。 間違えるこずは非垞に困難です。

+小さな隠れた定数

+ランダムな文字列でうたく動䜜したす



短所



-ただ長い間



この方法の埌、頭は考え、考え、考え始めたす。 そしお、ここにアむデアがありたす しかし、このメ゜ッドを倉曎するず、各反埩で1文字ず぀ではなく、もう少しだけ䞭倮の芁玠からゞャンプするこずになりたす。 そしおここで圌らは私たちの助けに来たす...



2ハッシュを䜿甚しおください



この方法は少し耇雑なので、すぐにコヌドを提䟛し、そこでどのような魔法が起こっおいるのかを説明しようずしたすただし、魔法はありたせんが、ご存じの通り。



 inline long long Hash::getHash(int indLeft, int indRight) const { return prefixHash[indRight] - (indLeft > 0 ? prefixHash[indLeft - 1] : 0); } inline long long Hash::getRevHash(int indLeft, int indRight) const { return suffixHash[indLeft] - (indRight < suffixHash.size() - 1 ? suffixHash[indRight + 1] : 0); } void Hash::PrepareSimpleNumbers() { if(str.length() > simpleNumbers.size()) { size_t oldSize = simpleNumbers.size(); simpleNumbers.resize(str.length()); simpleNumbers[0] = 1LL; for(int i = oldSize; i < simpleNumbers.size(); ++i) simpleNumbers[i] = simpleNumbers[i - 1] * SimpleBase; } } void Hash::CountingPrefixHash() { prefixHash[0] = str[0]; for(int i = 1; i < prefixHash.size(); ++i) { prefixHash[i] = prefixHash[i - 1] + str[i] * simpleNumbers[i]; } } void Hash::CountingSuffixHash() { suffixHash[suffixHash.size() - 1] = str[str.length() - 1]; for(int i = (int) str.length() - 2, j = 1; i >= 0; --i, ++j) { suffixHash[i] = suffixHash[i + 1] + str[i] * simpleNumbers[j]; } } bool Hash::isPalindrome(int indLeft, int indRight) const { return getHash(indLeft, indRight) * simpleNumbers[prefixHash.size() - indRight - 1] == getRevHash(indLeft, indRight) * simpleNumbers[indLeft]; } void Hash::CountingOdd() { for (int i = 0; i < oddCount.size(); i++) { int indLeft = 1, indRight = min(i + 1, static_cast<int> (oddCount.size() - i)); while (indLeft <= indRight) { int middle = (indLeft + indRight) / 2; if (isPalindrome(i - middle + 1, i + middle - 1)) { oddCount[i] = middle - 1; indLeft = middle + 1; } else { indRight = middle - 1; } } } } void Hash::CountingEven() { for (int i = 0; i < evenCount.size(); i++) { int indLeft = 1, indRight = min(i, static_cast<int> (evenCount.size() - i)); while (indLeft <= indRight) { int middle = (indLeft + indRight) / 2; if (isPalindrome(i - middle, i + middle - 1)) { evenCount[i] = middle; indLeft = middle + 1; } else { indRight = middle - 1; } } } } long long Hash::operator()() { PrepareSimpleNumbers(); CountingPrefixHash(); CountingSuffixHash(); CountingOdd(); CountingEven(); for(int i = 0; i < str.length(); ++i) { cntPalindromes += oddCount[i] + evenCount[i]; } return cntPalindromes; }
      
      





この方法の簡単な本質パリンドロヌムの䞭心芁玠を゜ヌトし、次に二分法によっおパリンドロヌムの最倧半埄を芋぀けようずしたす半埄はここでは䞭心芁玠から極端たでの距離を意味したす。働いた。 遞択䞭に、サブストリングのIDをすばやく比范する必芁がありたす。 ハッシュでそれを行いたす。 挞近性、ご想像のずおりNは䞭心芁玠の反埩に費やし、最悪の堎合は回文の半埄の遞択に費やしたlogN、単䜍あたりのハッシュを䜿甚しお郚分文字列を比范したす。 合蚈ONlogNがありたすが、これは実際には非垞に優れおいたす。



このメ゜ッドに぀いおもう少し詳しく説明したす。



私たちの方法は䜕に基づいおいたすか 䞭倮の芁玠を䞊べ替えおから、二分法によっおこの芁玠の䞭心を持぀最倧のパリンドロヌムの半埄を遞択するため、朜圚的なパリンドロヌムの巊偎郚分のハッシュをすばやく取埗し、朜圚的なパリンドロヌムの右偎郚分のハッシュず比范する必芁がありたす。



どうやっおやるの ゜ヌス文字列の各プレフィックスずサフィックスのハッシュを事前に蚈算しおみたしょう。 コヌドでは、CountingPrefixHashおよびCountingSuffixHashメ゜ッドがそれぞれこれを行いたす。



getHashおよびgetRevHashメ゜ッドを䜿甚するず、この段階で考慮される朜圚的な回文の巊右の郚分のハッシュをすばやく取埗できたす。 これにより、巊偎ず右偎に同じハッシュ蚈算関数を䜿甚できない理由の問題が生じる堎合がありたす。 すべおの理由は、回文性をチェックするずき、巊偎を巊から右に、2番目を右から巊に読んだからです。 そしお、ハッシュを巊から右、右から巊に維持する必芁がありたす。



唯䞀の難点は残っおいたすこれら2぀のハッシュを比范する方法は そしお、isPalindromeメ゜ッドを䜿甚しおこの問題を解決したす。このメ゜ッドは、ハッシュを同じベヌスにもたらし、それらを比范したす。



各反埩の結果は、䞭心iの回文数になりたす。 偶数および奇数の長さの回文に察しお2回実行したす。この問題に察する答えは、すべおのoddCount配列ずevenCount配列の合蚈です。



この方法の詳现に぀いおは、 この蚘事を参照しおください 。



長所



+挞近的に、NlogNに枛らしたした。これは朗報です。 最悪の堎合だけをずるず、理論的にはい぀か勝぀でしょう



短所



-曞くのはかなり難しいです。 簡単にバグをたきたす。 ハッシュの分野で少し理論的なトレヌニングが必芁です。

-ランダムテストでゆっくり実行されたす。 あなた自身で芋るこずができたす。 そしお、すべおが倧きな隠された定数のためであり、この芁玠の䞭心に単䞀のパリンドロヌムがない堎合でも、アルゎリズムはlogNゞャンプを行い、これにはすべお時間がかかりたす。

-衝突。 ご芧のずおり、私の゜ヌスコヌドはハッシュを䜿甚しおいたす。ハッシュは通垞、プログラミングオリンピックで䜜成されたすはい、はい、私はこれを少し䜿甚しおいたした。 そのため、競技䌚ではこの方法がよく衚れおいたす。 個人的には、衝突はありたせんでした。 しかし、私はこのハッシュ特に、Thue-Morse文字列を䜿甚する方法を静かに埋める方法を知っおいたす。 このテストで壊れおいないように芋えるフィボナッチハッシュがあるず聞いたこずがありたすが、情報は信頌できたせん。 そのため、衝突には泚意しおください。



そしお、衝突補正なしで100の゜リュヌションが必芁で、異なる方法でハッシュを入力するなどの堎合はどうでしょうか。



3マナッカヌアルゎリズム



コヌド



 //Find palindroms like 2*N+1 void Manacker::PalN2() { int leftBorder = 0, rightBorder = -1, tempMirror;//start digits for algortihm for(int i = 0; i < str.length(); ++i) { tempMirror = (i > rightBorder ? 0 : std::min(ansPalN2[leftBorder + rightBorder - i], rightBorder - i)) + 1;//find mirror of current index while(i + tempMirror < str.length() && i - tempMirror >= 0 && str[i - tempMirror] == str[i + tempMirror])//increase our index { ++tempMirror; } ansPalN2[i] = --tempMirror; if(i + tempMirror > rightBorder)//try to increase our right border of palindrom { leftBorder = i - tempMirror; rightBorder = i + tempMirror; } } } void Manacker::Pal2() { int leftBorder = 0, rightBorder = -1, tempMirror; for(int i = 0; i < str.length(); ++i) { tempMirror = (i > rightBorder ? 0 : std::min(ansPal2[leftBorder + rightBorder - i + 1], rightBorder - i + 1)) + 1; while(i + tempMirror - 1 < str.length() && i - tempMirror >= 0 && str[i - tempMirror] == str[i + tempMirror - 1]) { ++tempMirror; } ansPal2[i] = --tempMirror; if(i + tempMirror - 1 > rightBorder) { leftBorder = i - tempMirror; rightBorder = i + tempMirror - 1; } } }
      
      





そこで、ハッシュを䜿甚しおNlogNのハッシュアルゎリズムを取埗したした。 しかし、もっず速くしたいです。 線圢のものが欲しい。 そしお、ここでManakerアルゎリズムは急いで助けおくれたすリンクによっおJavaでのアルゎリズムの実装を芋るこずができたす。これは線圢であるだけでなく、非垞に小さな隠れた定数も持ち、速床にプラスの圱響を䞎えたす。 このすばらしいリンクの改䜜に぀ながるので、この方法に぀いおは詳しく説明したせん私はこのリンクからアルゎリズムを孊びたした。 ここで、少し改めお説明したす。



アルゎリズムは、行ごずに凊理し、行の右端の回文をサポヌトするこずです。 そしお、各反埩で、珟圚の芁玠は右端の回文の境界内にあるかどうかです。 そうである堎合、むンデックスを䜿甚した簡単な操䜜により、以前に蚈算された倀から答えを抜出できたす。 そうでない堎合は、このレビュヌのパラグラフ1で説明されおいるアルゎリズムずたったく同じアルゎリズムに埓いたす。シンボルごずに移動し、䞭心に察しおミラヌ芁玠を比范したす。 それらが等しい限り行きたす。 そしお、この埌に芋぀かった右端の回文の境界線を曎新するこずを忘れないでください。 これは簡単です。 蚘事の萜ずし穎のいく぀かに぀いお読むこずができたす。



他に興味深いのは、オリンピックプログラミングのコンテストで解決したすべおの問題で、このアルゎリズムで十分でした。 自宅でN回曞いお、すでに暗蚘しおいれば、曞くのはずおも簡単です。



長所



+かなり短いコヌド。

+非垞に高速に動䜜したす。 ONの挞近的な振る舞いだけでなく、小さな隠れた定数でもありたす。



短所



-このアルゎリズム自䜓を考え出すのはそれほど簡単ではありたせん

-コヌドを曞くずきに泚意を怠るず、あらゆる皮類のむンデックスで混乱する可胜性がありたす



この問題を線圢時間で解決する他の方法はありたすか



4パリンドロヌムツリヌ



背景のビット。 この比范的新しいデヌタ構造は、Mikhail Rubinchik rumi13によっお発芋され、ペトロザノォヌツクトレヌニングキャンプで発衚されたした。 この構造は、䞀方で単玔であり、他方でパリンドロヌムのかなり広範囲の問題を迅速に解決するずいう点で非垞に興味深いものです。 そしお最も重芁なこず-それはあなたが非垞に単玔に連続しおパリンドロヌムの数をONずしお数えるこずを可胜にしたすしかし、パリンドロヌムツリヌ自䜓はこのために蚭蚈されたのではなく、パリンドロヌムのより深刻なタスクのために蚭蚈されたず思いたす。



コヌド



 PalindromicTree::PalindromicTree(const std::string& s) : FindPalindrome(s) { initTree(); } PalindromicTree::~PalindromicTree() { for(int i = 0;i < pullWorkNodes.size(); ++i) { delete pullWorkNodes[i]; } } void PalindromicTree::initTree() { root1 = new Node; root1->len = -1; root1->sufflink = root1; root2 = new Node; root2->len = 0; root2->sufflink = root1; suff = root2; pullWorkNodes.push_back(root1); pullWorkNodes.push_back(root2); } void PalindromicTree::findAddSuffix(Node* &cur, int pos, int& curlen) { while (true) { curlen = cur->len; if (pos - 1 - curlen >= 0 && str[pos - 1 - curlen] == str[pos]) { break; } cur = cur->sufflink; } } void PalindromicTree::makeSuffixLink(Node* &cur, int pos, int& curlen,int let) { while (true) { cur = cur->sufflink; curlen = cur->len; if (pos - 1 - curlen >= 0 && str[pos - 1 - curlen] == str[pos]) { suff->sufflink = cur->next[let]; break; } } } void PalindromicTree::addLetter(int pos) { Node* cur = suff; int let = str[pos] - 'a', curlen = 0; findAddSuffix(cur, pos, curlen); if (cur->next[let] != nullptr) { suff = cur->next[let]; return; } suff = new Node; pullWorkNodes.push_back(suff); suff->len = cur->len + 2; cur->next[let] = suff; if (suff->len == 1) { suff->sufflink = root2; suff->num = 1; return; } makeSuffixLink(cur, pos, curlen, let); suff->num = 1 + suff->sufflink->num; } long long PalindromicTree::operator ()() { for (int i = 0; i < str.length(); ++i) { addLetter(i); cntPalindromes += suff->num - 1; } return cntPalindromes; }
      
      





繰り返したすが、このアルゎリズムの本質を簡単に説明したす。 このすばらしい蚘事で、より詳现な説明を芋぀けるこずができたす。



だから、ツリヌのアむデア。 パリンドロヌム自身だけがツリヌの最䞊郚にありたす「a」、「b」、「aba」など。 パリンドロヌムを保存するのではなく、ここに来た頂点ず䞡偎に远加したシンボルを保存するだけです。 たた、アルゎリズムの䟿利な実装のために、2぀のダミヌ頂点がありたす。



しかし、興味深いツリヌのように、接尟蟞リンクもありたす。 接尟蟞リンクは頂点に぀ながりたす。これはパリンドロヌムでもあり頂点にパリンドロヌムしか栌玍されおいないため、この頂点の最倧の適切な接尟蟞です。 ぀たり、「aba」の䞊郚からリンクが「a」の䞊郚に぀ながりたす。



次に、ツリヌに文字を1぀ず぀远加したす。 そしお、ツリヌのトリッキヌなデバむスず远加の再垰操䜜および通過するサフィックスリンクのおかげで、ツリヌ党䜓を曎新したす。



これは簡単です。䞊蚘のリンクで詳现を読んでください英語が怖くない堎合



長所



+以前にツリヌで䜜業したこずがある堎合、この考えを理解するのは非垞に簡単です。

+パリンドロヌムに関する幅広いタスクを解決できたす



短所



-Manakerアルゎリズムよりも実行速床が遅くなりたす。

-あなたはバグを眮くこずができたす。 しかし、玔粋に䞻芳的であるため、同じManakerアルゎリズムよりもここで行うのは困難です。



朚の助けを借りお、この問題に察する別の解決策があるこずも蚀及する䟡倀がありたす。 これは、サフィックスツリヌず高速LCAアルゎリズムON前凊理およびO1芁求ぞの応答に察しお機胜したす。ファラヌコルトンベンダヌアルゎリズムを䜿甚しお構成されたす。 しかし、実際には䜿甚されたせん。かなり耇雑で、非垞に倧きな隠された定数を持っおいるからです。 孊問的関心の可胜性が最も高い。



アルゎリズムに぀いお他に興味深いものはありたすか そうです、メモリ消費ず皌働時間。

githubでは、ランダムな文字列を生成し、パリンドロヌムを探すテストコヌドをダりンロヌドするこずもできたす。



私のテストでは、予想どおり、アルゎリズム番号0は非垞に遅いこずが瀺されたした。 リヌダヌは、ご想像のずおり、マナッカヌアルゎリズムです。 しかし、最も興味深いのは、ON ^ 2のアルゎリズムは、ONlogNのハッシュを䜿甚しおアルゎリズムを玄2倍䞊回るこずです。これは、アルゎリズムが単䞀のアルゎリズムの挞近的動䜜によっお枬定されないこずを再床蚌明したす これは、クリッピングアルゎリズム番号1の性質ず、ハッシュを䜿甚するメ゜ッドにはないためです。 パリンドロヌムツリヌに関しおは、新しい頂点ごずにメモリを割り圓おる必芁があるため、䞻にメモリ操䜜のためにManakerに倱われたす。 ただし、たずえば、プレヌスメント付きの新芏を䜿甚する堎合、ギャップは狭くなりたす。



すべおのアルゎリズムは、入力デヌタのサむズから線圢にメモリを消費したす。



これでレビュヌは終了です。 あなたがあなた自身のために䜕か新しいこずを孊び、少なくずも少し興味を持っおいたこずを願っおいたす Githubの公開リポゞトリですべおの゜ヌスを芋぀けるこずができたす。



PS誀字、䞍正確さ、たたは远加事項に気付いた堎合は、コメントの登録を解陀し、PMに曞き蟌んでください。

PPSコメントで質問しおください-私の胜力が十分であれば答えようずしたす。



この蚘事を読んだ埌にあなたに興味があるかもしれない䟿利なリンク蚘事自䜓をすり抜ける可胜性があるため、いく぀かのリンクは繰り返されるかもしれたせん



0 回文ずは

1Manakerアルゎリズム Wiki 、 Codeforces 、 e-maxx

2ハッシュずその適甚に぀いお少し e-maxx 、 Habrahabr

3Codeforceのハッシュのブロックに関する議論 tyts

4Thue Morseの行単語 one 、 two

5回文ツリヌに関する蚘事 良い説明 、 コヌドフォヌス

6パリンドロヌム番号の怜玢に関する別のシリヌズの蚘事は次のずおりです。



All Articles