查找字符串中子序列的出现次数

时间:2011-07-29 18:12:05

标签: python algorithm dynamic-programming

例如,让字符串为pi的前10位数,3141592653,子序列为123。请注意,序列出现两次:

3141592653
 1    2  3
   1  2  3

这是一个我无法回答的面试问题,我想不出一个有效的算法而且它让我烦恼。我觉得应该可以使用一个简单的正则表达式,但1.*2.*3之类的函数不会返回每个子序列。我在Python中的天真实现(在每个1之后计算每个2的3个)已经运行了一个小时而且还没有完成。

9 个答案:

答案 0 :(得分:113)

这是一个经典的dynamic programming问题(通常不使用正则表达式解决)。

  

我天真的实施(在每个1之后计算每个2的3个)已经运行了一个小时而且还没有完成。

这将是一种在指数时间内运行的详尽搜索方法。 (我很惊讶它跑了几个小时)。


以下是动态编程解决方案的建议:

递归解决方案的大纲:

(对于长篇描述道歉,但每一步都非常简单,所以请耐心等待;)

  • 如果子序列为空,则会找到匹配项(没有匹配的数字!)我们返回1

  • 如果输入序列为空,我们已经耗尽了数字,无法找到匹配项,因此我们返回0

  • (序列和子序列都不为空。)

  • (假设“ abcdef ”表示输入序列,“ xyz ”表示子序列。)

  • result设为0

  • bcdef xyz 的匹配数添加到result (即丢弃第一个输入数字并递归)

  • 如果前两位数匹配,即 a = x

    • bcdef yz 的匹配数添加到result(即,匹配第一个子序列数字并递归剩余的子序列数字)

  • 返回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

然后我们按照以下规则从上到下/从左到右填充表格:

  • 在单元格[] [ col ]中写入[ -1] [col]中找到的值。< / p>

    直观地,这意味着“221 / 2 的匹配数包括21 / 2 的所有匹配。”

  • 如果行的序列和列 col 的子序列以相同的数字开头,请添加在[中找到的值-1] [ col -1]到刚刚写入[] [ 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),其中 M N 表示序列的长度。

答案 1 :(得分:14)

很棒的答案,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

答案 2 :(得分:7)

一种方法是使用两个列表。称他们为OnesOneTwos

逐个字符地浏览字符串。

  • 每当您看到数字1时,请在Ones列表中输入一个条目。
  • 每当您看到数字2时,请浏览Ones列表并在OneTwos列表中添加一个条目。
  • 每当您看到数字3时,请浏览OneTwos列表并输出123

在一般情况下算法将非常快,因为它是单个传递字符串并且多次传递通常会小得多的列表。然而,病理性病例会杀死它。想象一下像111111222222333333这样的字符串,但每个数字重复数百次。

答案 3 :(得分:2)

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

答案 4 :(得分:0)

我的快速尝试:

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

答案 5 :(得分:0)

如何计算数字数组中的所有三个成员序列1..2..3。

快速而简单

注意,我们不需要查找所有序列,我们只需要COUNT它们。因此,所有搜索序列的算法都过于复杂。

  1. 扔掉每一个数字,不是1,2,3。结果将是char数组A
  2. 使并行int数组B为0。从末尾开始运行A,计算A中的每个2中A中的3个数。将这些数字放入B的适当元素中。
  3. 使并行int数组C为0。运行A从A的每个1的结束计数到其位置后的B的总和。结果放入C中的适当位置。
  4. 计算C的总和。
  5. 就是这样。复杂度为O(N)。实际上,对于正常的数字行,它将花费大约两倍于缩短源行的时间。

    如果序列更长,例如M个成员,则该过程可重复M次。复杂性将是O(MN),其中N已经是缩短的源字符串的长度。

答案 6 :(得分:0)

PSH。 O(n)解决方案更好。

通过构建树来思考它:

沿着字符串迭代 如果字符为'1',则将节点添加到树的根。 如果字符为'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;

答案 7 :(得分:0)

对于这个问题,我有一个有趣的 O(N)时间和O(M)空间解决方案
N是文本的长度,M是要搜索的模式的长度。 我将向您解释算法,因为我在C ++中实现。

假设给出的输入与您提供的一样3141592653 以及要查找的计数为123的模式序列。 我将从一个哈希映射开始,该映射将字符映射到它们在输入模式中的位置。我还将一个大小为M的数组初始化为0。

    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对我们没有价值。因为在它之后发现的任何1将最终形成这样的序列12和123不会形成Ryt?认为。 同样在目前的位置,我发现了一个2,它将形成序列123,只有之前发现的3个,如果我们之前发现了x 3,那么将形成x序列(如果找到2之前的序列的一部分)ryt? 因此,完整的算法是每当我找到一个存在于数组中的元素时,我会相应地检查它在模式中存在的位置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) 解决方案类似于不使用DP时,只是当模式中出现的元素出现时,我们只增加对应于它的数组位置j,我们现在必须更新模式中所有这些字符的出现,这将导致O的复杂度(N * maxium frequency)一个特征)

#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)

答案 8 :(得分:0)

基于dynamic programming from geeksforgeeks.org和来自aioobe的答案的Java脚本答案:

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]; 
    }
}