在序列中寻找元素包的算法

4

假设我有一个由感兴趣的元素A、B、C...以及不关心的符号x交错组成的序列。我想要在预先定义的距离内识别出一些有趣的组合集,并且这些组合集来自于一个预先定义好的集合。这些符号之间的跨度可以重叠。例如,在字符串"C x x A A x x C"中,如果最大距离为5,则算法会检测到模式"A A C"两次。

例如,假设我的有趣组合集如下:

A A C
A B C

我有一个序列:

B A x x C x x x A A x x x C

并且最大跨度为5。

我的算法应该输出:

B A x x C -> A B C

由于感兴趣元素之间的跨度大于5,因此它将无法识别模式A A C

我的直觉告诉我这是一种动态规划算法,但也许它只是一个我无法发现的众所周知的算法实例。

有什么提示可以提供关于应该采取什么方法/解决方案吗?


1
是的,让我来转述一下:考虑我的数组中所有可能的5个元素的括号。在这些括号上,找到从提供的字典中选出的元素子集。现在清楚了吗?显然,考虑所有可能的元素括号意味着需要检查N-M个括号,然后找到子集,这对我来说似乎是浪费的。因此,我的猜想是有一种聪明的方法可以做到这一点,因为我只关注所需的元素A、B、C... - tonicebrian
1
天真的方法是仅扫描所有长度为5的子序列,并检查它是否包含来自m个有趣集合之一的所有元素,因此时间复杂度为O(n*m)。您想要优化这个过程还是使用预计算? - SaiBot
我想利用有很多“don't care”符号和模式字典适中大的事实,而不是使用蛮力法。 - tonicebrian
是的,所以对于“括号”/部分 {A,A,x,B,B},如果您对{A,B}感兴趣,则会将{A,B}的“计数”增加4个? - Elliott
这是严格的5,还是长度可变的? - Elliott
显示剩余5条评论
2个回答

2
让我们给问题命名一些名称:
m = 数组序列的长度(例如您的示例中为14) n = 数组序列中唯一元素的总数(例如示例中为3) k = 每个搜索区域的长度(例如示例中为5) g = 您要查找的组数(例如示例中为2)
一种选择是在每个大小为k的搜索区域中总结您的数据。在您的示例中,如下所示:
{B A x x C}
{A x x C x}
...

我们为每个部分创建大小为n的向量,第一个元素表示第一种元素的出现情况,比如A

B A x x C --> [1,1,1] (one appearance of each)
A x x C x --> [1,0,1]

我们可以对我们正在搜索的群组执行相同的操作:

等等。

{A A C} --> [2,0,1]  
{A B C} --> [1,1,1]

现在问题变得很明显。假设我们考虑搜索区域的摘要 [3,2,5] 和我们要搜索的一组摘要 [0,1,2],我们可以通过认识到第二个元素有两种选择,以及对于第三个元素有 (5x4)/(1x2) 种选择来计算组合数,因此总共有20种选择。
因此,对于区域摘要 S[s1, s2,..,sn] 和一个单独的感兴趣的组 G[g1, g2,...gn],我们可以计算从 S 中提取 G 的总方法数(C++风格代码,除非 "!" 表示阶乘):
int total_options = 1; // total ways to select G from S
for (int i = 0; i < n; ++i)
{
    if(g[i] == 0)
        continue; // this is an element that doesn't appear in G, so it shouldn't effect our count

    if(s[i] < g[i])
        return 0; // not enough elements in S for G

    for (int d = 1, f = s[i]; f > max(g[i], s[i] - g[i]); --f, ++d)
        total_options = total_options / d * f; // f, d are effectively factorials

    // the previous loop is a more efficient version of:
    // total_options *= (s[i]!) /(g[i]! * (s[i] - g[i])!);
}

return  total_options;

