web-dev-qa-db-ja.com

特定の部分文字列を含む文字列の数

バイナリ文字列の数(たとえば、 "10010"にバイナリのサブ文字列( "00"など)が含まれていることに関する多くの質問(および回答)を見てきました。これを一般化する方法があるかどうか知りたいです。

長さnと文字列s(任意の文字A...Zが含まれる場合があります)が与えられた場合、いくつの異なる長さの文字列n、部分文字列sを少なくとも1回含むものはありますか?

これらの種類の質問は、多くの場合、組み合わせ論で解決できますが、動的プログラミングソリューションを見つけたいと思っています(したがって、この質問をmathexchangeの代わりにここに投稿しました)。

個人的に、私はここで冷たい蒸気で走っています。もちろん、いくつかの組み合わせ方法を試しました。

n = 5
s = CAR
? = unknown character

all possibilities:

CAR??
?CAR?
??CAR

これは基本的に26^0*26^2 + 26^1*26^1 + 26^2*26^0 = 3 * 26^2 = 2028の可能性に要約されます。ただし、次の場合を考えてください。

n = 7
s = CAR
? = unknown character

all possibilities:

CAR????
?CAR???
??CAR??
???CAR?
????CAR

どうしたの?まあ、重複した結果を生み出す可能性のある3つのポジションがあります:

CARCAR?
CAR?CAR
?CARCAR

今、可能な文字列の数は

(2 * 26^7) + (2 * 26^1 * 26^6) + (2 * 26^2 * 26^5) + (2 * 26^3 * 26^4) - (3 * 26)

これを一般化することはできません。

誰かの興味を引きましたか?どんな助けでもありがたいです。

6
Meri Craig

謝辞 @ Hirle's solution 、私はまだ自分の投稿をすると思っていました。

私は少し違う方向からこれに取り組みました。考えた結果、最も単純なプログラムが鍵となりました。
決定論的有限オートマトン。これは、後で使用できる隣接行列を計算できるため、文字列の出現を検索するdfaで解決できます。ささいなことをスキップすることに決め、Knuth–Morris–Prattアルゴリズムの決定論的バージョン(元の非決定論的で目的にかなわないもの)のコードをネットからコピーしました。

以下のコードは here に由来し、少し変更されています。

public static int[][] dfa;

// create the DFA from a String
public static void kmp(String pat) {

    // build DFA from pattern
    int m = pat.length();
    dfa = new int[26][m]; 
    dfa[pat.charAt(0) - 'A'][0] = 1; 
    for (int X = 0, j = 1; j < m; j++) {
        for (int c = 0; c < 26; c++) 
            dfa[c][j] = dfa[c][X];          // Copy mismatch cases. 

        dfa[pat.charAt(j) - 'A'][j] = j+1;  // Set match case. 
        X = dfa[pat.charAt(j) - 'A'][X];    // Update restart state. 
    } 
}

パターンABCの場合、これにより、次の2Dマトリックスまたはdfaが生成されます。

[1, 1, 1]
[0, 2, 0]
[0, 0, 3]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]

ここで、たとえば、最初の行は、表示される文字が「A」の場合、現在のstateに応じて、行で示される状態に移動することを意味します。

現在の状態が初期状態(状態0)であるとしましょう。ここで文字「A」を見た場合、次の状態は、最初の行の最初の要素である1によって通知されます。つまり、状態は1です。もう一度、文字「A」を見たとしましょう。最初の行の2番目の要素も1であるため、状態は1のままです。さて、さて、文字「B」を見たとしましょう。 B行(2行目)の2番目の要素が2であるため、状態は2に進みます。

基本的に、ABCという文字が連続して表示された場合にのみ、状態は3になります。行D-Zはすべてゼロです。A-C以外の文字を見た場合、最初(この場合はA)から検索を開始する必要があるためです。

一般化するために、マトリックス要素[0][1]は、表示された文字がAであり、状態1であった場合、次に進むべき状態を示します。同様に、マトリックス要素[6][6]は、表示された文字はGであり、状態6でした(上記のマトリックスでは、長さ3の文字列で作成したため、もちろん状態6はありません)。

とにかく、これで隣接行列を作成するために必要なすべてが揃いました。さらに苦労せずに、これがそのための私のコードです:

