2つのシーケンスが与えられた場合、一方の終わりともう一方の始まりの間の最大の重なりを見つける


11

次の問題を解決するには、効率的な(疑似)コードを見つける必要があります。

(必ずしも明瞭ではない)整数の二つの配列を考慮する(a[1], a[2], ..., a[n])(b[1], b[2], ..., b[n])、最大見つけるdようにa[n-d+1] == b[1]a[n-d+2] == b[2]、...、とa[n] == b[d]

これは宿題ではありません。実際には、できるだけ多くの次元で2つのテンソルを縮小しようとしたときにこれを思いつきました。効率的なアルゴリズムが存在するのではないO(n)かと思う(たぶん?)が、そうでないものは思いつきませんO(n^2)O(n^2)アプローチは、上の明白なループになりd、その後、項目の内部ループは最大を打つまでに必要な条件をチェックしますd。しかし、私はこれより良い何かが可能であると思います。


配列内のオブジェクトのグループに対してローリングハッシュを計算できる場合、これはより効率的に実行できると思います。要素のハッシュを計算し、それが一致する場合はb[1] to b[d]配列a計算ハッシュに進み、それがa[1] to a[d]答えです。そうでない場合は、計算されたハッシュをa[2] to a[d+1]再利用してハッシュを計算しa[1] to a[d]ます。しかし、配列内のオブジェクトがローリングハッシュの計算に適しているかどうかはわかりません。
SomeDude

2
@becko申し訳ありませんが、私はあなたが達成しようとしていることをようやく理解したと思います。これは、の終わりaとの始まりの間の最大の重なりを見つけることですbこのように
user3386109

1
問題は文字列マッチングのバリエーションであるように見えますが、これはKnuth–Morris–Prattアルゴリズムのバリエーションで解決できます。実行時間であろうO(M + N)は、ここでmの要素数でありa、そしてn内の要素の数ですb。残念ながら、KMPをどのように適応させるかを説明する十分な経験がありません。
user3386109

1
@ user3386109私のソリューションは、ハッシュ関数としてホーナーの方法を使用した、Rabin-Karpと呼ばれる文字列照合アルゴリズムのバリエーションでもあります。
ダニエル

1
@ダニエルああ、ローリングハッシュがどこかで使用されているのを見たことは知っていましたが、どこにいたのか思い出せませんでした:)
user3386109

回答:


5

次のような線形時間(O(n))アルゴリズムであるzアルゴリズムを利用できます。

長さnの文字列Sが与えられると、Zアルゴリズムは配列Zを生成します。 ここで、Z [i]S [i]から始まる最長の部分文字列の長さで もあり、これもSのプレフィックスです。

あなたの配列を連結する必要がある(B + )と第1まで、得られた構築アレイ上のアルゴリズムを実行IようにZ [i]が + I == M + N

例えば、のため= [1、2、3、6、2、3]&B = [2、3、6、2、1、0]、連結[2、3、6、2、1であろう、0、1、2、3、6、2、3]となり、Z [10] = 2はZ [i] + i = 12 = m + nを満たします。


綺麗な!ありがとう。
ベッコ

3

O(n)の時間/空間の複雑さの秘訣は、各サブシーケンスのハッシュを評価することです。配列を考えてみましょうb

[b1 b2 b3 ... bn]

ホーナーの方法を使用して、各サブシーケンスのすべての可能なハッシュを評価できます。ベース値B(両方の配列のどの値よりも大きい)を選択します。

from b1 to b1 = b1 * B^1
from b1 to b2 = b1 * B^1 + b2 * B^2
from b1 to b3 = b1 * B^1 + b2 * B^2 + b3 * B^3
...
from b1 to bn = b1 * B^1 + b2 * B^2 + b3 * B^3 + ... + bn * B^n

前のシーケンスの結果を使用して、O(1)時間で各シーケンスを評価できるため、すべてのジョブにO(n)がかかることに注意してください。

これで配列ができましたHb = [h(b1), h(b2), ... , h(bn)]。ここで、Hb[i]はからb1までのハッシュbiです。

配列についても同じことを行いますaが、少しトリックがあります。

from an to an   =  (an   * B^1)
from an-1 to an =  (an-1 * B^1) + (an * B^2)
from an-2 to an =  (an-2 * B^1) + (an-1 * B^2) + (an * B^3)
...
from a1 to an   =  (a1   * B^1) + (a2 * B^2)   + (a3 * B^3) + ... + (an * B^n)

あるシーケンスから別のシーケンスにステップするときは、前のシーケンス全体にBを乗算し、Bを乗算した新しい値を追加することに注意してください。次に例を示します。

from an to an =    (an   * B^1)

for the next sequence, multiply the previous by B: (an * B^1) * B = (an * B^2)
now sum with the new value multiplied by B: (an-1 * B^1) + (an * B^2) 
hence:

