Loading

扩展 KMP

简介

对于一个长度为 \(n\) 的字符串 \(s\),定义函数 \(z[i]\) 表示 \(s\)\(s[i,n-1]\) (即以 \(s[i]\) 开头的后缀)的最长公共前缀(LCP)的长度。\(z\) 被称作为 \(s\)Z 函数。特别地,\(z[0]=[0]\)

国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP

样例

下面若干样例展示了对于不同字符串的 Z 函数:

  • \(z(\text{aaaaa})=[0,4,3,2,1]\)
  • \(z(\text{aaabaab})=[0,2,1,0,2,1,0]\)
  • \(z(\text{abacaba})=[0,0,1,0,3,0,1]\)

线性算法

在该算法中,我们从 \(1\)\(n−1\) 顺次计算 \(z[i]\) 的值\((z[0]=0)\)。在计算 \(z[i]\) 的过程中,我们会利用已经计算好的 \(z[0],…,z[i−1]\)

对于 \(i\),我们称区间 \([i,i+z[i]−1]\)\(i\)匹配段,也可以叫 Z-box。

举例:

对于 \(z(\text{abacaba})=[0,0,1,0,3,0,1]:\)

  • \(i=2\) 时,匹配段为 \([2,2]\),即字符 a
  • \(i=4\) 时,匹配段为 \([4,6]\),即子串 abc

算法的过程中我们维护右端点最靠右的匹配段。为了方便,记作 \([l,r]\)。根据定义,\(s[l,r]\)\(s\) 的前缀。在计算 \(z[i]\) 时我们保证 \(l≤i\)。初始时 \(l=r=0\)

在计算 \(z[i]\) 的过程中:

  • 如果 \(i≤r\),那么根据 \([l,r]\) 的定义有 \(s[i,r]=s[i−l,r−l]\),因此 \(z[i]≥min(z[i−l],r−i+1)\)。这时:
    • \(z[i−l]<r−i+1\),则 \(z[i]=z[i−l]\)
    • 否则 \(z[i−l]≥r−i+1\),这时我们令 \(z[i]=r−i+1\),然后暴力枚举下一个字符扩展 \([i]\) 直到不能扩展为止。
  • 如果 \(i>r\),那么我们直接按照朴素算法,从 \(s[i]\) 开始比较,暴力求出 \(z[i]\)
  • 在求出 \(z[i]\) 后,如果 \(z[i]>r\),我们就需要更新 \([l,r]\),即令 \(l=i,r=i+z[i]−1\)

代码

设小串(模式串)为 \(B\),大串(文本串)为 \(A\)

本代码中 \(z[0]=|B|\)

\(z\) 数组:

void getZ()
{
    z[0] = lenb;
    int now = 0;
    while (now + 1 < lenb && B[now] == B[now + 1])
        now++;
    z[1] = now;
    int p0 = 1;
    for (int i = 2; i < lenb; ++i)
    {
        if (i + z[i - p0] < p0 + z[p0])
        {
            z[i] = z[i - p0]; //第一种情况
        }
        else
        {
            now = p0 + z[p0] - i;
            now = max(now, 0);
            while (now + i < lenb && B[now] == B[now + i])
                now++;
            z[i] = now;
            p0 = i;
        }
    }
}

\(B\)\(A\) 的每一个后缀的 LCP 长度数组:

void exkmp()
{
    int now = 0;
    while (now < lenb && now < lena && B[now] == A[now])
        now++;
    ext[0] = now;
    int p0 = 0;
    for (int i = 1; i < lena; ++i)
    {
        if (i + z[i - p0] < p0 + ext[p0])
            ext[i] = z[i - p0];
        else
        {
            now = p0 + ext[p0] - i;
            now = max(now, 0);
            while (now < lenb && now + i < lena && B[now] == A[now + i])
                now++;
            ext[i] = now;
            p0 = i;
        }
    }
}

复杂度分析

对于内层 while 循环,每次执行都会使得 \(r\) 向后移至少 \(1\) 位,而 \(r<n−1\),所以总共只会执行 \(n\) 次。

对于外层循环,只有一遍线性遍历。

总复杂度为 \(O(n)\)

posted @ 2021-06-30 10:38  EdisonBa  阅读(86)  评论(0)    收藏  举报