最長共通部分列: Python, C++ 例:

⚡ スマートサマリー

最長共通部分列は、連続した文字を必要とせずに、2つの文字列間で共有される最長の順序付き要素パターンを特定します。この動的計画法の古典的な手法は、配列を多項式時間で効率的に比較することで、diffユーティリティ、DNAアライメント、バージョン管理などの基盤となっています。

  • 📘 コアコンセプト: 最長共通部分列は、入力文字列の両方に現れる文字の最長の順序付きセットを、元の相対的な順序を維持したまま返します。
  • 🐢 素朴なアプローチ: 総当たり攻撃では、最初の文字列のすべての部分列を列挙し、それを2番目の文字列と比較します。実行時間は指数関数的なO(n·2^m)です。
  • 🔁 再帰的方法: 再帰ルールは最後の文字に一致するか、より小さな部分文字列に対して再帰しますが、重複を再計算しますping 部分問題を繰り返し。
  • 🧮 動的計画法: 2次元の動的計画法テーブルは部分問題の結果をキャッシュし、O(m·n)の補助空間でO(m·n)のクリーンな解を生成します。
  • 🐍 言語範囲: 完全 Python (NAIST) と C++ 実装例では、実用的な用途のために、再帰的なベースラインとメモ化された動的計画法テーブルの両方を示しています。
  • 🌐 実際の応用例: 最長共通部分配列は、DNAおよびタンパク質における差分ツール、盗作チェッカー、スペル修正ツール、およびバイオインフォマティクスの配列アライメントの基盤となる技術です。

最長共通部分列

最長共通部分列とは何ですか?

最長共通部分列(LCS)とは、2つの文字列、パターン、またはオブジェクトのシーケンスが与えられたときに、それらの文字列またはシーケンスの中から、両方の文字列またはパターンに同じ順序で存在する要素の最長部分列を見つける問題です。

例:

例えば、2つの文字列が与えられているとします。以下のように仮定しましょう。

パターン_1 = 「RGBGARGA」
パターン_2 = 「BGRARG」

  • パターン1から、「RGB」、「RGGA」、「RGAR」などのシーケンスを生成できます。シーケンスを作成するには、文字列内の各文字の相対位置を維持する必要があります。
  • パターン2からは、「BGR」、「BRAG」、「RARG」のようなシーケンスを生成できます。元の文字列の相対位置を維持する限り、シーケンスを生成できます。

相対位置という用語は順序を意味します。

例えば、「BRG」は有効なシーケンスです。なぜなら、元の文字列パターン2では「B」が最初に現れ、「R」が次に現れ、「G」が次に現れるからです。しかし、「RBRG」というシーケンスは無効です。なぜなら、元の文字列(パターン2)では「B」が最初に現れるからです。

最長共通部分列の例となる文字列

指定された XNUMX つのシーケンスまたは配列から最長共通部分シーケンスを見つけるには XNUMX つのオプションがあります。

  • 素朴な方法
  • 動的プログラミング ソリューション: 最長共通部分列は LCS とも呼ばれます。

単純な解法は時間計算量が大きく、最適解ではありません。動的計画法(DP)を用いることで、この計算量の問題を克服できます。

素朴な方法

ナイーブ法は、時間計算量やその他の最適化要因に関係なく、問題に対する単純なアプローチです。ほとんどの場合、「総当たり」、複数のループ、および再帰呼び出しで構成されます。総当たりとは、与えられた問題に対して考えられるすべてのパターンを網羅的に調べることを意味します。

例:

上記のパターン 1 とパターン 2 の例から、パターン 1 の長さが m、パターン 2 の長さが n であると仮定します。 考えられるすべてのケースをチェックするには、パターン 1 の考えられるすべてのサブシーケンスをパターン 2 で評価する必要があります。

ここに「ABCD」という4文字の文字列があります。例えば、「ABCD」からシーケンスを作成する必要があります。文字を選択するかしないかのどちらかです。つまり、各文字に対して2つの選択肢があります。

  • 文字がサブシーケンスに追加されます。
  • 文字はサブシーケンスには追加されません。

ここで、画像は文字列「ABCD」から作成できるすべてのシーケンスを示しています。

ABCDの単純な方法のシーケンス

1 文字のシーケンス:

ナイーブメソッド 単一文字シーケンス

2 文字のシーケンス:

素朴な方法 2文字シーケンス

3 文字のシーケンス:

素朴な方法 3文字シーケンス

上記の図から、14個のシーケンスが確認できます。文字を一切使用しない場合、つまり空の文字列の場合、シーケンスの総数は15個になります。さらに、「ABCD」という文字列自体もシーケンスです。したがって、シーケンスの総数は16個になります。

したがって、「ABCD」から 2^4 または 16 個の部分列を生成することが可能です。次に、長さが m 合計で 2^m 個の部分列を持つことになります。

各部分シーケンスについて、パターン2全体をチェックする必要があります。これにはO(n)の時間がかかります。O(n)とは、実行にかかる時間を計算する複雑度関数を意味します。