from an-1 to an =  (an-1 * B^1) + (an * B^2)

これで配列ができましたHa = [h(an), h(an-1), ... , h(a1)]。ここで、Ha[i]はからaiまでのハッシュanです。

これで、nから1までのHa[d] == Hb[d]すべてのd値を比較できます。それらが一致する場合は、答えがあります。


注意:これはハッシュ方式であり、値が大きくなる可能性があり、高速指数化方式とモジュラー算術を使用する必要がある場合があります。これにより、(ほとんど)衝突発生し、この方式は完全に安全ではなくなります。基底Bを本当に大きな素数として選択することをお勧めします(少なくとも配列内の最大値よりも大きい)。また、各ステップで数の制限がオーバーフローする可能性があるためK、各操作で(モジュロ)を使用する必要があるため、注意が必要です(ここでKより大きい素数になる可能性がありますB)。

つまり、2つの異なるシーケンスは同じハッシュを持つ可能性ありますが、2つの等しいシーケンスは常に同じハッシュを持つことになります。


リソースの要件の評価からこの回答を始めていただけますか?
greybeard

2

これは確かに、線形時間O(n)およびO(n)の余分なスペースで実行できます。入力配列は文字列であると想定しますが、これは必須ではありません。

素朴な方法は、でしょう-一致した後、k個の等しい文字-一致しない文字を見つけ、そして戻ってK-1に単位を、インデックスをリセットし、B、およびそこからのマッチング処理を開始します。これは明らかにO(n²)の最悪の場合を表しています。

このバックトラッキングプロセスを回避するために、最後のk-1文字のスキャン中にb [0]文字に遭遇しなかった場合、戻ることは役に立たないことがわかります。我々は場合やったその文字を見つけることであれば、その位置までバックトラックのみ、有用であろうkはサブストリングのサイズの私たちは定期的に繰り返していました。

例えば、我々はどこかに「ABCABC」サブストリングを見れば、およびbは「abcabd」であり、我々はの最後の文字がわかりbが一致していない、我々は成功した試合は、第二「」で開始される可能性がありますことを考慮する必要があります部分文字列で、比較を続行する前に、それに応じてbの現在のインデックスを戻す必要があります。

次に、文字列bに基づいていくつかの前処理を行い、不一致があるかどうかをチェックするのに役立つbの後方参照をログに記録します。たとえば、bが "acaacaacd"の場合、これらの0ベースの後方参照を特定できます(各文字の下に配置)。

index: 0 1 2 3 4 5 6 7 8
b:     a c a a c a a c d
ref:   0 0 0 1 0 0 1 0 5

我々が持っている場合たとえば、「acaacaaca」に等しいの最初の不一致が最後の文字で発生します。上記の情報は、「acaac」が一般的であるため、アルゴリズムにbでインデックス5に戻るように指示します。そしてだけで、現在のインデックスを変更すると、B我々は、現在のインデックスでマッチングを続けることができます。この例では、最後の文字の一致が成功します。

これにより、検索を最適化し、aのインデックスが常に前方に進むようにすることができます。

JavaScriptでのそのアイデアの実装は、その言語の最も基本的な構文のみを使用しています。

function overlapCount(a, b) {
    // Deal with cases where the strings differ in length
    let startA = 0;
    if (a.length > b.length) startA = a.length - b.length;
    let endB = b.length;
    if (a.length < b.length) endB = a.length;
    // Create a back-reference for each index
    //   that should be followed in case of a mismatch.
    //   We only need B to make these references:
    let map = Array(endB);
    let k = 0; // Index that lags behind j
    map[0] = 0;
    for (let j = 1; j < endB; j++) {
        if (b[j] == b[k]) {
            map[j] = map[k]; // skip over the same character (optional optimisation)
        } else {
            map[j] = k;
        }
        while (k > 0 && b[j] != b[k]) k = map[k]; 
        if (b[j] == b[k]) k++;
    }
    // Phase 2: use these references while iterating over A
    k = 0;
    for (let i = startA; i < a.length; i++) {
        while (k > 0 && a[i] != b[k]) k = map[k];
        if (a[i] == b[k]) k++;
    }
    return k;
}

console.log(overlapCount("ababaaaabaabab", "abaababaaz")); // 7

ネストされたwhileループがありますが、これらには合計でnより多くの反復はありません。これは、kの値がwhile体内で厳密に減少し、負になることができないためです。これはk++、そのような減少に十分な余地を与えるために何回も実行されたときにのみ発生します。したがって、全体として、whileボディの実行数は実行数より多くすることはできませんk++。後者は明らかにO(n)です。

完了するために、ここでは上記と同じコードを見つけることができますが、インタラクティブなスニペットでは、独自の文字列を入力して結果をインタラクティブに確認できます。

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