Algorithm Z算法背后的直觉

Algorithm Z算法背后的直觉,algorithm,Algorithm,Z算法是一种具有O(n)复杂度的字符串匹配算法 一个用例是从字符串B中查找字符串A的最长出现时间。例如,从“stackoverflow”中查找“overdose”的最长出现时间将是“over”。您可以通过使用组合字符串调用Z算法来发现这一点。“overdose#stackoverflow”(其中#是两个字符串中都不存在的字符)。然后,Z算法将尝试将组合字符串与自身匹配,并创建一个数组Z[],其中Z[i]给出从索引i开始的最长匹配长度。在我们的例子中: index 0 1 2 3 4

Z算法是一种具有O(n)复杂度的字符串匹配算法

一个用例是从字符串B中查找字符串A的最长出现时间。例如,从
“stackoverflow”
中查找
“overdose”
的最长出现时间将是
“over”
。您可以通过使用组合字符串调用Z算法来发现这一点。
“overdose#stackoverflow”
(其中#是两个字符串中都不存在的字符)。然后,Z算法将尝试将组合字符串与自身匹配,并创建一个数组Z[],其中Z[i]给出从索引i开始的最长匹配长度。在我们的例子中:

index  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
string o  v  e  r  d  o  s  e  #  s  t  a  c  k  o  v  e  r  f  l  o  w
z    (21) 0  0  0  0  1  0  0  0  0  0  0  0  0  4  0  0  0  0  0  1  0
该算法有很多代码实现和面向数学的解释,以下是一些很好的解释:


我知道它是怎么工作的,但我不明白为什么。这几乎像是黑魔法。我有一个非常强烈的直觉,这项任务应该采取
O(n^2)
,但这里有一个算法,它在
O(n)
中完成,我也不觉得它完全直观,所以我认为我有资格回答。否则我只能说你不明白,因为你是个白痴,这肯定不是你所希望的答案:-)

举例说明(从解释中引用):

所以,让我们试着更加直观

首先,我猜想O(n^2)的常见直觉是:对于长度为n的字符串,如果在没有其他信息的情况下,将其放置在字符串中的随机位置I,则必须匹配x( 然而,Z算法充分利用了您从过去的计算中获得的信息

让我看看

首先,只要没有匹配项(Z[i]=0),就可以沿着字符串前进,每个字符进行一次比较,这就是O(N)。 其次,当你找到一个匹配的范围时(在索引i处),诀窍是使用之前的Z[0…i-1]进行巧妙的演绎,以恒定时间计算该范围内的所有Z值,而不在该范围内进行其他比较。接下来的匹配将仅在范围的右侧进行


这就是我对它的理解,希望这能有所帮助。

我想对这个算法有更深入的理解,因此我发现了这个问题

起初我不理解,但后来我发现这足以让我理解,我注意到这篇文章并不完全准确,而且它省略了思考过程中的一些步骤,让人有点困惑

让我试着纠正那篇文章中的错误,并澄清一些我认为可以帮助人们将点与线联系起来的步骤。在这个过程中,我希望我们能从原作者那里学到一些直觉。在解释中,我将混合一些引用自codeforces的块和我自己的注释,这样我们就可以将原始帖子与我们的讨论保持接近

Z算法开始时为:

当我们迭代字符串中的字母时(索引i从1到n - 1) ,我们保持一个间隔[L, R] 这是最大R的间隔,使得1 ≤ L ≤ 我 ≤ R和S[L…R]是一个前缀子字符串(如果不存在这样的间隔,就让L = R =  - 1). 因为我 = 1,我们可以通过比较S[0…]和S[1…]来简单地计算L和R。此外,在这个过程中,我们也得到了Z

这是简单明了的

现在假设我们有正确的间隔[L, R] 因为我 - 1和i之前的所有Z值 - 1.我们将计算Z[i]和新的[L, R] 通过以下步骤:

  • 如果我 > R、 那么就不存在在i之前开始、在i或之后结束的S前缀子串。如果存在这样的子串,[L, R] 将是该子字符串的间隔,而不是其当前值。因此我们“重置”并计算一个新的[L, R] 通过比较S[0…]和S[i…],同时得到Z[i](Z[i] = R - L + 1)
要点中的粗体部分可能令人困惑,但如果你读两遍,它实际上只是重复了R的定义

  • 否则,我 ≤ R、 所以现在的[L, R] 至少延伸到i。让k = 我 - L.我们知道Z[i] ≥ min(Z[k], R - 我 + 1) 因为S[i..]与S[k..]至少匹配R - 我 + 1个字符(它们位于[L, R] 我们知道是前缀子字符串的间隔)。现在我们有更多的案例要考虑。
粗体部分并不完全准确,因为R - 我 + 1可以大于Z[k],在这种情况下,Z[i]将是Z[k]

现在让我们关注关键点:Z[i] ≥ min(Z[k], R - 我 + 1) 。为什么这是真的?由于以下原因:

  • 基于区间[L,R]和i的定义 ≤ R、 我们已经确认S[0…R-L]==S[L…R],因此S[0…k]==S[L…i]和S[k…R-L]==S[i…R]
  • 假设Z[k]=x,根据Z的定义,我们知道S[0…x]==S[k…k+x]
  • 结合以上方程,我们知道当x
这些是我在开头提到的缺失点,它们解释了第二个和第三个要点,部分解释了最后一个要点。当我读到codeforces的帖子时,这并不简单。对我来说,这是这个算法最重要的部分

对于最后一个项目符号,如果Z[k] ≥ R - 我 + 1,我们将刷新[L,R],使用i作为新的L,并将R扩展到更大的R'

在整个过程中,Z算法只使用每个字符一次进行比较,因此时间复杂度为O(n)

正如伊利亚所回答的那样,这个算法的直觉是
Correctness is inherent in the algorithm and is pretty intuitively clear.