您需要为每个部分和每个搜索组执行此操作。
时间复杂度:O(g*m*(k+n))(由于最坏情况下的阶乘计算,我们必须包括k)
空间复杂度:O(m+g*n)(我们可以在进行计算时计算每个部分,因此无需同时存储多个部分)
然后,我们可以通过意识到每个连续的“部分”只是考虑到离开的“尾”元素和进入的“头”元素而不同来改进它,因此我们应该计算这两个如何将“选项计数”更改为我们迭代到下一个部分。我们将通过维护先前的“选项计数”计算和NF(失败数)来执行此操作,即区域中太低以至于无法满足搜索组的元素数量。诀窍是维护一个正的“选项计数”,仅当NF为0时才将其添加到总数中。这将使您在迭代大小为m的主数组时为每个G提供常量时间结果。
时间复杂度:O(g*m+g*n) 空间复杂度:O(g*n+m)
当主数组中的每个元素都是唯一的,并且这些元素中的每个元素都至少出现在某些搜索中时,此算法具有最坏情况性能(否则,我们可以将不出现在任何搜索中的任何元素视为相同,例如您的示例中的“x”)。因此,最坏情况下的复杂性可以简化为:
时间复杂度:O(g*m) 空间复杂度:O(g*m)
我看不出如何可能获得更好的时间复杂度,但如果有聪明人能想出具有较低空间复杂度的方法,我会很感兴趣。
如果您不知道仅通过考虑头和尾进行常量时间迭代是什么意思,请告诉我,我将用示例说明。

1
  1. 将所有有趣的组合构建成一棵树,使得有趣的组合导致叶子节点,而不有趣的则不是。首先排序,以便对应于早期符号的边更靠近根。

  2. 读取前五个元素,并增加相应的频率计数器,以记录每个元素出现的次数。

  3. 通过根据频率计数器遍历树来检查最多五个值的子集。如果到达叶子节点,则发出当前匹配。

  4. 为了滑动窗口,减少与当前最左侧有趣符号相关联的计数器,并增加向右滑动后吸入的有趣符号的计数器。

示例1:

AAC, ABC => (-)        [B A x x C] x x x A A x x x C
             |         f[A] = 1, f[B] = 1, f[C] = 1
             A         A->B->C, emit ABC
             |
            (-)        B [A x x C x] x x A A x x x C
            / \        f[B]--; A->x; continue
           A   B       
           |   |       B A x x [C x x x A] A x x x C
          (-) (-)      f[A]--; f[A]++; A->x; continue
           |   | 
           C   C       B A x x C x x x [A A x x x] C
           |   |       f[C]--; f[A]++; A->A->x; continue
          (+) (+)

                       B A x x C x x x A [A x x x C]
                       f[A]--; f[C]++; A->x; done

例子 #2:
AAC => (-)             [C x x A A] x x C
        |              f[A]=2, f[B]=0, f[C]=1
        A              A->A->C, emit AAC; continue
        |
       (-)             C x x [A A x x C]
        |              f[C]--; f[C]++; A->A->C; emit AAC; done
        A
        |
       (-)
        |
        C
        |
       (+)

这个解决方案应该可以适用于不同的窗口大小,甚至可以通过将内部节点标记为匹配(而不仅仅是叶子节点)来允许有趣的不同大小的组合。这将在输入流中的元素数量方面具有线性时间和空间复杂度,尽管它将需要一些额外的内存,以便记录有趣的组合和窗口大小的数量。具体的时间/空间分析留作练习。


据我从评论中了解,5:AB [AAxBB] 应该计为 4。事实上,我认为 4:AB [AAxBB] 也应该计为 4。我不认为你的算法能够做到这一点... - RBarryYoung
但我认为可以通过添加搜索模式的频率计数,并在找到树匹配后,在窗口内取每个(字符搜索计数+1) - (字符发现计数)的乘积来进行调整。 - RBarryYoung
@RBarryYoung 是示例中的一个错误吗?你有反例吗?我不确定我错过了什么。 - Patrick87
1
@RBarryYoung 啊等等,也许我明白了。[AAxBB]算作四对,而不是两对。是的,也许这可以调整为可能多次遍历树...但这会损害性能,也许很严重。 - Patrick87
@RBarryYoung也许你说得对,这种想法似乎是有道理的... - Patrick87

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接