String s = "ABC"; // The string we built our dfa with, e.g the string we are "searching"

paths = new long[s.length() + 1][s.length() + 1];
for (int k = 0; k < 26; k++)
    for (int i = 0; i < dfa[0].length; i++)
        paths[i][dfa[k][i]]++;

paths[paths.length - 1][paths.length - 1] = 26;

したがって、文字列「ABC」で作成されたdfaから隣接行列を作成している場合、隣接行列は次のようになります。

[25, 1, 0, 0]
[24, 1, 1, 0]
[24, 1, 0, 1]
[0 , 0, 0, 26]

ここでpaths[i][j]は、j州からi州に到達する方法がいくつあるかを示します。たとえば、状態0から他の状態に到達する方法は1つしかないため、状態0から状態0に到達する方法は25通りあります。また、最後の状態になるのに26の方法があります。最新の状況。

これまでに「AA」という文字列ですべてを構築した場合、隣接行列は次のようになります。

[25, 1, 0]
[25, 0, 1]
[0, 0, 26]

ならどうしよう?私たちは、クールなものを教えてくれる、非常に強力な隣接行列を持っています、それをどうするか?グラフ理論が登場します。

参照 Wolfram Alpha

kの隣接行列のGth乗の(u,v)th要素は、頂点ku間の長さvのパスの数を示します。

つまり、隣接行列を3乗すると、要素paths[0][k]は、状態0から状態kまでの長さ3のパスがいくつあるかを示します。

このためにもう少し準備します。文字列Sがあり、その文字列でdfaの最後の状態が得られる場合、S[〜# 〜] ok [〜#〜]。したがって、元の問題に対する答えを得るために知りたいのは、長さnの大きな文字列に含まれるOK文字列の数です。

これが、隣接行列をnth乗に累乗する理由です。要素paths[0][k](ここで、kは検索する文字列の長さ)によって、長さnの長い文字列に含まれるOK文字列の数がわかります。これがたまたま問題の答えです。

マトリックスをnth乗に累乗するコード:

public static long[][] matrixToPower(long[][] a, int p) {
    long[][] b = a;
    for (int n = 1; n < p; n++)
        a = matrixMultiplication(a, b);
    return a;
}

public static long[][] matrixMultiplication(long[][] m1, long[][] m2) {
    int len = m1.length;
    long[][] mResult = new long[len][len];
    for(int i = 0; i < len; i++) {
        for(int j = 0; j < len; j++) {
            for(int k = 0; k < len; k++) {
                mResult[i][j] += (m1[i][k] * m2[k][j]) % M;
                mResult[i][j] %= M;
            }
        }
    }
    return mResult;
}

大きな入力に対して信頼できる回答を得るために、サイズ10^9 + 7のモジュロを使用しています。

完全なコード:

import Java.util.*;

public class Program {

    public static final int M = 1000000007;

    public static int[][] dfa;
    public static long[][] paths;

    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        String s = sc.next();

        kmp(s); // generate dfa

        paths = new long[s.length() + 1][s.length() + 1];
        for (int k = 0; k < 26; k++)
            for (int i = 0; i < dfa[0].length; i++)
                paths[i][dfa[k][i]]++;            
        paths[paths.length - 1][paths.length - 1] = 26;

        paths = matrixToPower(paths, n);

        System.out.println(paths[0][s.length()]);
    }

    public static long[][] matrixToPower(long[][] a, int p) {
        long[][] b = a;
        for (int n = 1; n < p; n++)
            a = matrixMultiplication(a, b);
        return a;
    }

    public static long[][] matrixMultiplication(long[][] m1, long[][] m2) {
        int len = m1.length;
        long[][] mResult = new long[len][len];
        for(int i = 0; i < len; i++) {
            for(int j = 0; j < len; j++) {
                for(int k = 0; k < len; k++) {
                    mResult[i][j] += (m1[i][k] * m2[k][j]) % M;
                    mResult[i][j] %= M;
                }
            }
        }
        return mResult;
    }

    // create the DFA from a String
    public static void kmp(String pat) {
        // build DFA from pattern
        int m = pat.length();
        dfa = new int[26][m]; 
        dfa[pat.charAt(0) - 'A'][0] = 1; 
        for (int X = 0, j = 1; j < m; j++) {
            for (int c = 0; c < 26; c++) 
                dfa[c][j] = dfa[c][X];         // Copy mismatch cases. 
            dfa[pat.charAt(j) - 'A'][j] = j+1; // Set match case. 
            X = dfa[pat.charAt(j) - 'A'][X];   // Update restart state. 
        } 
    }

}
2
Olavi Mustanoja

