Cプログラミング
 Computer >> コンピューター >  >> プログラミング >> Cプログラミング

C++プログラムにおけるLCS(最長共通部分列)の空間計算量最適化手法

LCS問題とは

本記事では、LCS(最長共通部分列:Longest Common Subsequence)問題を解くための、空間計算量を最適化したアプローチを紹介します。LCSとは、2つの文字列に共通して現れる部分列のうち、最も長いものを指します。例えば、「BHHUBC」と「HYUYBZC」という2つの文字列の場合、最長共通部分列の長さは4になります。

従来の動的計画法(DP)によるアプローチでもLCSは求められますが、この方法では大きなメモリ領域が必要になります。具体的には、1つ目の文字列の文字数をm、2つ目の文字列の文字数をnとすると、m×nのサイズを持つ2次元テーブルを用意しなければなりません。

O(n)の補助記憶域で実現する工夫

ここで紹介するのは、補助記憶域をO(n)に抑える実装方法です。従来のアプローチをよく観察すると、各反復処理において参照しているのは直前の行のデータだけであることが分かります。つまり、それ以前の行のデータはすべて不要なのです。

そこで、サイズが2×(n+1)、すなわち2行分のテーブルを用意するだけで十分です。行番号を0と1で交互に切り替えながら計算を進めることで、メモリ使用量を大幅に削減できます。以下のアルゴリズムで具体的な流れを確認してみましょう。

アルゴリズム

lcs_problem(X, Y) −

begin
   m := 文字列Xの長さ
   n := 文字列Yの長さ
   サイズ L[2, n+1] のテーブルを定義
   index はテーブルLの0行目または1行目を指す
   i を 1 から m まで繰り返す:
      index := index AND 1
      j を 0 から n まで繰り返す:
         if i = 0 または j = 0 の場合:
            L[index, j] := 0
         else if X[i - 1] = Y[j - 1] の場合:
            L[index, j] := L[1 – index, j - 1] + 1
         else:
            L[index, j] := L[1 – index, j] と L[index, j-1] の最大値
         end if
      繰り返し終了
   繰り返し終了
   return L[index, n]
end

C++による実装例

#include <iostream>
using namespace std;
int lcsOptimized(string &X, string &Y) {
   int m = X.length(), n = Y.length();
   int L[2][n + 1];
   bool index;
   for (int i = 0; i <= m; i++) {
      index = i & 1;
      for (int j = 0; j <= n; j++) {
         if (i == 0 || j == 0)
            L[index][j] = 0;
         else if (X[i-1] == Y[j-1])
            L[index][j] = L[1 - index][j - 1] + 1;
         else
            L[index][j] = max(L[1 - index][j], L[index][j - 1]);
      }
   }
   return L[index][n];
}
int main() {
   string X = "BHHUBC";
   string Y = "HYUYBZC";
   cout << "Length of LCS is :" << lcsOptimized(X, Y);
}

実行結果

Length of LCS is :4

まとめ

このように、2行分のテーブルだけでLCSの長さを正しく求めることができました。ビット演算 i & 1 を使って行インデックスを交互に切り替えるのがポイントです。文字列が非常に長い場合でも、メモリ使用量を従来のm×nからわずか2×(n+1)まで抑えられるため、大規模な入力を扱う際に有効な実用的な最適化手法といえます。

  1. 配列の左回転をO(n)時間・O(1)空間で実現するC++プログラムの書き方

    本記事では、サイズnの整数配列と複数の回転位置kが与えられたとき、指定されたインデックスkから配列を左方向へ回転させた結果を出力する方法を、時間計算量O(n)・空間計算量O(1)の制約のもとで解説します。 配列の左回転とは、各要素を左へk個分ずらし、はみ出した要素を右端に折り返して配置する操作です。例えば、配列 {1, 2, 3, 4, 5} を1回左に回転すると {2, 3, 4, 5, 1} になります。 この手法の鍵となるのは剰余演算(%)です。回転後の配列を新たに作成することなく、インデックス計算だけで結果を直接出力できるため、追加のメモリ領域を一切必要としません。 入力例と出力例

  2. Cプログラムで追加領域やリストの変更なしに連結リストを逆順に表示する方法

    この課題は、連結リスト(リンクリスト)のノードを末尾から先頭に向かって表示するというものです。ただし、追加のメモリ領域を使用しないことが条件です。つまり、再帰呼び出しやスタックのような補助変数・データ構造を使わず、先頭ノードを指すヘッドポインタだけを利用して実現する必要があります。例入力:10 21 33 42 89 出力:89 42 33 21 10連結リストを逆順に表示する方法はいくつか考えられます。例えば、以下のようなアプローチが挙げられます。再帰的な手法:関数呼び出しのスタックを使用するため、O(n) の追加領域が必要になります。リスト自体を反転させる手法:元の連結リストに変更を加えて