web-dev-qa-db-ja.com

文字列内のサブシーケンスの出現回数を見つける

たとえば、文字列をpiの最初の10桁、3141592653、サブシーケンスは123。シーケンスは2回発生することに注意してください。

3141592653
 1    2  3
   1  2  3

これは私が答えることができなかったインタビューの質問であり、効率的なアルゴリズムを考えることができず、私を悩ませています。単純な正規表現を使用して実行できるはずですが、1.*2.*3すべてのサブシーケンスを返すわけではありません。 Python(各1の後に2ごとに3を数える)での私の素朴な実装は1時間実行され、完了していません。

60
Jake

これは古典的な 動的プログラミング 問題です(通常、正規表現を使用して解決されません)。

私の素朴な実装(各1の後に2ごとに3を数える)が1時間実行され、完了していません。

これは、指数関数的な時間で実行される網羅的な検索アプローチです。 (私はそれが何時間も実行されることに驚いています)。


動的プログラミングソリューションの提案を次に示します。

再帰的ソリューションの概要:

(長い説明については謝罪しますが、各ステップは本当に簡単ですのでご容赦ください;-)

  • サブシーケンスが空の場合、一致が見つかり(一致する数字はありません!)、1を返します。

  • 入力シーケンスが空の場合、数字を使い果たして一致が見つからない可能性があるため、0を返します。

  • (シーケンスもサブシーケンスも空ではありません。)

  • (「abcdef」が入力シーケンスを示し、「xyz」がサブシーケンスを示すと仮定します。)

  • resultを0に設定します

  • resultbcdefおよびxyzの一致数を追加します(つまり、最初の入力数字を破棄して再帰します)

  • 最初の2桁が一致する場合、つまりa = x

    • resultbcdefおよびyzの一致数を追加します(つまり、最初のサブシーケンスの数字と一致し、再帰します残りのサブシーケンスの数字)

  • resultを返します


入力1221/12の再帰呼び出しの図を次に示します。 (太字のサブシーケンス、空の文字列を表します。)

enter image description here


動的プログラミング

単純に実装すると、いくつかの(副)問題が複数回解決されます(たとえば、上記の図では2つ)。ダイナミックプログラミングは、以前に解決された副問題(通常はルックアップテーブル)の結果を記憶することにより、このような冗長な計算を回避します。

この特定のケースでは、テーブルをセットアップします

  • [シーケンスの長さ+ 1]行、および
  • [サブシーケンスの長さ+ 1]列:

enter image description here

これは、対応する行/列に221/2の一致数を入力するという考え方です。完了したら、セル1221/12に最終的なソリューションが必要です。

私たちはすぐに知っていることをテーブルに追加し始めます(「ベースケース」):

  • サブシーケンスの数字が残っていない場合、1つの完全一致があります。

enter image description here

  • シーケンスの数字が残っていない場合、一致するものはありません。

    enter image description here

次に、次のルールに従ってテーブルをトップダウン/左から右に移していきます。

  • セル[row] [col]に、[row-1] [col]で見つかった値を書き込みます。

    直観的には、これは"221/2の一致数には21/2。のすべての一致が含まれます。"

  • のシーケンスと列のサブシーケンスが同じ数字で始まる場合、[-1] [-で見つかった値を追加しますcol-1]を[row] [col]に書き込まれた値に変更します。

    直感的には、これは"1221/12の一致数には221/12。のすべての一致も含まれます。"

enter image description here

最終結果は次のようになります。

enter image description here

そして、右下のセルの値は実際には2です。


コード内

Pythonではありません(私の謝罪)。

class SubseqCounter {

    String seq, subseq;
    int[][] tbl;

    public SubseqCounter(String seq, String subseq) {
        this.seq = seq;
        this.subseq = subseq;
    }

    public int countMatches() {
        tbl = new int[seq.length() + 1][subseq.length() + 1];

        for (int row = 0; row < tbl.length; row++)
            for (int col = 0; col < tbl[row].length; col++)
                tbl[row][col] = countMatchesFor(row, col);

        return tbl[seq.length()][subseq.length()];
    }

    private int countMatchesFor(int seqDigitsLeft, int subseqDigitsLeft) {
        if (subseqDigitsLeft == 0)
            return 1;

        if (seqDigitsLeft == 0)
            return 0;

        char currSeqDigit = seq.charAt(seq.length()-seqDigitsLeft);
        char currSubseqDigit = subseq.charAt(subseq.length()-subseqDigitsLeft);

        int result = 0;

        if (currSeqDigit == currSubseqDigit)
            result += tbl[seqDigitsLeft - 1][subseqDigitsLeft - 1];

        result += tbl[seqDigitsLeft - 1][subseqDigitsLeft];

        return result;
    }
}

