web-dev-qa-db-ja.com

与えられた2つの文字列のすべての共通部分文字列を見つける

私は見つけるために問題文に出くわしました 指定された2つのサブストリング間のすべての共通サブストリング どのような場合でも、最も長い部分文字列を印刷する必要があります。問題のステートメントは次のとおりです。

与えられた2つの文字列間の共通部分文字列を見つけるプログラムを作成します。ただし、より長い共通部分文字列に含まれる部分文字列は含めないでください。

たとえば、入力文字列eatsleepnightxyzおよびeatsleepabcxyzを指定すると、結果は次のようになります。

  • eatsleepeatsleepnightxyzeatsleepabcxyz
  • xyzeatsleepnightxyzeatsleepabcxyz
  • aeatsleepnightxyzeatsleepabcxyz
  • teatsleepnightxyzeatsleepabcxyz

ただし、結果セットはnot include e from eatsleepnightxyzeatsleepabcxyz。これは、両方のesが上記のeatsleepに既に含まれているためです。 eaeatatsなども含めないでください。これらもすべてeatsleepでカバーされています。

これでは、contains、indexOf、StringTokenizer、split、replaceなどのStringユーティリティメソッドを使用する必要はありません。

私のアルゴリズムは次のとおりです。私はブルートフォースから始めており、基本的な理解が向上したら、より最適化されたソリューションに切り替えます。

 For String S1:
     Find all the substrings of S1 of all the lengths
     While doing so: Check if it is also a substring of 
     S2.

私のアプローチの時間の複雑さを理解しようとします。