[編集:以前に回答したことがすべてのケースで機能するわけではないので、編集して修正します https://meta.stackexchange.com/questions/186689/editing-answers ]

まず第一に、あなたの数学には少し混乱があると思います(n = 7の場合にはn = 10のいくつかの数学があります)

(2 * 26^7) + (2 * 26^1 * 26^6) + (2 * 26^2 * 26^5) + (2 * 26^3 * 26^4) - (3 * 26)

する必要があります

(2 * 26^4) + (2 * 26^1 * 26^3) + (26^2 * 26^2)  - (3 * 26)

または単純に

(5*26^4)-(3*26)

とにかく、動的プログラミングの問題として組み立てられているので、次のように見てみることができます。

1)sをあるインデックスに配置し、結果の文字列にiを配置します。 n - length(s) + 1の可能性があります。

2)各可能性について、残りのインデックスを入力する可能性を数えるだけです。お気づきのとおり、重複の問題が発生しています。これは、siで最初に発生する可能性をカウントするだけで回避できます。したがって、CARをi=0に配置して[〜#〜] car [〜#〜] CAR?を取得するとカウントされますが、CARをインデックスi=3に配置するとCARはカウントしません[〜#〜] car [〜#〜]? 。 siで最初に発生する可能性のみをカウントする場合、重複はカウントしません。 siの後の結果の文字列の部分(サフィックスと呼びます)は、任意の方法で埋めることができます。 sの前の部分i(プレフィックスと呼びます)では、sの出現を挿入しないように注意する必要があります。

2a)ここでできることは、接頭辞を埋める可能性をすべて数え、次にsが含まれている可能性を差し引くことです。この後の部分を数えると、元の問題に帰着します。長さn)特定の部分文字列を含む」より短い長さのみ(文字列全体の長さではなく、接頭辞の長さ)。これで、問題がより小さな問題に削減され、動的な解決策が得られました。

