给定两个序列,找到一个结束点与另一个开始点之间的最大重叠


11

我需要找到一个有效的(伪)代码来解决以下问题:

鉴于(不一定是不同的)整数两个序列(a[1], a[2], ..., a[n])(b[1], b[2], ..., b[n]),找到最大d,从而a[n-d+1] == b[1]a[n-d+2] == b[2]......,和a[n] == b[d]

这不是家庭作业,实际上是在尝试使两个张量沿尽可能多的维度收缩时想到的。我怀疑存在一种有效的算法(也许O(n)?),但是我无法提出不存在的算法O(n^2)。该O(n^2)方法是在明显的循环d,然后在项目内部循环来检查所需要的状态,直到打到最大d。但是我怀疑有比这更好的事情。


如果可以为数组中的对象组计算滚动哈希,那么我认为这样做可以更高效。计算元素的哈希值b[1] to b[d],然后转到数组a计算哈希值(a[1] to a[d]如果匹配),这就是您的答案;如果不计算哈希值,则可以a[2] to a[d+1]通过重新使用为计算得出的哈希值a[1] to a[d]。但是我不知道数组中的对象是否适合在其上计算滚动哈希。
SomeDude

2
@becko对不起,我想我终于了解您要完成的工作。就是找到的结尾a与的开头之间的最大重叠b这样
user3386109

1
在我看来,问题出在字符串匹配上,这可以通过使用Knuth-Morris-Pratt算法来解决。运行时间为O(m + n),其中m是中的元素数an是中的元素数b。不幸的是,我在KMP方面没有足够的经验来告诉您如何适应它。
user3386109

1
@ user3386109我的解决方案也是字符串匹配算法Rabin-Karp的一种变体,它使用Horner的方法作为哈希函数。
丹尼尔

1
@Daniel啊,我知道我曾经在某个地方看到过滚动的哈希,但不记得在哪里:)
user3386109

Answers:


5

您可以利用z算法(一种线性时间(O(n))算法):

给定长度为n 的字符串S,Z算法会生成数组Z ,其中Z [i]是从S [i]开始的最长子字符串的长度,该字符串 也是S的前缀

您需要连接数组(b + a)并在生成的构造数组上运行算法,直到第一个i使得Z [i] + i == m + n

例如,对于a = [1、2、3、6、2、3]和b = [2、3、6、2、1、0],串联将为[2、3、6、2、1 ,0、1、2、3、6、2、3],这将产生Z [10] = 2,满足Z [i] + i = 12 = m + n


美丽!谢谢。
becko

3

对于O(n)的时间/空间复杂度,诀窍是评估每个子序列的哈希值。考虑数组b

[b1 b2 b3 ... bn]

使用Horner的方法,您可以评估每个子序列的所有可能哈希值。选择一个基值B(大于两个数组中的任何值):

from b1 to b1 = b1 * B^1
from b1 to b2 = b1 * B^1 + b2 * B^2
from b1 to b3 = b1 * B^1 + b2 * B^2 + b3 * B^3
...
from b1 to bn = b1 * B^1 + b2 * B^2 + b3 * B^3 + ... + bn * B^n

请注意,您可以使用前一个序列的结果来评估O(1)时间中的每个序列,因此所有作业成本为O(n)。

现在您有了一个数组Hb = [h(b1), h(b2), ... , h(bn)]Hb[i]b1到的哈希值在哪里bi

对数组执行相同的操作a,但有一点技巧:

from an to an   =  (an   * B^1)
from an-1 to an =  (an-1 * B^1) + (an * B^2)
from an-2 to an =  (an-2 * B^1) + (an-1 * B^2) + (an * B^3)
...
from a1 to an   =  (a1   * B^1) + (a2 * B^2)   + (a3 * B^3) + ... + (an * B^n)

必须注意的是,当您从一个序列移至另一个序列时,请将整个先前序列乘以B,然后将新值乘以B。例如:

from an to an =    (an   * B^1)

for the next sequence, multiply the previous by B: (an * B^1) * B = (an * B^2)
now sum with the new value multiplied by B: (an-1 * B^1) + (an * B^2) 
hence:

from an-1 to an =  (an-1 * B^1) + (an * B^2)

现在您有了一个数组Ha = [h(an), h(an-1), ... , h(a1)]Ha[i]ai到的哈希值在哪里an

现在,您可以比较从n到1的Ha[d] == Hb[d]所有d值,如果它们匹配,您将得到答案。


注意:这是一个哈希方法,值可能很大,您可能必须使用快速取幂方法和模块化算术,这可能(几乎)给您带来冲突,从而使此方法并不完全安全。一个好的做法是选择一个底数B作为一个非常大的素数(至少大于数组中的最大值)。您还应该小心,因为数字的限制可能会在每个步骤中溢出,因此您必须K在每个操作中使用(取模)(其中K可能比素数大B)。

这意味着两个不同的序列可能具有相同的哈希,但是两个相等的序列将始终具有相同的哈希。


您可以通过评估资源需求来开始这个答案吗?
灰胡子

2

实际上,这可以在线性时间,O(n)O(n)额外空间中完成。我将假设输入数组是字符串,但这不是必需的。

幼稚方法将-匹配后ķ字符相等-发现不匹配的字符,并返回k-1个单元中一个,重置索引中b,然后从那里开始的匹配处理。这显然代表了O(n²)最坏的情况。

为避免此回溯过程,我们可以观察到,如果在扫描最后k-1个字符时未遇到b [0]字符,则返回没有用。如果确实找到了该字符,则只有在该k大小的子字符串中有一个周期性重复时,回溯到该位置才有用。

例如,如果我们在a的某处查看子字符串“ abcabc” ,而b是“ abcabd”,并且发现b的最后一个字符不匹配,则必须考虑成功的匹配可能始于第二个“ a”在子字符串中,我们应该在继续比较之前将当前索引b相应地移回。

我们的想法是,然后做一些基于字符串预处理b重新登录引用在b当出现不匹配是检查有用。因此,例如,如果b为“ acaacaacd”,我们可以识别出这些基于0的反向引用(放在每个字符下方):

index: 0 1 2 3 4 5 6 7 8
b:     a c a a c a a c d
ref:   0 0 0 1 0 0 1 0 5

例如,如果我们有一个等于“acaacaaca”第一个不匹配发生在最后一个字符。由于“ acaac”是常见的,因此上述信息然后告诉算法在b中返回索引5。然后,只有在改变当前指数b,我们可以在当前指数继续匹配的一个。在此示例中,最后一个字符的匹配成功。

有了这个,我们可以优化搜索,并确保在索引一个可以随时向前进步。

这是JavaScript中该想法的实现,仅使用该语言的最基本语法:

function overlapCount(a, b) {
    // Deal with cases where the strings differ in length
    let startA = 0;
    if (a.length > b.length) startA = a.length - b.length;
    let endB = b.length;
    if (a.length < b.length) endB = a.length;
    // Create a back-reference for each index
    //   that should be followed in case of a mismatch.
    //   We only need B to make these references:
    let map = Array(endB);
    let k = 0; // Index that lags behind j
    map[0] = 0;
    for (let j = 1; j < endB; j++) {
        if (b[j] == b[k]) {
            map[j] = map[k]; // skip over the same character (optional optimisation)
        } else {
            map[j] = k;
        }
        while (k > 0 && b[j] != b[k]) k = map[k]; 
        if (b[j] == b[k]) k++;
    }
    // Phase 2: use these references while iterating over A
    k = 0;
    for (let i = startA; i < a.length; i++) {
        while (k > 0 && a[i] != b[k]) k = map[k];
        if (a[i] == b[k]) k++;
    }
    return k;
}

console.log(overlapCount("ababaaaabaabab", "abaababaaz")); // 7

尽管存在嵌套while循环,但这些循环的合计次数不超过n。这是因为k的值在while体内严格减小,并且不能变为负值。只有在k++执行多次以提供足够的空间进行这种减少时,才会发生这种情况。因此,总而言之,执行while身体的次数不能超过k++执行的次数,而后者显然为O(n)。

要完成此操作,您可以在此处找到与上面相同的代码,但是在一个交互式代码段中:您可以输入自己的字符串并以交互方式查看结果:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.