求前缀函数的线性算法(KMP)

我们定义的所有字符串都是以下标 \(0\) 开头的。

首先定义字符串 \(p\),长度为 \(k\),其第 \(i+1\) 位字符为 \(p_i\),以 \(p_i\) 为结尾字符的长度为 \(i+1\) 的前缀为 \(t_i\).

定义 \(p\) 的前缀函数 \(\pi_i\)\(\pi_i\)\(t_i\) 的最长的、对应一个与之相同的 \(t_i\) 的真后缀的真前缀的长度

我们可以朴素地计算 \(pi\)

for(int i=1;i<k;i++) {
  for(;pi[i]<i+1;pi[i]++) { // 枚举长度
    if(!str_same(p,p+j+1,p+i-j+1,p+i)) break;
  }
}

这样的时间复杂度是 \(\Theta(n_2)\)\(\Theta(n_3)\) 的,取决于 str_same 是哈希实现的还是朴素实现的。

不难发现,枚举长度的时候,我们有很多冗余的字符串比对。如果我们已经知道了 \(\pi_{i-1}\),那么 \(t_{\pi_{i-1}-1} = p[i-\pi_{i-1}+1...i-1]\) 是显然的。

并且基于定义,必然有 \(\pi_i \le \pi_{i-1}+1\). 这在 \(p_i = p_{\pi_{i-1}}\) 时候取到。

我们只需要知道 \(p_i \ne p_{\pi_{i-1}}\) 时,\(\pi_i\) 的取值就可以去掉字符串比对了。

观察,我们实际上需要找的就是这个更短的绿框,满足灰色的圆角方形等于黑色的圆角方形。不难发现它的长度就等于 \(\pi_{\pi{i-1}}\),由定义。(把后面的红框和绿框平移到左边红框,发现两个绿框代表了相等的、红色框的真前缀和真后缀)

所以我们能写出以下线性(并不显然)算法:

for(int i=1;i<k;i++) {
  pi[i] = pi[i-1];
  while(1) {
    if(p[pi[i]] == p[i]) break;
    if(pi[i] == 0) break;
    pi[i] = pi[pi[i]-1];
  }
}
posted @ 2025-09-07 19:25  奇怪的知识增加了!  阅读(16)  评论(0)    收藏  举报