これはCARのような文字列では機能しますが、それ自体と重複する可能性のある文字列では機能しません(長さ7の文字列のインデックス3にLOLを配置すると、?LO [〜 #〜] lol [〜#〜] ?、 LOLの2つのオカレンスを含みます。最初のオカレンスは(完全に)接頭辞にありません(重複しているため)。

2b)これは、少し広い問題を考慮することで修正できます。部分文字列を含むことができる任意の文字列の数は数えませんが、特定のフォームの文字列の数は数えます(一部のインデックスで既に文字が固定されている可能性があります)部分文字列を含めることができます。これを行うと、以前と同じことができます。繰り返しますが、sをいくつかのインデックスに配置し、iをフォーム文字列に配置します(現在、必ずしもn - length(s) + 1sを配置する可能性はありません。このフォームでは、固定文字が含まれている場合があり、sはどこにも収まらない場合があります)。接頭辞を入力する可能性はすべて数えます。ただし、sプレフィックスにある可能性を差し引いてはなりませんが、sプレフィックスにある場合はプラスしますインデックスsに配置したiは、最後の文字を保存します。したがって、??? LOL?がある場合、LOL [〜#〜] lol [〜#〜]?を作成できるため、減算する必要があることだけでなく、 LOL文字列??? LOに入力する可能性があります。?LOLOL?を作成できるため、減算する必要があることもわかります。ここで、より広い問題を検討したので、すでにいくつかのインデックスで文字が固定されている可能性があるフォームの場合、これも問題を縮小して同じサイズの小さな問題にすることです。

クイックコードは次のようになります。

import Java.util.*;

public class StringsInStrings {
    //use arrays of CustomCharacter as strings/forms, use nulls when a index is not yet filled in
    enum CustomCharacter {
        A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
    };

    public static void main(String[] args) {
        CustomCharacter[] string = new CustomCharacter[] { null, null, null, null, null, null, null };
        CustomCharacter[] substring = new CustomCharacter[] { CustomCharacter.L, CustomCharacter.O, CustomCharacter.L };
        System.out.println(countPossibilities(string, substring));
    }

    static int countPossibilities(CustomCharacter[] form, CustomCharacter[] substring) {
        int possibilities = 0;
        //put our substring at any index in the form
        for (int index = 0; index <= form.length - substring.length; index++) {
            //see if the substring fits at that index
            if (fits(form, substring, index)) {
                //count the possibilities for filling in the prefix
                CustomCharacter[] prefix = Arrays.copyOfRange(form, 0, index);
                int prefixPossibilities = (int) Math.pow(CustomCharacter.values().length, countNulls(prefix));
                //count the possibilities for filling in the suffix
                CustomCharacter[] suffix = Arrays.copyOfRange(form, index+substring.length, form.length);
                int suffixPossibilities = (int) Math.pow(CustomCharacter.values().length, countNulls(suffix));

                //count the possibilities where we fill the prefix such that our index is not the first occurence of the substring anymore
                //these need to be subtracted
                CustomCharacter[] reducedForm = Arrays.copyOfRange(insert(form,substring,index), 0, index + substring.length - 1);
                int invalidPrefixPossibilities = countPossibilities(reducedForm, substring);


                possibilities += (prefixPossibilities - invalidPrefixPossibilities) * suffixPossibilities;
            }

        }

        return possibilities;
    }

    private static boolean fits(CustomCharacter[] form, CustomCharacter[] substring, int index) {
        boolean result = true;
        for (int subStrIndex = 0; subStrIndex < substring.length; subStrIndex++) {
            if (!(form[index + subStrIndex] == null || form[index + subStrIndex] == substring[subStrIndex])) {
                result = false;
            }
        }
        return result;
    }

    private static int countNulls(CustomCharacter[] arr) {
        int result = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == null) {
                result++;
            }
        }
        return result;
    }

    private static CustomCharacter[] insert(CustomCharacter[] form, CustomCharacter[] substring, int index) {
        CustomCharacter[] result = Arrays.copyOf(form, form.length);
        for (int i = 0; i < substring.length; i++) {
            result[index + i] = substring[i];
        }
        return result;
    } 
}

ええ、楽しい問題です。これは正しい解決策だと思います。たぶんもっと簡単な解決策がありますか?

[もう1つ編集:メモ化とBigIntegerを備えたバージョン。大きな例をサポートすることで、効率を高め、効率を利用する...コメントで説明されているように]

import Java.math.BigInteger;
import Java.util.*;

public class StringsInStringsCached {
    //use arrays of CustomCharacter as strings/forms, use nulls when a index is not yet filled in
    enum CustomCharacter {
        A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
    };

    static HashMap<List<CustomCharacter>, BigInteger> cache;

    public static void main(String[] args) {
        List<CustomCharacter> string = customCharListWithNulls(100);
        List<CustomCharacter> substring = customCharListFromString("ABCDABD");

        cache = new HashMap<List<CustomCharacter>, BigInteger>();

        System.out.println(countPossibilities(string, substring));
    }

    static BigInteger countPossibilities(List<CustomCharacter> form, List<CustomCharacter> substring) {
        if(!cache.containsKey(form)){
            BigInteger possibilities = BigInteger.ZERO;
            //put our substring at any index in the form
            for (int index = 0; index <= form.size() - substring.size(); index++) {
                //see if the substring fits at that index
                if (fits(form, substring, index)) {
                    //count the possibilities for filling in the prefix
                    List<CustomCharacter> prefix = copyOfRange(form, 0, index);
                    BigInteger.valueOf(CustomCharacter.values().length).pow(countNulls(prefix));
                    BigInteger.valueOf(countNulls(prefix));

                    BigInteger prefixPossibilities = BigInteger.valueOf(CustomCharacter.values().length).pow(countNulls(prefix));
                    //count the possibilities for filling in the suffix
                    List<CustomCharacter> suffix = copyOfRange(form, index+substring.size(), form.size());
                    BigInteger suffixPossibilities = BigInteger.valueOf(CustomCharacter.values().length).pow(countNulls(suffix));

                    //count the possibilities where we fill the prefix such that our index is not the first occurence of the substring anymore
                    //these need to be subtracted
                    List<CustomCharacter> reducedForm = copyOfRange(insert(form,substring,index), 0, index + substring.size() - 1);
                    BigInteger invalidPrefixPossibilities = countPossibilities(reducedForm, substring);

                    possibilities = possibilities.add((prefixPossibilities.subtract(invalidPrefixPossibilities)).multiply(suffixPossibilities));
                }

            }

            cache.put(form, possibilities);
        }
        return cache.get(form);
    }

