[luogu p5410] 【模板】扩展 KMP(Z 函数) & 扩展 KMP(Z 函数)学习笔记

Z函数 & exKMP

P5410 【模板】扩展 KMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

我的 zz 函数和流行版本不太相同,相对而言,更短小精悍且易懂

概念与约定

本文字符串从 11 开始编号。

下文中有些概念是我的自定义概念。

字符串的第 ii 个后缀:从第 ii 个字符开始一直到末尾形成的子字符串。

定义 z(i)z(i) 表示字符串与它第 ii 个字符开始的后缀的最大公共前缀长度。 考虑到 z(1)z(1) 的特殊性,其值可以为 00,也可以为 S|S|,本文的算法中需要令 z(1)=0z(1) = 0.

[l,r][l, r] 表示字符串从第 ll 个字符到第 rr 个字符连接形成的子串;[x][x] 表示字符串的第 xx 个字符。

zz 函数算法可以做到在 O(S)\operatorname{O}(|S|) 复杂度内完成 z(i)(2iS)z(i)(2 \le i \le |S|) 的求解。

算法讲解

zz 函数

既然 zz 函数是 exKMP 的核心部分,自然和 KMP 的核心部分——求 π\pi 数组(别名很多,failnxt、……)相似,都是从向后递推,用前面的 zz 函数值推导后面的 zz 函数值。

现在假设我们要求 z(x2)z(x \ge 2),自然地,z(1)z(x1)z(1) \cdots z(x-1)zz 函数值都已知了。我们随便取一个 1k<x1 \le k < x,记 p=k+z[k]1p = k + z[k] - 1

根据定义可以得到 [k,p]=[1,z(k)][k, p] = [1, z(k)],如下图蓝色部分所示:

当然了,x>kx > k,因此 xx 应该表示在 kk 的右边,现在我们来表示一下它。我们知道,两个蓝色区域是完全相同的,因此两个蓝色串其中,相对位置一样的子串,也应该完全相同,于是有 [x,p]=[xk+1,z(k)][x, p] = [x - k + 1, z(k)],如下图橙色部分所示:

对了,那有些人会问了,xx 会不会超过 pp 啊?这点我们最后再提,先看现在 xx 比较正常,在第二块蓝色色块上的图:

l=z(xk+1)l = z(x - k + 1),可以得到 [xk+1,xk+l]=[1,l]=[x,x+l1][x-k+1, x-k+l] = [1, l]=[x, x+l-1],先上图再解释:

上图的紫色部分就是刚刚说的三个相等的子串。其中:

[xk+1,xk+l][x-k +1, x - k + l][1,l][1, l],即前两个紫色块,他们完全相同的原因是 zz 函数的定义;

[xk+1,xk+l][x-k+1, x-k+l][x,x+l1][x, x+l-1],即后两个紫色块,他们完全相同的原因:这两个紫色块是原来的两个橙色块中相对位置相同的子串。请注意这里相同的原因。

综上可以得到三个紫色块完全相同。

好的,观察这个图片,已经可以得到结论:z(x)=l=z(xk+1)z(x) = l = z(x - k + 1),接下来开始解释。

我们考虑上面三根紫线分别紧挨着的后面的字符,它们分别是:[l+1][l + 1][xk+l+1][x - k + l + 1][x+l][x + l]

首先可以证明:[l+1][l + 1][xk+l+1][x - k + l +1] 不同。因为如果他们俩相同,那么根据 zz 函数的定义,代表紫色区块的 z(xk+1)z(x - k +1) 绝对还可以再扩大,而事实上没有。

其次可以看出:[xk+l+1][x - k + l + 1][x+l][x + l] 相同,因为它们是原来两个橙色块中相对位置相同的字符。同时也请注意这里相同的原因

因此可以得到:[l+1][l +1][x+l][x + l] 不同

于是就能证明:原串和 [x,S][x, |S|] 的最长公共前缀也是紫色部分。因为紧接着接下来那个字符就不同了嘛。

因此答案是 z(x)=l=z(xk+1)z(x) = l = z(x - k + 1).

那么结束了吗?其实并没有。因为上述的图中我们可以得到这样一个结论,需要一个重要的地方做支撑:ll严格小于原来的橙色块的长度 k+z[k]xk + z[k] - x,也就是紫色块不可以淹没橙色块。为什么?让我来看一下这种紫色淹没橙色的情况(我规定刚刚那种是第一种情况,现在这种是第二种):

不难发现这时,紫色完全淹没了橙色。与第一种情况不同,此处箭头指向的三个区域中,前两个箭头所指区域仍然保证相同,但和三个区域不再保证相同。前两块相同仍然因为 zz 函数的定义,这点无影响;而原先关于第二个区域和第三个区域相同的证明此处不再成立,因为原先的证明依赖【这两块是原橙色部分的相同位置子串】,而此处这两块因为逾越了橙色块,所以不是。

因此我们现在只能下结论说 z(x)k+z(k)xz(x) \ge k + z(k) - xk+z(k)xk + z(k) - x 即上面三个紫色块的长度。为了确定 z(x)z(x) 的具体值,我们需要从 [k+z(k)x+1][k + z(k) - x + 1](第一个紫色块右方)和 [p+1=k+z(k)][p +1 = k + z(k)](第三个紫色块右方)分别维护两个指针,同步向右走,暴力判断字符是否相等,给 z(x)z(x) 暴力地不断自增 11

