Z算法背后的直觉

时间:2016-01-10 15:23:45

标签: algorithm

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

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

"overdose#stackoverflow"

有很多代码实现和数学导向的算法解释,这里有一些好的:

http://www.geeksforgeeks.org/z-algorithm-linear-time-pattern-searching-algorithm/ http://codeforces.com/blog/entry/3107

我可以看到 如何运作,但我不明白为什么。它看起来几乎像黑魔法。我有一个非常强烈的直觉,这个任务应该采用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)

2 个答案:

答案 0 :(得分:2)

我也不觉得它完全直观,所以我认为我有资格回答。否则我只是说你不懂,因为你是个白痴,当然这不是你希望的答案: - )

案例(引证解释):

Correctness is inherent in the algorithm and is pretty intuitively clear.

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

首先,我猜想O(n ^ 2)的常见直觉是这样的:对于一个长度为N的字符串,如果你被丢弃在字符串中的随机位置我没有其他信息,你有匹配x(< N)个字符以计算Z [i]。如果你被摔了N次,你必须做N(N-1)次测试,所以这是O(n ^ 2)。

然而,Z算法充分利用了您从过去的计算中获得的信息。

让我们看看。

首先,只要你没有匹配(Z [i] = 0),你就会沿着字符串前进,每个字符进行一次比较,这样就是O(N)。 其次,当你找到一个匹配的范围(在索引i处)时,诀窍是使用先前的Z [0 ... i-1]使用巧妙的推论来计算该范围内的所有Z值 in恒定时间,在该范围内没有其他比较 。接下来的比赛只会在范围的右边进行。

无论如何,这就是我的理解,希望这会有所帮助。

答案 1 :(得分:0)

我一直在寻找对该算法的更深入的了解,因此我发现了这个问题。

最初我并不了解codeforces post,但是后来我发现它足以理解,并且我注意到该帖子并不完全准确,并且省略了思考过程中的一些步骤,使之成为可能。有点混乱。

让我尝试更正该帖子中的不准确性,并阐明一些我认为可以帮助人们将点连接起来的步骤。在这个过程中,我希望我们可以从原作者那里学到一些直觉。在解释中,我将引用一些来自Codeforce的引用块和我自己的注释,以便使原始帖子更接近我们的讨论。

Z算法的起始地址为:

  

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

这很简单直接。

  

现在,假设我们对i-have1有正确的间隔[L,R],所有Z值直到i-1,我们将通过以下步骤计算Z [i]和新的[L,R] :

     
      
  • 如果i> R,则不存在S的前缀子串,该前缀子串在i之前开始,在i或之后结束。如果存在这样的子串,则[L,R]将具有是该子字符串的间隔,而不是其当前值。因此,我们通过比较S [0 ...]与S [i ...]来“重置”并计算新的[L,R],并同时获得Z [i](Z [i] = R-L + 1)。
  •   

项目符号中的粗体部分可能会造成混淆,但是如果您阅读两次,实际上只是在重复 R 的定义。

  
      
  • 否则,i≤R,因此电流[L,R]至少延伸到i。令k = i-L.我们知道Z [i]≥min(Z [k],R-i + 1),因为 S [i ...]至少匹配S [k ...] R-i + 1个字符(它们位于[L,R]间隔中,我们知道这是一个前缀子字符串)。现在,我们还有更多情况需要考虑。
  •   

粗体部分不是完全准确的,因为R -ii + 1可以大于Z [k],在这种情况下Z [i]为Z [k]。

现在让我们重点关注键: Z [i]≥min(Z [k],R-i + 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];
  • 结合以上方程,我们知道S [0 ... x] == S [L ... L + x] == S [k ... k + x] == S [i ... i + x],当x

这些是我一开始提到的缺失点,它们解释了第二和第三个要点,部分解释了最后一个要点。当我阅读codeforces帖子时,这并非一帆风顺。对我来说,这是该算法最重要的部分。

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

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

正如Ilya回答的那样,此算法的直觉是仔细重用我们到目前为止收集的每条信息。我只是用另一种方式解释了。希望对您有所帮助。