【问题标题】:Common subsequence of given length给定长度的公共子序列
【发布时间】:2015-09-02 09:58:54
【问题描述】:

找到所有两个字符串的长度为k的公共子序列有哪些好方法?

示例:

s1= AAGACC

s2= AGATAACCAGGAGCTGC

长度为5的所有常见子序列:AAGACAAACCAGACCAAGCC

【问题讨论】:

  • OP,你熟悉动态规划吗?你应该在任何好的算法书籍中找到它。
  • 公共子序列是否与字符串相同但与源位置序列不同时是否被视为相等?例如,在您的示例中,有 3*15=45 种方式产生公共子序列 AA,那么 AA 应该输出 45 次,还是只输出一次?
  • @j_random_hacker 一次。
  • 在这种情况下,我相信避免 O(|A|^k) 空间解决方案的唯一方法(您需要记录每个长度为 k 的子序列是否已被“看到” -- 像(AB)^(2k) 这样的输入似乎强制这样做)是尝试以某种(例如,字典顺序)顺序为每个字符串生成长度为 k 的子序列,并将它们列表合并。
  • 在前面的评论中,我的意思是 |A|是字母表的大小。

标签: c string algorithm subsequence


【解决方案1】:

一种相对简单的方法是从 LCS 矩阵重构序列。这是一个 O(n^2 * k + x * n) 算法,其中 x 是输出的大小(即长度为 k的公共子序列的数量>)。它是用 C++ 编写的,但应该很容易翻译成 C:

const int N = 100;
int lcs[N][N];
set<tuple<string,int,int,int>> vis;

string s1 = "AAGACC";
string s2 = "AGATAACCAGGAGCTGC";

void reconstruct(const string& res, int i, int j, int k) {
    tuple<string,int,int,int> st(res, i, j, k);
    if (vis.count(st))
        return;
    vis.insert(st);
    if (lcs[i][j] < k) return;
    if (i == 0  && j == 0 && k == 0) {
        cout << res << endl;
        return;
    }
    if (i > 0)
        reconstruct(res, i-1, j, k);
    if (j > 0)
        reconstruct(res, i, j-1, k);
    if (i>0 && j>0 && s1[i-1] == s2[j-1])
        reconstruct(string(1,s1[i-1]) + res, i-1, j-1, k-1);
}

int main() {
    lcs[0][0] = 0;
    for (int i = 0; i <= s1.size(); ++i)
        lcs[i][0] = 0;
    for (int j = 0; j <= s1.size(); ++j)
        lcs[0][j] = 0;
    for (int i = 0; i <= s1.size(); ++i) {
        for (int j = 0; j <= s2.size(); ++j) {
            if (i > 0)
                lcs[i][j] = max(lcs[i][j], lcs[i-1][j]);
            if (j > 0)
                lcs[i][j] = max(lcs[i][j], lcs[i][j-1]);
            if (i > 0 && j > 0 && s1[i-1] == s2[j-1])
                lcs[i][j] = max(lcs[i][j], lcs[i-1][j-1] + 1);
        }
    }
    reconstruct("", s1.size(), s2.size(), 5);
}

还应该有一个 O(n * (k + x)) 的方法来解决这个问题,基于稍微不同的 DP 方法:让 f(i, k) 为最小索引 j 使得 lcs(i, j) >= k。我们有复发

f(i, 0) = 0 for all i
f(i, k) = min{f(i-1, k), 
              minimum j > f(i-1, k-1) such that s2[j] = s1[i]}

我们还可以从矩阵f重构长度为k的序列。