したがって、総時間計算量は O(n*2^m)。 上記の例では、m=8、n=5 となります。

Naive Method の手順は次のとおりです。

ステップ1) パターン1からシーケンスを取得します。
ステップ2) ステップ1のシーケンスをパターン2と照合してください。
ステップ3) 一致する場合は、サブシーケンスを保存します。
ステップ4) パターン1にまだシーケンスが残っている場合は、ステップ1に戻ります。
ステップ5) 最長のサブシーケンスを出力します。

最適な下部構造

最適部分構造という用語は、部分問題を解くことによって最適解を見つけることができることを意味します。例えば、上記の例では、パターン1とパターン2があります。

ステップ1) それぞれのパターンから最初の2文字を取り出します。

ステップ2) 各パターンから XNUMX 番目から XNUMX 番目の文字を取り出します。

ステップ3) 残りの文字も同様に続けます。

LCS問題の再帰構造

LCS問題の再帰構造

部分文字列(元の文字列から生成された文字列)の最長共通部分文字列(LCS)を求めます。そして、部分文字列のLCSの長さを記録します。

さて、ここにもう XNUMX つの興味深いプロパティがあります。 オーバーラップping サブ問題問題には重複があると言われますping 問題文を小さなサブ問題に分割し、プログラム内で複数回使用できる場合は、サブ問題と呼びます。

以下の図は、再帰アルゴリズムが同じパラメーターを持つ関数を複数回呼び出したことを示しています。

最適なサブストラクチャの重なりping 部分問題

例えば、再帰ツリーを見てください。濃い色の枠内には、重なりが見られます。ping 部分問題。(「RG」、「RA」)、(「RG」、「R」)などが複数回呼び出されます。

これを最適化するには、次のようなアプローチがあります。 動的計画法 (DP)。

最長共通部分列の再帰的方法

上記のグラフは再帰的なメソッドを示しています。各再帰関数には、再帰を中断したり、スタックから戻り始めるための基本ケースがあります。

この実装では、基本ケースを使用します。 アルゴリズム は次のようになります。

  • 最後の要素より前のすべての要素に一致するものがあれば、長さを1つ増やして返す。
  • 関数に2つのパターンを渡し、戻り値の最大値を取得します。
  • 0 つのパターンの長さがゼロの場合、比較するサブシーケンスはありません。 この場合は XNUMX を返します。 これは再帰の基本的なケースです。

ニックネーム Code:

def lcs:
    input: pattern_1, pattern_2, len_1, len_2
    if len_1 or len_2 is zero:
        return 0
    if pattern_1[len_1 - 1] equals pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

での実装 C++

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int lcs(string pattern_1, string pattern_2, int len_1, int len_2) {
  if (len_1 == 0 || len_2 == 0)
    return 0;
  if (pattern_1[len_1 - 1] == pattern_2[len_2 - 1]) {
    return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1);
  } else {
    return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1));
  }
}
int main() {
  string pattern_1, pattern_2;
  pattern_1 = "RGBGARGA";
  pattern_2 = "BGRARG";
  cout<<"Length of LCS is: "<<lcs(pattern_1, pattern_2, pattern_1.size(), pattern_2.size())<<endl;
}

出力:

Length of LCS is: 5

での実装 Python

def lcs(pattern_1, pattern_2, len_1, len_2):
    if len_1 == 0 or len_2 == 0:
        return 0
    if pattern_1[len_1 - 1] == pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS is: ", lcs(pattern_1, pattern_2, len(pattern_1), len(pattern_2)))

出力:

Length of LCS is:  5

最長共通部分列(LCS)の動的計画法

動的計画法とは、単純な再帰的手法を最適化することです。例えば、再帰的または単純なアプローチのグラフを見ると、複数の同一の関数呼び出しが存在することがわかります。動的計画法では、すべての計算結果を配列に記録し、必要に応じて再利用します。

m×nの次元を持つ2次元配列を使用します。ここで、mとnはパターン1とパターン2の長さです。 2D配列リストデータ構造は Python またはベクトル/配列データ構造 C++.

ニックネーム Code DPを使用したLCSの場合:

LCS(pattern_1, pattern_2):
    m = length of pattern_1 + 1
    n = length of pattern_2 + 1
    dp[n][m]
    for i in range 0 to n + 1:
        for j in range 0 to m + 1:
            if i or j equals to 0:
                dp[i][j] = 0
            else if pattern_1[i] == pattern_2[j]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[n][m]

以下に、動的計画法のアプローチにおける2次元配列データ構造として使用される表LCSを示します。

LCS 2Dテーブルの動的計画法

ここで使用したロジックについて説明します。手順は次のとおりです。

ステップ1) iまたはjがゼロの場合、与えられた2つの文字列から空の文字列を取り出し、共通の部分列を見つけようとします。しかし、取り出した部分文字列は空なので、部分列の長さは0になります。

ステップ2) 2 つの文字が一致する場合、前の行の (i-1,j-1) インデックスに存在する、以前に計算された LCS をインクリメントすることにより、(i,j) インデックスに値を割り当てます。

