プログラミング原人の進化ログ

プログラミング原人の進化論

オレ プログラミング ベンキョウ スル。マナンダ コト カク。

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

動的計画法の例題です。最長共通部分列を実装します。

目標

最長共通部分列

2つの列X、Yがあるとします。XとYの共通部分列のうち、最長のものを最長共通部分列Longest Common Subsequence, LCS)と言います。列は数列など色々なものが考えられますが、ここでは文字列を取り上げます。X = {a, b, c, d}、Y = {a, c, d, e} のとき、最長共通部分列は {a, c, d}となります。ここで注意しておきたいのが、部分列は連続である必要はなく、元の列の要素はとびとびになっていても良いということです。

最長共通部分列問題では、与えられた2つの列の最長共通部分列の長さを求めます。

アルゴリズム

 X = \{x_1, x_2, \dots , x_m\}, Y = \{y_1, y_2, \dots , y_n \}に対して、部分列 X_i = \{x_1, x_2, \dots , x_i \}, Y_j = \{y_1, y_2, \dots , y_j \}を考えます。 X_i, Y_jのLCS(LCS(i, j)と書くことにします)は、それまでに計算した値を使って求めることができます。次の2つに場合分けをします。

  •  x_i = y_j の場合: LCS(i-1, j-1)に x_i = y_jを加えたものが LCS(i, j) となります。

  •  x_i \neq y_j の場合: LCS(i, j-1)とLCS(i-1, j)のうち、より長いほうが LCS(i, j) です。

従って、次の漸化式が成り立ちます。


| LCS(i, j) | = \left \{
\begin{array}{}
0 && (i = 0  \; or \;  j = 0) \\
| LCS(i-1, j-1) | + 1 && (i, j > 0 \; and \; x_i = y_j) \\
max( | LCS(i, j-1) | , | LCS(i-1, j) | ) && (i, j > 0 \;  and \; x_i \neq y_j) \\
\end{array}
\right.

計算量は O(nm)となります。LCS(m, n)がX、Yの最長共通部分列です。

実装

次の問題を解いてみます。

最長共通部分列 | アルゴリズムとデータ構造 | Aizu Online Judge

解答例:

#include<iostream>
#include<string>
using namespace std;

#define N 1000

int lcs(string x, string y) {
  int i, j, m, n, lcs[N+1][N+1];
  
  m = x.length();
  n = y.length();
  // indexずれを補正
  x = ' ' + x;
  y = ' ' + y;
  
  // 初期値
  for (i = 0; i < m + 1; i++) lcs[i][0] = 0;
  for (j = 0; j < n + 1; j++) lcs[0][j] = 0;
  // 漸化式
  for (i = 1; i <= m; i++) {
    for (j = 1; j <= n; j++) {
      if (x[i] == y[j]) lcs[i][j] = lcs[i-1][j-1] + 1;
      else lcs[i][j] = max(lcs[i-1][j], lcs[i][j-1]);
    }
  }

  return lcs[m][n];
}

int main() {
  int i, q, lcs_arr[N];
  string x, y;

  cin >> q;

  for (i = 0; i < q; i++) {
    cin >> x;
    cin >> y;
    lcs_arr[i] = lcs(x, y);
  }    

  for (i = 0; i < q; i++) cout << lcs_arr[i] << endl;
    
  return 0;
}

注意すべき点は、文字列の各文字にアクセスするときに添字のずれが生じないように、頭に空白文字を加えておくことです。

参考

渡部有隆. プログラミングコンテスト攻略のためのアルゴリズムとデータ構造. Ozy, 秋葉拓哉協力. 株式会社マイナビ. 2015.