字符串(KMP系列)

· · 算法·理论

一. KMP

1. border

如果一个字符串的前缀等于他的后缀,我们称这个字符串是原字符串的border。
例如,abcakmeabc 中,abc 就是一个border。

2. KMP

定义一个字符串的 nxt 数组表示:前 i 个字符的最长border长度。
那怎么求一个字符的nxt呢?代码如下:

void kmp(int n,string b)
{
      int j=0;
      for(int i=2;i<=n;i++)
      {
          while(j&&b[i]!=b[j+1]) j=nxt[j];
          if(b[j+1]==b[i]) j++;
          nxt[i]=j;
      }
}

时间复杂度 O(n)
KMP经常用来求解模式串匹配的问题。即询问 st 中出现的次数。一般来说,代码如下:

j=0;
for(int i=1;i<=m;i++)
{
        while(j>0&&b[j+1]!=a[i])
            j=nxt[j];
        if(b[j+1]==a[i]) 
            j++;
        if(j==n)
        {
            cout<<i-n+1<<endl;
            j=nxt[j];
        }
}

其中,m=|t|,n=|s|

原因自己上题解搜。

二. exKMP/扩展KMP/Z函数

1. 什么是 Z 函数

注:字符串从 0 开始编号。

什么是 \text Z 函数呢?我们定义 z_i 表示 ss 的每一个后缀 i\sim n-1 的最长公共前缀(LCP)的长度,此时我们称 zs\text Z 函数。特殊的,\mathbf{z[0]=0}

举几个栗子:

### 2. Z 函数线性求法 就像 $nxt$ 一样,Z 函数也可以在 $O(n)$ 复杂度中求。 首先假设已知 $z_0,z_1,\cdots,z_{i-1}$,现在要求 $z_i$。 如果 $z_i$ 被求出来了,那么 $s[i,i+z_i-1]$ 一定是 $s$ 的前缀,且 $s[i,i+z_i]$ 一定**不**是 $s$ 的前缀。那么现在正在处理 $z_i$,区间 $s[i,i+z_i-1]$ 就称为我们的**匹配段**,简称 Z-box。 由于 $\LaTeX$ 太难敲了,所以我们简称 $[l,r]$ 是我们的匹配段。由于左端点已知,我们只需要扩展右端点。根据定义,$[l,r]$ 是 $s$ 的前缀。我们维护的时候 $l\le i$,初始 $l=r=0$。 分两种情况讨论 - 当前处理的 $i$ 在 $r$ 的左边($i\le r$):由于是匹配段,那么同一长度的 $s$ 肯定是相等的!(因为他们都是 $s$ 的前缀,长度又相等,必然相同)就有性质:$s[i,r]=s[i-l,r-l]$。所以 $z_i=z_{i-l}$?别忘了,$z[i]$ 的长度一定不超过 $r-i+1$(隔着超过字符串长度了?)所以 $z_i\ge\min\{z_{i-l},r-i+1\}$。 - 如果 $z_{i-l}<r-i+1$,那么 $z_i=z_{i-l}$。 - 如果 $z_{i-1}\ge r-i+1$,那么 $z_i=r-i+1$ 的基础上,暴力扩展 $z_i$。 - 如果 $i>r$,暴力扩展 最后,如果 $i+z_i-1>r$,则令 $l=i,r=i+z_i-1$ 即可。 ### 3. 代码 ```cpp for(int i=1;i<n;i++) { int len=r-i+1; if(i<=r&&z[i-l]<len) z[i]=z[i-l]; else { z[i]=max(0ll,len); while(i+z[i]<n&&a[z[i]]==b[i+z[i]]) z[i]++; } if(i+z[i]-1>r) l=i,r=i+z[i]-1; } ``` ### 4. 时空复杂度分析 容易发现,内层 $r$ 每次至少加一,外层 $i$ 每层加一。总的时间复杂度 $O(n)$。