【讨论】:

  • 我用 s1= ACACTTAGTGGGAGTCTCA 和 s2 = TACGCk=3 进行了尝试,其中一个输出是 GAC;这显然 不是 s1 和 s2 之间的公共子序列。
  • @CamiloCelisGuzman 有趣。那么在这种情况下,我将暂时回滚到我最初的答案,这应该是正确的
  • 这是否只生成一次等于公共子序列的每个字符串? OP(迟来的)表明这是一项要求。如果没有,我认为在最坏的情况下调整它需要 O(|A|^k) 空间来存储是否已经看到给定字符串的状态(|A| 字母大小)。
  • @j_random_hacker 不,它没有。我以为我有另一种基于输出敏感的第二次重复的方法,但我认为它可能无法修复。我认为问题不在于空间消耗,而是必须渐进地生成更多序列,然后才能过滤掉
  • 我认为避免空间爆炸的唯一方法是按(例如,lex)顺序分别生成每个字符串的所有长度为k的子序列,并将它们与列表合并相交。通过对字符串字符重复应用 find-next-largest 算法,可以在 O(|A|^k) 时间内生成 2 个单独的有序列表。也可能有捷径(可能不会降低渐近复杂度),例如如果没有带有前缀 X 和 |X| 的 CS = i i) CS。
【解决方案2】:

s1 中为给定长度k 的所有子序列创建一个trie,然后遍历s2 并检查每个长度为k 的序列是否在trie 中。

【讨论】:

  • 作为@NikklasB。指出,问题是有O(Choose(n,k)) 子序列(它不是子字符串),它在O(min{n^k,n^(n-k)}) 中(所以,很多)
  • 是的。有没有办法使用 LCS(最长公共子序列)记忆矩阵来实现这一点?
  • @amit - 我同意,但无论如何您都需要检查所有选项,除非我遗漏了什么。你可以做一些复杂的嵌套循环——我会考虑的。
  • @shapiro.yaacov 不,您只需要检查 common 子序列,根据输入,这些子序列可能会逐渐减少
  • @shapiro.yaacov 不确定,最长公共子序列不需要它。我想知道您是否可以更有效地仅获得计数,这也将使获得子序列的效率更高(虽然它们的数量仍然是线性的,在最坏的情况下可能是指数级的,但通常要好得多)。
【解决方案3】:

这是一个使用递归的算法版本,一个大小为k 的堆栈,并包括两个优化来跳过已经看到的字符和跳过不存在的子序列。字符串不是唯一的(可能有重复),因此通过uniq 运行输出。

#include <stdio.h>
#include <string.h>

/* s1 is the full first string, s2 is the suffix of the second string
 * (starting after the subsequence at depth r),
 * pos is the positions of chars in s1, r is the recursion depth,
 * and k is the length of subsequences that we are trying to match
 */
void recur(char *s1, char *s2, size_t pos[], size_t r, size_t k)
{
    char seen[256] = {0};       /* have we seen a character in s1 before? */
    size_t p0 = (r == 0) ? 0 : pos[r-1] + 1;    /* start at 0 or pos[r-1]+1 */
    size_t p1 = strlen(s1) - k + r;             /* stop at end of string - k + r */
    size_t p;

    if (r == k)         /* we made it, print the matching string */
    {
        for (p=0; p<k; p++)
            putchar(s1[pos[p]]);
        putchar('\n');
        return;
    }

    for (p=p0; p<=p1; p++)      /* at this pos[], loop through chars to end of string */
    {
        char c = s1[p];         /* get the next char in s1 */
        if (seen[c])
            continue;           /* don't go any further if we've seen this char already */
        seen[c] = 1;

        pos[r] = p;
        char *s = strchr(s2, c);        /* is this char in s2 */
        if (s != NULL)
            recur(s1, s+1, pos, r+1, k);        /* recursively proceed with next char */
    }
}


int main()
{
    char *s1 = "AAGACC";
    char *s2 = "AGATAACCAGGAGCTGC";
    size_t k = 5;
    size_t pos[k];

    if (strlen(s1) < k || strlen(s2) < k)       /* make sure we have at least k chars in each string */
        return 1;       /* exit with error */

    recur(s1, s2, pos, 0, k);
    return 0;
}

输出是:

AAGAC
AAGCC
AAACC
AGACC

【讨论】:

    猜你喜欢
    • 2011-03-01
    • 2021-04-28
    • 1970-01-01
    • 1970-01-01
    • 2011-02-25
    • 2013-02-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多