    private static boolean fits(List<CustomCharacter> form, List<CustomCharacter> substring, int index) {
        boolean result = true;
        for (int subStrIndex = 0; subStrIndex < substring.size(); subStrIndex++) {
            if (!(form.get(index + subStrIndex) == null || form.get(index + subStrIndex) == substring.get(subStrIndex))) {
                result = false;
            }
        }
        return result;
    }

    private static int countNulls(List<CustomCharacter> l) {
        int result = 0;
        for (int i = 0; i < l.size(); i++) {
            if (l.get(i) == null) {
                result++;
            }
        }
        return result;
    }

    private static List<CustomCharacter> insert(List<CustomCharacter> form, List<CustomCharacter> substring, int index) {
        List<CustomCharacter> result = new ArrayList<CustomCharacter>(form);
        for (int i = 0; i < substring.size(); i++) {
            result.set(index + i, substring.get(i));
        }
        return result;
    } 

    private static List<CustomCharacter> copyOfRange(List<CustomCharacter> l, int from, int to){
        List<CustomCharacter> result = new ArrayList<CustomCharacter>();
        for(int i = from; i < to; i++){
            result.add(l.get(i));
        }
        return result;
    }

    private static List<CustomCharacter> customCharListWithNulls(int size) {
        List<CustomCharacter> result = new ArrayList<CustomCharacter>();
        for(int i = 0; i < size; i++){
            result.add(null);
        }
        return result;
    }

    private static List<CustomCharacter> customCharListFromString(String s) {
        List<CustomCharacter> result = new ArrayList<CustomCharacter>();
        for(char c : s.toCharArray()){
            result.add(CustomCharacter.valueOf(String.valueOf(c)));
        }

        return result;
    }
}
2
Hirle

動的プログラミング問題のようにそれを解決するアイデアがあります。

対称問題を見てみましょう:特定の部分文字列Tを含まない文字列の数

必要な文字列の長さがnであり、有効な文字列の1つがSであると想定します。(n == S.length())

2d配列dp [n] [T.length()]を定義し、
dp [i、j]を使用して、「サブストリングs [0 ... i]のサフィックスがTのプレフィックスと同じで、最も長いプロパティの組み合わせがいくつあるかを表します。接尾辞/接頭辞の長さ(変数「j」)?」。

言い換えると:定数iについては、「S [i-len + 1、i] == T [0 ... len]」のdmax [len}を見つけ、dp [i 、len]は、この状態での組み合わせの数を意味します。

例:
T = abcabcおよびS [0 ... i]:
j = 6は、「Sの末尾の6文字」がabcabcであることを意味します。 (S[i-5...I] == abcabc
j = 3は、「Sの末尾の6文字」が### abcであり、###!="abc"
dp [i、j = 3]にはdp [i、j = 6]が含まれていません

そしてここにコードがあります

パフォーマンスのために、KMPアルゴリズムを使用して文字列Tを前処理し、「nxt」配列を生成します。

T  = '*'+T; // to avoid -1 in index of array dp. (dp[i,-1])
geneNext();
dp[0][0]=25; dp[0][1]=1;
for (int i=0; i<n; i++) {
    for (int j=0; j<T.size()-1; j++) {
        //cout<<"dp["<<i<<","<<j<<"] = "<<dp[i][j]<<endl;
        for (int ch=0; ch<26; ch++) {//here we assume S[i+1]=ch.
            int tmp;// tmp is the new variable 'j' for S[0...i+1].
            for (tmp=j+1; tmp>0; tmp = nxt[tmp])
                if (T[tmp] == ch+'a')
                    break;
            dp[i+1][tmp] = dp[i+1][tmp]+dp[i][j];
        }
    }
}
int ans = 0;
for (int i=0; i<T.size()-1; i++)
    ans = ans + dp[n-1][i];
cout<<ans<<endl; // ans is my symmetric problem.
cout<<26^n - ans<<endl; // the answer for poster's problem.
0
vrqq