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

C++で解く最短共通スーパーシーケンス(最短共通超部分列)の求め方

2つの文字列 str1 と str2 が与えられたとき、その両方を部分列として含む最短の文字列(最短共通スーパーシーケンス)を求める問題を考えます。答えが複数存在する場合は、そのうちの1つを返せばよいことになっています。

ここで、文字列 S が文字列 T の部分列であるとは、T からいくつかの文字(0個でも可)を任意の位置から削除した結果が S と一致することを指します。

例えば、入力が "acab" と "bac" の場合、出力は "bacab" となります。これは、与えられた2つの文字列がどちらも "bacab" の部分列になっているためです。

アルゴリズムの考え方

この問題は、まず最長共通部分列(LCS: Longest Common Subsequence)を動的計画法(DP)で求め、その LCS を「共有部分」として2つの文字列をマージすることで解けます。手順は以下の通りです。

ステップ1:LCS を求める関数 getLCS() を定義する

  • 結果を格納する空文字列 ret を用意します。
  • n := s1 の長さ、m := s2 の長さとします。
  • (n + 1) × (m + 1) のサイズの2次元配列 dp を定義します。
  • s1 と s2 の先頭に空白文字を連結し、インデックスを1始まりにします。
  • i = 1 から n まで、j = 1 から m まで二重ループを回します。
    • s1[i] と s2[j] が一致する場合:dp[i][j] := 1 + dp[i-1][j-1]
    • 一致しない場合:dp[i][j] := max(dp[i-1][j], dp[i][j-1])

ステップ2:DPテーブルを逆traceしてLCS文字列を復元する

  • i と j がどちらも0でない間、以下を繰り返します。
    • dp[i][j] == dp[i-1][j] なら i を減らしてスキップ。
    • dp[i][j] == dp[i][j-1] なら j を減らしてスキップ。
    • それ以外の場合、ret に s1[i] を追加し、i と j を両方減らします。
  • 最後に ret を反転して返します(後ろから辿っているため)。

ステップ3:LCSを基準に2つの文字列をマージする

  • s3 := getLCS(str1, str2) としてLCSを取得します。
  • ret := 空文字列、i := 0、j := 0、k := 0 と初期化します。
  • k が s3 のサイズ未満の間、以下を繰り返します。
    • i が str1 の範囲内かつ str1[i] != s3[k] なら、ret に str1[i] を追加して i++。
    • j が str2 の範囲内かつ str2[j] != s3[k] なら、ret に str2[j] を追加して j++。
    • それ以外の場合、ret に s3[k] を追加し、i・j・k をすべて1増やします。
  • ループ終了後、str1 と str2 の残りの文字をそれぞれ ret に追加します。
  • ret を返します。

C++での実装例

それでは、実際のコードを見て理解を深めましょう。

#include <bits/stdc++.h>
using namespace std;
class Solution {
    public:
    string shortestCommonSupersequence(string str1, string str2){
        string s3 = getLCS(str1, str2);
        string ret = "";
        int i = 0;
        int j = 0;
        int k = 0;
        while (k < s3.size()) {
            if (i < str1.size() && str1[i] != s3[k]) {
                ret += str1[i];
                i++;
                continue;
            }
            if (j < str2.size() && str2[j] != s3[k]) {
                ret += str2[j];
               j++;
                continue;
            }
            ret += s3[k];
            k++;
            i++;
            j++;
        }
        while (i < str1.size()) {
            ret += str1[i];
            i++;
        }
        while (j < str2.size()) {
            ret += str2[j];
            j++;
        }
        return ret;
    }
    string getLCS(string s1, string s2){
        string ret = "";
        int n = s1.size();
        int m = s2.size();
        vector<vector<int> > dp(n + 1, vector<int>(m + 1));
        int i = n;
        int j = m;
        s1 = " " + s1;
        s2 = " " + s2;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (s1[i] == s2[j]) {
                    dp[i][j] = 1 + dp[i - 1][j - 1];
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        while (i && j) {
            if (dp[i][j] == dp[i - 1][j]) {
                i--;
                continue;
            }
            if (dp[i][j] == dp[i][j - 1]) {
                j--;
                continue;
            }
            ret += s1[i];
            i--;
            j--;
        }
        reverse(ret.begin(), ret.end());
        return ret;
    }
};
main(){
    Solution ob;
    cout << (ob.shortestCommonSupersequence("acab", "bac"));
}

入力

"acab", "bac"

出力

bacab

計算量について

LCSを求めるDPテーブルの構築に O(n × m)、バックトラックにも O(n + m) しかかからないため、全体の計算量は O(n × m)、必要なメモリも DP テーブル分の O(n × m) となります。この手法は LeetCode の「Shortest Common Supersequence」などの典型的な動的計画法の練習問題として知られており、LCSへの理解を深める良い題材です。

  1. C++で解く「ジャンプゲームV」:メモ化再帰による最大訪問インデックス数の求め方

    問題の概要整数型の配列 arr と整数 d が与えられます。1ステップごとに、インデックス i から次の場所へジャンプできます。右方向: i + x(ただし i + x < n、かつ x は 1 以上 d 以下)左方向: i - x(ただし i - x >= 0、かつ x は 1 以上 d 以下)ここで n は配列のサイズです。さらに重要な制約として、インデックス i から j へジャンプできるのは、arr[i] > arr[j] であり、かつ i と j の間にあるすべてのインデックス k に対して arr[i] > arr[k] を満たす場合のみです。つまり、より低

  2. C++で約数がちょうど4個の整数の約数の総和を求める方法

    整数配列 nums が与えられたとき、その中から「約数がちょうど4個」である整数を見つけ出し、それらの約数の総和を計算する問題を考えてみましょう。もし該当する整数が配列内に1つも存在しない場合は、0 を返します。例えば、入力が [21, 4, 7] の場合、出力は 32 になります。これは次のような理由によるものです。21 の約数は 1, 3, 7, 21 の4つ → 条件を満たす4 の約数は 1, 2, 4 の3つ → 条件を満たさない7 の約数は 1, 7 の2つ → 条件を満たさないしたがって、答えは条件を満たす 21 の約数の総和である 32 となります。解法のアプローチこの問題を解く