与えられた2つの文字列をn1-Stringとn2-Stringとする

  1. S1の部分文字列の数は、明らかにn1(n1 + 1)/ 2です。
  2. しかし、S1の部分文字列の平均長を見つける必要があります。
  3. それがmだとしましょう。 mを個別に見つけます。
  4. M-Stringがn-Stringの部分文字列であるかどうかを確認する時間計算量はO(n * m)です。
  5. ここで、各m-StringがS2のサブストリングであるかどうかを確認しています。これはn2-Stringです。
  6. これは、上で見たように、O(n2 m)アルゴリズム。
  7. その場合、アルゴリズム全体に必要な時間は
  8. Tn =(S1のサブストリングの数)*(文字比較手順の平均サブストリング長時間)
  9. 特定の計算を実行することで、時間の複雑さはO(n3 m2
  10. さて、私たちの仕事はn1でmを見つけることです。

N1でmを見つけようとします。

Tn =(n)(1)+(n-1)(2)+(n-2)(3)+ ..... +(2)(n-1)+(1)(n)
Tn すべてのサブストリングの長さの合計です。

平均は、この合計を生成された部分文字列の総数で割ったものになります。

これは、単純に総和と除算の問題で、その解は次のとおりですO(n)

したがって...

私のアルゴリズムの実行時間はO(n ^ 5)です。

これを念頭に置いて、次のコードを作成しました。

 package pack.common.substrings;

 import Java.util.ArrayList;
 import Java.util.LinkedHashSet;
 import Java.util.List;
 import Java.util.Set;

 public class FindCommon2 {
    public static final Set<String> commonSubstrings = new      LinkedHashSet<String>();

 public static void main(String[] args) {
    printCommonSubstrings("neerajisgreat", "neerajisnotgreat");
    System.out.println(commonSubstrings);
}

 public static void printCommonSubstrings(String s1, String s2) {
    for (int i = 0; i < s1.length();) {
        List<String> list = new ArrayList<String>();
        for (int j = i; j < s1.length(); j++) {
            String subStr = s1.substring(i, j + 1);
            if (isSubstring(subStr, s2)) {
                list.add(subStr);
            }
        }
        if (!list.isEmpty()) {
            String s = list.get(list.size() - 1);
            commonSubstrings.add(s);
            i += s.length();
        }
    }
 }

 public static boolean isSubstring(String s1, String s2) {
    boolean isSubstring = true;
    int strLen = s2.length();
    int strToCheckLen = s1.length();
    if (strToCheckLen > strLen) {
        isSubstring = false;
    } else {
        for (int i = 0; i <= (strLen - strToCheckLen); i++) {
            int index = i;
            int startingIndex = i;
            for (int j = 0; j < strToCheckLen; j++) {
                if (!(s1.charAt(j) == s2.charAt(index))) {
                    break;
                } else {
                    index++;
                }
            }
            if ((index - startingIndex) < strToCheckLen) {
                isSubstring = false;
            } else {
                isSubstring = true;
                break;
            }
        }
    }
    return isSubstring;
 }
}

私のコードの説明:

 printCommonSubstrings: Finds all the substrings of S1 and 
                        checks if it is also a substring of 
                        S2.
 isSubstring : As the name suggests, it checks if the given string 
               is a substring of the other string.

問題:入力を考える

  S1 = “neerajisgreat”;
  S2 = “neerajisnotgreat”
  S3 = “rajeatneerajisnotgreat”

S1およびS2の場合、出力はneerajisおよびgreatである必要がありますが、S1およびS3の場合、出力はneerajisrajgreateatですが、それでもneerajisおよびgreatを出力として取得しています。これを理解する必要があります。

コードをどのように設計すればよいですか?

23
neerajdorle

総当たり的なアプローチよりも、タスクに適切なアルゴリズムを使用した方が良いでしょう。ウィキペディアでは、 最長共通部分文字列問題suffix-tree および dynamic-programming の2つの一般的なソリューションについて説明しています。

動的計画法の解決にはO(nm)時間とO(nmが必要です) スペース。これはほとんどの単純なJava最長共通部分文字列のWikipedia擬似コードの翻訳です。

public static Set<String> longestCommonSubstrings(String s, String t) {
    int[][] table = new int[s.length()][t.length()];
    int longest = 0;
    Set<String> result = new HashSet<>();

    for (int i = 0; i < s.length(); i++) {
        for (int j = 0; j < t.length(); j++) {
            if (s.charAt(i) != t.charAt(j)) {
                continue;
            }

            table[i][j] = (i == 0 || j == 0) ? 1
                                             : 1 + table[i - 1][j - 1];
            if (table[i][j] > longest) {
                longest = table[i][j];
                result.clear();
            }
            if (table[i][j] == longest) {
                result.add(s.substring(i - longest + 1, i + 1));
            }
        }
    }
    return result;
}

ここで、最長のものだけでなく、すべての共通部分文字列が必要になります。このアルゴリズムを拡張して、短い結果を含めることができます。例の入力eatsleepnightxyzおよびeatsleepabcxyzのテーブルを調べてみましょう。

  e a t s l e e p a b c x y z
e 1 0 0 0 0 1 1 0 0 0 0 0 0 0
a 0 2 0 0 0 0 0 0 1 0 0 0 0 0
t 0 0 3 0 0 0 0 0 0 0 0 0 0 0
s 0 0 0 4 0 0 0 0 0 0 0 0 0 0
l 0 0 0 0 5 0 0 0 0 0 0 0 0 0
e 1 0 0 0 0 6 1 0 0 0 0 0 0 0
e 1 0 0 0 0 1 7 0 0 0 0 0 0 0
p 0 0 0 0 0 0 0 8 0 0 0 0 0 0
n 0 0 0 0 0 0 0 0 0 0 0 0 0 0
i 0 0 0 0 0 0 0 0 0 0 0 0 0 0
g 0 0 0 0 0 0 0 0 0 0 0 0 0 0
h 0 0 0 0 0 0 0 0 0 0 0 0 0 0
t 0 0 1 0 0 0 0 0 0 0 0 0 0 0
x 0 0 0 0 0 0 0 0 0 0 0 1 0 0
y 0 0 0 0 0 0 0 0 0 0 0 0 2 0
z 0 0 0 0 0 0 0 0 0 0 0 0 0 3
  • eatsleepの結果は明らかです。それは、左上の12345678対角線です。
  • xyzの結果は、右下の123対角線です。
  • aの結果は、上部(2行9列)の近くの1で示されます。
  • tの結果は、左下にある1で示されます。

1および6の左側、上部、および隣にある他の7はどうですか?それらは12345678対角線で形成された長方形内に表示されるため、カウントされません。つまり、すでにeatsleepで覆われています。

テーブルを構築する以外に何もせずに1パスすることをお勧めします。次に、2番目のパスを作成し、右下から逆方向に繰り返して、結果セットを収集します。

17
200_success

通常、このタイプの部分文字列照合は、 Trie (試行された発音と呼ばれる)と呼ばれる別のデータ構造を使用して行われます。この問題に最適な特定のバリアントは、 サフィックスツリー です。最初のステップは、入力を取得してサフィックスツリーを構築することです。次に、接尾辞ツリーを使用して最長の共通部分文字列を決定する必要があります。これは良い練習です。

5
ktbiz