複雑

この「テーブルに記入」アプローチのボーナスは、複雑さを把握するのが簡単なことです。各セルに対して一定量の作業が行われ、シーケンスの長さの行とサブシーケンスの長さの列があります。そのため、複雑さO(MN) where [〜#〜] m [〜#〜]および[〜#〜] n [〜#〜]は、シーケンスの長さを示します。

116
aioobe

素晴らしい答え、 aioobe !あなたの答えを補完するために、Pythonでのいくつかの可能な実装:

# straightforward, naïve solution; too slow!

def num_subsequences(seq, sub):
    if not sub:
        return 1
    Elif not seq:
        return 0
    result = num_subsequences(seq[1:], sub)
    if seq[0] == sub[0]:
        result += num_subsequences(seq[1:], sub[1:])
    return result

# top-down solution using explicit memoization

def num_subsequences(seq, sub):
    m, n, cache = len(seq), len(sub), {}
    def count(i, j):
        if j == n:
            return 1
        Elif i == m:
            return 0
        k = (i, j)
        if k not in cache:
            cache[k] = count(i+1, j) + (count(i+1, j+1) if seq[i] == sub[j] else 0)
        return cache[k]
    return count(0, 0)

# top-down solution using the lru_cache decorator
# available from functools in python >= 3.2

from functools import lru_cache

def num_subsequences(seq, sub):
    m, n = len(seq), len(sub)
    @lru_cache(maxsize=None)
    def count(i, j):
        if j == n:
            return 1
        Elif i == m:
            return 0
        return count(i+1, j) + (count(i+1, j+1) if seq[i] == sub[j] else 0)
    return count(0, 0)

# bottom-up, dynamic programming solution using a lookup table

def num_subsequences(seq, sub):
    m, n = len(seq)+1, len(sub)+1
    table = [[0]*n for i in xrange(m)]
    def count(iseq, isub):
        if not isub:
            return 1
        Elif not iseq:
            return 0
        return (table[iseq-1][isub] +
               (table[iseq-1][isub-1] if seq[m-iseq-1] == sub[n-isub-1] else 0))
    for row in xrange(m):
        for col in xrange(n):
            table[row][col] = count(row, col)
    return table[m-1][n-1]

# bottom-up, dynamic programming solution using a single array

def num_subsequences(seq, sub):
    m, n = len(seq), len(sub)
    table = [0] * n
    for i in xrange(m):
        previous = 1
        for j in xrange(n):
            current = table[j]
            if seq[i] == sub[j]:
                table[j] += previous
            previous = current
    return table[n-1] if n else 1
14
Óscar López

それを行う1つの方法は、2つのリストを使用することです。それらをOnesおよびOneTwosと呼びます。

文字列を1文字ずつ調べます。

  • 数字1が表示されたら、Onesリストにエントリを作成します。
  • 数字2が表示されたら、Onesリストに目を通し、OneTwosリストにエントリを追加します。
  • 数字3が表示されるたびに、OneTwosリストを調べて、123を出力します。

一般的な場合、このアルゴリズムは文字列を1回通過し、通常ははるかに小さいリストを複数回通過するため、非常に高速になります。しかし、病理学的なケースはそれを殺します。 111111222222333333のような文字列を想像してください。ただし、各桁は何百回も繰り返されます。

7
Jim Mischel
from functools import lru_cache

def subseqsearch(string,substr):
    substrset=set(substr)
    #fixs has only element in substr
    fixs = [i for i in string if i in substrset]
    @lru_cache(maxsize=None) #memoisation decorator applyed to recs()
    def recs(fi=0,si=0):
        if si >= len(substr):
            return 1
        r=0
        for i in range(fi,len(fixs)):
            if substr[si] == fixs[i]:
                r+=recs(i+1,si+1)
        return r
    return recs()

#test
from functools import reduce
def flat(i) : return reduce(lambda x,y:x+y,i,[])
N=5
string = flat([[i for j in range(10) ] for i in range(N)])
substr = flat([[i for j in range(5) ] for i in range(N)]) 
print("string:","".join(str(i) for i in string),"substr:","".join(str(i) for i in substr),sep="\n")
print("result:",subseqsearch(string,substr))

出力(瞬時):

string:
00000000001111111111222222222233333333334444444444
substr:
0000011111222223333344444
result: 1016255020032
2
Luka Rahne

geeksforgeeks.orgからの動的プログラミング および aioobe からの回答に基づくJavascript回答:

class SubseqCounter {
    constructor(subseq, seq) {
        this.seq = seq;
        this.subseq = subseq;
        this.tbl = Array(subseq.length + 1).fill().map(a => Array(seq.length + 1));
        for (var i = 1; i <= subseq.length; i++)
          this.tbl[i][0] = 0;
        for (var j = 0; j <= seq.length; j++)
          this.tbl[0][j] = 1;
    }
    countMatches() {
        for (var row = 1; row < this.tbl.length; row++)
            for (var col = 1; col < this.tbl[row].length; col++)
                 this.tbl[row][col] = this.countMatchesFor(row, col);

        return this.tbl[this.subseq.length][this.seq.length];
    }
    countMatchesFor(subseqDigitsLeft, seqDigitsLeft) {
            if (this.subseq.charAt(subseqDigitsLeft - 1) !=     this.seq.charAt(seqDigitsLeft - 1)) 
            return this.tbl[subseqDigitsLeft][seqDigitsLeft - 1];  
            else
            return this.tbl[subseqDigitsLeft][seqDigitsLeft - 1] +     this.tbl[subseqDigitsLeft - 1][seqDigitsLeft - 1]; 
    }
}
0

私はこの問題に対して興味深いO(N)時間とO(M) space solutionがあります。
Nはテキストの長さ、Mは検索するパターンの長さです。 C++で実装しているため、アルゴリズムを説明します。

与えられた入力が3141592653を提供したものであり、検索するカウントが123であるパターンシーケンスであるとします。入力パターン内の文字の位置に文字をマッピングするハッシュマップを取得することから始めます。また、最初に0に初期化されたサイズMの配列を使用します。

    string txt,pat;
    cin >> txt >> pat;
    int n = txt.size(),m = pat.size();
    int arr[m];
    map<char,int> mp;
    map<char,int> ::iterator it;
    f(i,0,m)
    {
        mp[pat[i]] = i;
        arr[i] = 0;
    }

後ろから要素を探し始め、各要素がパターン内にあるかどうかを確認します。その要素がパターン内にある場合。私は何かしなければならない。

今、私は2から前を見つけた場合、後ろから見て始めたときに、3を見つけていません。この2は私たちにとって価値がありません。それは、そのようなシーケンス12と123を形成することがほとんどないため、1が見つかったので、Rytは形成されませんか?と思います。また、現在の位置iで2が見つかったため、以前に見つかった3のみでシーケンス123を形成し、以前にx 3が見つかった場合はxシーケンスを形成します(2より前のシーケンスの一部が見つかった場合)したがって、完全なアルゴリズムは、配列に存在する要素を見つけるたびに、パターン(ハッシュマップに格納)に存在した位置jに対応してチェックします。私はちょうどインクインクリメント

 arr[j] += arr[j+1];

それが乾く前に見つかった3つのシーケンスに貢献することを意味しますか?見つかったjがm-1の場合、単純にインクリメントします

 arr[j] += 1; 

これらを行う以下のコードスニペットを確認してください

    for(int i = (n-1);i > -1;i--)
    {
        char ch = txt[i];
        if(mp.find(ch) != mp.end())
        {
            int j = mp[ch];
            if(j == (m-1))
                arr[j]++;
            else if(j < (m-1))
                arr[j] += arr[j+1];
            else
                {;}
        }
    }

事実を考えてみましょう

配列内の各インデックスiは、パターンS [i、(m-1)]の部分文字列が入力文字列のシーケンスとして出現する回数を格納するため、最終的にarr [0]の値を出力します

    cout << arr[0] << endl;

出力付きコード(パターン内の一意の文字) http://ideone.com/UWaJQF

出力付きコード(文字の繰り返しが許可されています) http://ideone.com/14DZh7

パターンが一意の要素を持っている場合にのみ拡張が機能するパターンが一意の要素を持っている場合、複雑さはO(MN)それに対応する配列位置jは、パターンのこのすべての文字の出現を更新する必要があります。これにより、O(N *文字の最大頻度)の複雑さが生じます。

#define f(i,x,y) for(long long i = (x);i < (y);++i)



int main()
{
long long T;
cin >> T;
while(T--)
{
    string txt,pat;
    cin >> txt >> pat;
    long long n = txt.size(),m = pat.size();
    long long arr[m];
    map<char,vector<long long> > mp;
    map<char,vector<long long> > ::iterator it;
    f(i,0,m)
    {
        mp[pat[i]].Push_back(i);
        arr[i] = 0;
    }

    for(long long i = (n-1);i > -1;i--)
    {
        char ch = txt[i];
        if(mp.find(ch) != mp.end())
        {
            f(k,0,mp[ch].size())
            {
                long long j = mp[ch][k];
                if(j == (m-1))
                    arr[j]++;
                else if(j < (m-1))
                    arr[j] += arr[j+1];
                else
                    {;}
                }
                }
                }
                cout <<arr[0] << endl;
                }
                 }

繰り返しのある文字列でDPを使用せずに同様の方法で拡張できますが、複雑さはO(MN)

0
Vinayak Sangar

私の簡単な試み:

def count_subseqs(string, subseq):
    string = [c for c in string if c in subseq]
    count = i = 0
    for c in string:
        if c == subseq[0]:
            pos = 1
            for c2 in string[i+1:]:
                if c2 == subseq[pos]:
                    pos += 1
                    if pos == len(subseq):
                        count += 1
                        break
        i += 1
    return count

print count_subseqs(string='3141592653', subseq='123')

編集:これは、1223 == 2およびより複雑なケース:

def count_subseqs(string, subseq):
    string = [c for c in string if c in subseq]
    i = 0
    seqs = []
    for c in string:
        if c == subseq[0]:
            pos = 1
            seq = [1]
            for c2 in string[i + 1:]:
                if pos > len(subseq):
                    break
                if pos < len(subseq) and c2 == subseq[pos]:
                    try:
                        seq[pos] += 1
                    except IndexError:
                        seq.append(1)
                        pos += 1
                Elif pos > 1 and c2 == subseq[pos - 1]:
                    seq[pos - 1] += 1
            if len(seq) == len(subseq):
                seqs.append(seq)
        i += 1
    return sum(reduce(lambda x, y: x * y, seq) for seq in seqs)

assert count_subseqs(string='12', subseq='123') == 0
assert count_subseqs(string='1002', subseq='123') == 0
assert count_subseqs(string='0123', subseq='123') == 1
assert count_subseqs(string='0123', subseq='1230') == 0
assert count_subseqs(string='1223', subseq='123') == 2
assert count_subseqs(string='12223', subseq='123') == 3
assert count_subseqs(string='121323', subseq='123') == 3
assert count_subseqs(string='12233', subseq='123') == 4
assert count_subseqs(string='0123134', subseq='1234') == 2
assert count_subseqs(string='1221323', subseq='123') == 5
0
Jakub

psh。 O(n)解決策ははるかに優れています。

ツリーを構築して考えてみてください。

文字が「1」の場合、文字列に沿って反復し、ツリーのルートにノードを追加します。文字が「2」の場合、各第1レベルのノードに子を追加します。文字が「3」の場合、各第2レベルのノードに子を追加します。

第3層ノードの数を返します。

これはスペース効率が悪いので、各深さのノード数だけを保存しないのはなぜですか。

infile >> in;
long results[3] = {0};
for(int i = 0; i < in.length(); ++i) {
    switch(in[i]) {
        case '1':
        results[0]++;
        break;
        case '2':
        results[1]+=results[0];
        break;
        case '3':
        results[2]+=results[1];
        break;
        default:;
    }
}

cout << results[2] << endl;
0
datdo

数字の配列内のすべての3メンバーシーケンス1..2..3をカウントする方法。

すばやく簡単に

すべてのシーケンスを見つける必要はなく、カウントするだけです。そのため、シーケンスを検索するすべてのアルゴリズムは非常に複雑です。

  1. 1、2、3ではないすべての数字を捨てます。結果はchar配列Aになります
  2. 0の並列int配列Bを作成します。 Aを最後から実行し、Aの各2の後に、Aの3の数を数えます。これらの数値をBの適切な要素に入れます。
  3. 0の並列int配列Cを作成します。Aの各1の終了カウントからAをその位置の後のBの合計で実行します。結果はCの適切な場所に配置されます。
  4. Cの合計を数えます.

それだけです。複雑さはO(N)です。実際、通常の数字の行では、ソース行の短縮の約2倍の時間がかかります。

シーケンスがより長い場合、たとえばMメンバーの場合、手順をM回繰り返すことができます。そして、複雑さはO(MN)になります。Nはすでに短縮されたソース文字列の長さです。

0
Gangnus