总结一下:

  • z(xk+1)<k+z(k)xz(x - k + 1) < k + z(k) - x 时,z(x)=z(xk+1)z(x) = z(x - k + 1)
  • 否则,初始令 z(x)=k+z(k)xz(x) = k + z(k) - x,然后暴力判断字符累加 z(x)z(x)

恭喜你,已经学会了 zz 函数,但是或许你会有疑虑,这样的做法真的是线性吗?事实上,只要我们改动一下,不随便地选取 kk,而是令 kk 初始时为 11,然后每次在第二种情况后kxk \gets x 就可以得到线性复杂度了(第一种情况后 kk 不变)。这里就是和网上的流行版本不同的地方了,网上流行版本需要将 kk 设置为 k+z[k]1k + z[k] - 1 最大的那个,事实上不太直观,不易于理解而且相对麻烦。

第一种情况显然是 O(1)\mathcal{O}(1),而此时,第二种情况均摊后也是线性的,原因是,观察到第二种情况中我们暴力判断字符的过程,右侧那个字符始终从 [p+1][p + 1] 开始向右判断,最终结束后,pp 又会被赋为当前的 x+z(x)1x + z(x) - 1,所以放眼全局,我们暴力判断的右侧那个字符从左到右扫过一次,换句话说,我们全局上最多只会暴力判断 S|S| 次。所以均摊也是线性的。

最后来到了喜闻乐见的细节:xx 会不会超过 pp 呢。其实是有可能的,这时候一定走第二种情况。细节问题在于此时算出的 z(x)z(x) 最小值等于 px+1p - x + 1,由于 xx 超过 pp 甚至可以算出负数,这肯定不是我们想要的。因此在 z(x)px+1z(x) \leftarrow p - x + 1 时,要注意判断如果 z(x)<0z(x) < 0,要使 z(x)0z(x) \gets 0.

代码(事实上没有注释后相当短小好记):

void get_z(char *P) {
    int m = strlen(P) - 1; // 注意,这里的 m 其实就是字符串 P 的长度,只是我从 1 开始,因此 strlen 得到的长度会多 1
    for (int i = 2, k = 1; i <= m; ++i) {
        if (k + z[k] - i <= z[i - k + 1]) { // 第二种情况
        // 相当于 z[i - k + 1] >= k + z[k] - i
        // 也就是补齐后的紫色大于等于橙色时,进入第二种情况
            z[i] = k + z[k] - i; // 将z[i] 调到 k + z[k] - i,这是它的最小值
            if (z[i] < 0) // 判断,防止负数出现
                z[i] = 0;
            while (i + z[i] <= m && P[z[i] + 1] == P[i + z[i]])
                ++z[i]; // 暴力枚举,判断字符相等
            k = i; // 直接令 k = i
        } else
            z[i] = z[i - k + 1]; // 第一种情况
    }
    z[1] = m; // 题目中要求 z[1] = m,在算法处理后赋值
    return ;
}

exKMP

exKMP 也就是求模式串 PP 在文本串 TT 的每个后缀中的最长公共前缀,其实原理本质还是 zz 函数。

一种做法:令 S=P+c+TS' = P + c + T,其中 ccPPTT 中均不含有的字符,然后对 SS' 进行 zz 函数,就是答案。具体正确性在此不证明(因为比较直观易懂),其实中间就是起到一个隔离符的作用,让 zz 函数求解算法不会将 PPTT 做拼在一起子串等的行为,导致错误结果。

还有一种做法(真正的exKMP),只需要在 zz 函数算法基础上,将几个 zz 替换为 ee,再把字符串分裂开:

需要提前预处理出 PPzz 函数。

代码:

void exkmp(char *T, char *P) {
    int n = strlen(T) - 1, m = strlen(P) - 1; // 同上,注意这里 n 和 m 就是分别实际的文本串和模式串的长度
    for (int i = 1, k = 0; i <= n; ++i) { // 注意哪些地方换成了 p(也就是刚刚我说的 e),哪些还是原来的 z
        // 这里让 k = 0,一定会走到第二种情况暴力判断,然后 k = 1 就正常了
        if (k + p[k] - i <= z[i - k + 1]) {
            p[i] = k + p[k] - i; 
            if (p[i] < 0)
                p[i] = 0;
            while (i + p[i] <= n && p[i] < m && P[p[i] + 1] == T[i + p[i]])
                ++p[i];
            k = i;
        } else
            p[i] = z[i - k + 1];
    }
    return ;
}

后话

洛谷该题目模板中,最高赞的一篇题解是 George1123 写的 zz 函数,纯推的式子非常易懂。唯独可惜的是中间推理,红色括号中的条件是错误的(多了个等号);同时最后关于线性复杂度的证明其实有点问题,或者没说明白为什么每个字符不会被暴力判断多次(因为 zz 函数中有两种情况)。其他题解的问题就更多了,所以我就想写一篇这样详尽的题解,尽我可能做到最好地达到讲授传达到的目的。

模板洛谷 P5410。

谢谢。

posted @   dbxxx  阅读(305)  评论(5编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示