ステップ3) 一致しない場合は、隣接する2つのインデックスの最小共通部分列(LCS)の最大値を取得します。このようにして、2次元配列内のすべての値を埋める必要があります。

ステップ4) 最後に、2D 配列の最後のセルの値を返します。

基本的に、2次元配列のすべての値は共通部分列の長さを表しています。その中で、最後のセルには最長の共通部分列の長さが格納されています。

での実装 C++

#include<iostream>
using namespace std;
int lcs(string pattern_1, string pattern_2) {
  int m = pattern_1.size();
  int n = pattern_2.size();
  // dp will store solutions as the iteration goes on
  int dp[n + 1][m + 1];
  for (int i = 0; i < n + 1; i++) {
    for (int j = 0; j < m + 1; j++) {
      if (i == 0 || j == 0) {
        dp[i][j] = 0;
      } else if (pattern_2[i - 1] == pattern_1[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  return dp[n][m];
}
int main() {
  string pattern_1 = "RGBGARGA";
  string pattern_2 = "BGRARG";
  cout<<"Length of LCS: "<<lcs(pattern_1, pattern_2)<<endl;
}

出力:

Length of LCS: 5

での実装 Python

def lcs(pattern_1, pattern_2):
    m = len(pattern_1)
    n = len(pattern_2)
    # dp will store solutions as the iteration goes on
    dp = [[None] * (n + 1) for item in range(m + 1)]
    for i in range(m + 1):
        for j in range(n + 1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            elif pattern_1[i - 1] == pattern_2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS: ", lcs(pattern_1, pattern_2))

出力:

Length of LCS: 5

したがって、両方の文字列には長さ 5 の最長共通部分列があります。

要するに、DP法では各タスクを一度だけ計算します。再帰法では、重複が発生する可能性があります。ping 部分問題。

この動的プログラミング アルゴリズムでは、2D マトリックスを使用しています。 XNUMX つの文字列が指定されます (両方とも長さが n であると仮定します)。 この場合、配列に必要なスペースは nx n です。 文字列が十分に大きい場合は、メモリ最適化バージョンの DP ソリューションが必要になります。

コードに組み込まれた簡略化されたロジックは次のとおりです。

  • 2D 配列 DP[m][n] を宣言します。
  • DP 配列の最初の行と最初の列を 0 で埋めます。
  • 反復のために i と j を考えます。
  • pattern1[i]がpattern2[j]と等しい場合、DP[i][j] = DP[i-1][j-1] + 1に更新します。
  • pattern1[i]がpattern2[j]と等しくない場合、DP[i][j]はDP[i-1][j]とDP[i][j-1]の間の最大値になります。
  • i と j が m と n に達するまで続けます。
  • 最後の要素である DP[m-1][n-1] には長さが含まれます。

ここでは、配列のインデックスが0から始まるため、DP[m-1][n-1]と表記されます。

よくあるご質問

機械学習パイプラインでは、テキスト分類、シーケンス間評価、コード盗用検出において、LCSを類似性特徴量として使用します。また、生成されたテキストを参照出力と比較してスコアリングするBLEUやROUGEスタイルの指標の基盤にもなっています。

はい。GitHub CopilotやGPTのようなAIコーディングアシスタントは、LCSの再帰的および動的プログラミングバージョンを生成できます。 Python, C++または Javaまた、要求に応じてメモ化を追加したり、実際のサブシーケンスを出力したり、コードを反復形式に変換したりすることもできます。

部分文字列は連続している必要がありますが、部分列は順序が保持されていればよいです。「ABCDE」の場合、「ACD」は有効な部分列ですが部分文字列ではありません。一方、「BCD」は部分文字列でもあり、部分列でもあります。

動的計画法を用いたバージョンは、2つの入力シーケンスの長さをmとnとした場合、O(m·n)の時間と空間で実行されます。単純な再帰バージョンは、最悪の場合、指数関数的なO(2^(m+n))の時間で実行されます。

LCSは、ファイル差分ユーティリティ、Gitマージ、バイオインフォマティクスにおけるDNAおよびタンパク質配列のアライメント、盗作検出、スペルチェッカー、およびレコードの共有順序を維持する必要のあるデータ同期ツールなどに利用されています。

標準的な表ではO(m·n)の空間が必要です。長さだけが必要な場合は、2行移動最適化によって空間をO(min(m, n))まで削減できますが、実際のサブシーケンスを再構築するには依然として完全な表が必要です。

はい、純粋な再帰は短い文字列には有効ですが、同じサブ問題を何度も再計算するため、20~25文字を超えると非実用的になります。メモ化またはDPテーブルを追加すると、 tracテーブルのパフォーマンス。

はい。動的計画法(DP)の考え方は、k次元テーブルを用いてk個の配列に拡張でき、時間と空間はO(n^k)となります。この手法は、バイオインフォマティクスにおける複数ファイル差分ツールや多重配列アライメントに用いられています。