【算法】KMP 与 Z 函数

news/2024/11/19 12:05:54/文章来源:https://www.cnblogs.com/Daniel-yao/p/18554369

1. KMP

1.1 算法简介

可以做到线性匹配的快速匹配字符串的算法,并可以维护字符串最长公共前后缀,扩展出计算字符串周期。

在 OI 界 KMP 算法是字符串板块中很经典的算法,可以扩展出很多巧妙的解题技巧。

1.2 算法流程

1.2.1 字符串匹配

考虑 \(O(n^2)\) 暴力的匹配,瓶颈在于每次匹配了很多重复却非法的字符导致效率很慢。

然后就是考虑如何优化,无非就是利用已经计算过的信息。

这里补充一个 最长公共前后缀 的概念:对于字符串 \(a\),最长公共前后缀的长度为满足 \(a[1,i]=a[n-i+1,n]\) 的最大的 \(i(i<n)\)。可以发现如果该定义包含本身则没有意义(长度一定为 \(n\))。所以需要保证最长公共前后缀小于本身的长度。

一个匹配下分 \(s\)(模式串)和 \(t\) (匹配串),串长分别为 \(n\)\(m\)。记 \(kmp_i (1\le i\le m)\) 表示前缀 \(t[1,i]\) 的最长公共前后缀。

假设已经计算出了 \(kmp_i\)。考虑如何匹配字符串。

s: caabaaabacb
t: aaba

此时 \(kmp:[0,1,0,1]\)

  • \(i=1,j=1\);此时两串匹配的为空串,\(i\to i+1\)
  • \(i=2,j=1\);此时两串可匹配成功,匹配了 aaba\(i\to i+1,j=kmp_{j}=kmp_{4}=1\)
  • \(i=3,j=2\);此时两串匹配了 a\(i\to i+1,j=kmp_{j}=kmp_{1}=0\)
  • \(i=4,j=1\);此时两串匹配的为空串,\(i\to i+1\)
  • \(i=5,j=1\);此时两串匹配了 aa\(i\to i+1,j=kmp_{j}=kmp_{2}=1\)
  • \(i=6,j=2\);此时两串可匹配成功,匹配了 aaba\(i\to i+1,j=kmp_{j}=kmp_{4}=1\)

如此匹配,我们便找到了所有的合法匹配位置,分别为 \(2,6\)

考虑分析时间复杂度,可以看成 \((i,j)\) 对齐,按位匹配。所以整个流程 \(t\) 串一直在往前移动,时间复杂度 \(O(n+m)\)

1.2.2 计算 kmp 数组

和匹配很像,相当于自己和自己匹配。所到之处的 \(j\) 记录为 \(kmp_i\) 即可。

不过更好的理解是用两个指针 \((i,j)\)。如果 \(s_i\)\(s_j\) 匹配,则将 \(j\) 指针后移,否则跳 \(kmp_j\)(可以保证 \(kmp_j\) 已经算出)。然后 \(kmp_i=j\)

这个过程一定也是 \(O(n)\) 的。分析与证明参考匹配。

1.3 算法实现

计算 \(kmp\) 数组:

for (int i = 2, j = 0; i <= n; i++) {while(j && t[j + 1] != t[i]) j = nx[j];if(t[j + 1] == t[i]) j++;nx[i] = j;
}

匹配:

for (int i = 1, j = 0; i <= n; i++) {while(j && t[j + 1] != s[i]) j = nx[j];if(t[j + 1] == s[i]) j++;if(j == m) {cout << i - m + 1 << '\n';}
}

然后是 P3375 【模板】KMP,就把她俩整合一下即可。

#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)using namespace std;const int N = 1e6 + 10;int n, m, nx[N];char s[N], t[N];signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);cin >> (s + 1) >> (t + 1);n = strlen(s + 1);m = strlen(t + 1);for (int i = 2, j = 0; i <= n; i++) {while(j && t[j + 1] != t[i]) j = nx[j];if(t[j + 1] == t[i]) j++;nx[i] = j;}for (int i = 1, j = 0; i <= n; i++) {while(j && t[j + 1] != s[i]) j = nx[j];if(t[j + 1] == s[i]) j++;if(j == m) {cout << i - m + 1 << '\n';}}For(i,1,m) cout << nx[i] << ' ';return 0;
}

1.4 扩展

1.4.1 字符串周期

UVA1328 Period

给定一个字符串 \(s\),判断其前缀 \(s[1,i]\) 是否为周期字符串,并求出其周期长度和循环节数量。

可以发现一些性质:如果对于 \(s[1,i]\)\(i \bmod (i-kmp_i) = 0\),则为周期字符串。

证明很简单。

image

按照这样的方式对于每一个小块染上不同的颜色。

image

这里红色段和黄色段相等。

image

这样每一个小段可以传递相等。这样就可以证明出其为周期字符串。

代码挂着
#include<bits/stdc++.h>
#define int long long
#define reg register 
#define For(i,l,r) for(reg int i=l;i<=r;++i)
#define FOR(i,r,l) for(reg int i=r;i>=l;--i)using namespace std;const int N = 1e6 + 10;int n, kmp[N], id;char s[N];signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);while(cin >> n && n != 0) {For(i,1,n) kmp[i] = 0;cin >> (s + 1);for (int i = 2, j = 0; i <= n; ++i) {while(j && s[i] != s[j + 1]) j = kmp[j];if(s[i] == s[j + 1]) j++;kmp[i] = j;}++id;cout << "Test case #" << id << '\n';For(i,1,n) {if(i % (i - kmp[i]) == 0 && kmp[i] != 0) {cout << i << ' ' << (i / (i - kmp[i])) << '\n';}} cout << '\n';}return 0;
}

2. Z 函数

2.1 算法流程

有一个这样的问题:

给定两个字符串 \(a,b\),你要求出两个数组:

  • \(b\)\(z\) 函数数组 \(z\),即 \(b\)\(b\) 的每一个后缀的 LCP 长度。
  • \(b\)\(a\) 的每一个后缀的 LCP 长度数组 \(p\)

对于第一个 subtask 所求的数组 \(z\),则为 Z 函数,可以用 扩展 KMP 算法(exKMP) 求得。

2.1.1 暴力求解

很好想,就拿一对指针 \((i,j)\) 去匹配,匹配记录,失配重置。

时间复杂度 \(O(n^2)\)

2.1.2 Z-box 引入

维护一个区间 \([l,r]\)\(l=i,r=i+z_i-1\),其中 \(r\) 为已知最大的合法右端点。

对于 \(i\le r\),可以分为 \(z_{i-l+1}<r-l+1\)\(z_{i-l+1}\ge r-l+1\) 两种情况。

  • \(z_{i-l+1}<r-l+1\);此时 \(z_i=z_{i-l+1}\)
  • \(z_{i-l+1}\ge r-l+1\);此时 \(z_i=r-i+1\),然后暴力匹配。

其他情况暴力匹配,然后更新 \([l,r]\) 即可。

时间复杂度 \(O(n)\),这样可以做到线性了。

可以发现 \(exKMP\)\(Manacher\) 的算法思想很像。

2.2 算法实现。

计算 \(Z\) 函数:

z[1] = m;
for (reg int i = 2, l, r = 0; i <= m; ++i) {if(i <= r) z[i] = min(z[i - l + 1], r - i + 1);while(i + z[i] <= m && t[1 + z[i]] == t[i + z[i]]) z[i]++;if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}

计算 \(s,t\) 后缀的公共最长前缀。

把匹配 \(s\) 的数组换成 \(p\),求法和 \(Z\) 函数的求法一样。

for (reg int i = 1, l, r = 0; i <= n; ++i) {if(i <= r) p[i] = min(z[i - l + 1], r - i + 1);while(1 + p[i] <= m && i + p[i] <= n && s[i + p[i]] == t[1 + p[i]]) p[i]++;if(i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}

然后是 【模板】扩展 KMP/exKMP(Z 函数)

#include<bits/stdc++.h>
#define int long long
#define reg register
#define For(i,l,r) for(reg int i=l;i<=r;++i)
#define FOR(i,r,l) for(reg int i=r;i>=l;--i)using namespace std;const int N = 2e7 + 10;int n, m, z[N], p[N], ans1, ans2;char s[N], t[N];signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);cin >> (s + 1) >> (t + 1);n = strlen(s + 1), m = strlen(t + 1);z[1] = m;for (reg int i = 2, l, r = 0; i <= m; ++i) {if(i <= r) z[i] = min(z[i - l + 1], r - i + 1);while(i + z[i] <= m && t[1 + z[i]] == t[i + z[i]]) z[i]++;if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;}for (reg int i = 1, l, r = 0; i <= n; ++i) {if(i <= r) p[i] = min(z[i - l + 1], r - i + 1);while(1 + p[i] <= m && i + p[i] <= n && s[i + p[i]] == t[1 + p[i]]) p[i]++;if(i + p[i] - 1 > r) l = i, r = i + p[i] - 1;}For(i,1,m) ans1 = (ans1 ^ (i * (z[i] + 1)));For(i,1,n) ans2 = (ans2 ^ (i * (p[i] + 1)));cout << ans1 << '\n' << ans2 << '\n';return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/836738.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

apifox使用小记

1.copy as cURL(cmd)之后在apifox里直接import cURL 2.调用时发生301错误 通常情况下是因为有session校验存在(用户校验)。解决方案: F12里将cookie里的session取到,在apifox里全局配置 这里踩了一个坑 第一次我是import了一个get请求,发送后发生301,所以我去设置了co…

毕业实习总结报告

毕业实习总结报告这既是毕业实习要求的总结报告,也是我对AutoSAR的一点理解,更是个人对未来生活的一点思考。我不希望把这份报告草草水过,而是希望把现在的感受记录下来,给以后的自己看一看,好记性不如记下来。涉及到工作细节和隐私的部分不在此展示了。时间像一头野驴呀,…

IDEA 2024 最新激活码,激活至2099(附有效idea激活码+激活工具)

IDEA 2024最新激活码,激活至2099(附有效idea激活码+激活工具)若提示We could not validate your license ff83b7bd51f5460ca43aabd7a96863a0.信息,idea激活时提示激活码失效解决方法: IDEA 2024 解决 We could not validate your license ff83b7bd51f5460ca43aabd7a96863a…

大学物理上册

质点作曲线运动时,质点在某一点的速度方向就是沿该点曲线的切线方向。

KingbaseES V8R6备份恢复案例之---sys_backup.sh init错误

KingbaseES 、sys_rman案例说明: KingbaseES V8R6数据库单实例环境,执行sys_backup.sh init时,出现“ repo_ip [127.0.0.1] must be located in local”错误,初始化失败。 适用版本:KingbaseES V8R6 一、问题现象 如下所示,执行sys_backup.sh init时出现以下故障:二、问…

鱼厂实习,光速转正了!

从最初那一份懵懂,到如今独立承担项目,回想这一路在鱼厂的成长,每一步都像是在重塑一个新的自己。今天要分享的这篇文章,比较特殊,是我们团队一位同事写的。主要分享了他从 0 开始学编程,再到加入鱼厂光速转正的故事。长达 6000 多字,诉说了自己四年多的经历,满满的真情…

深育大讲堂 | 洞见容器存储技术原理和市场应用趋势

深育大讲堂 | 洞见容器存储技术原理和市场应用趋势 4月12日,【深育大讲堂】系列直播活动第一讲“从容器存储讲起”圆满结束。深信服产教中心资深讲师丁运管、深信服四川省云业务总监薛悟团分别就多场景下的容器存储技术以及容器技术的应用与最佳实践进行深入剖析;并聚焦前沿…

apipost学习

开发团队的痛点: 1. 任何一个团队都是由前端、后端、测试三个TEAM组成的。 2. 产品经理确定需求和过评审后,前后端开发人员一起开会研讨定接口。并先由一个开发人员负责用Swagger定义API文档; 3. 后端会参照API 文档开发接口,并进行调试,用Postman里再跑一遍。 4. 接口…

你的团队如何在项目管理中实现智能化协作?

随着科技的快速发展,现代社会的各个领域都进入了智能化与高效化的时代。在这个背景下,项目管理软件作为提升工作效率、协作能力和组织规范性的重要工具,逐渐融入了更多跨界元素。从与人工智能(AI)的结合,到与创业者的合作,再到时间管理的深度挖掘,项目管理软件已经不再…

HyperWorks一维单元创建与模型连接管理

在HyperWorks的有限元分析中,一维单元是非常重要的概念。我们可以使用一维单元连接节点,或将不匹配的网格部件进行连接,进行载荷施加,以及用于建立焊接,螺栓,铆钉等等各类工程中经常运用的模型连接方式。 一维单元的类型是非常多的。有简单的刚性连接单元,有包含复杂截面…

JAVA反序列化学习-CommonsCollections3(基于ysoserial)

环境准备 JDK1.7(7u80)、commons-collections(3.x 4.x均可这里使用3.2版本) JDK:https://repo.huaweicloud.com/java/jdk/7u80-b15/jdk-7u80-windows-x64.exe <dependency><groupId>commons-collections</groupId><artifactId>commons-collections<…

单变量微积分学习笔记:反函数求导法则(12)【6,9,11】

常用公式 \(\arcsin(x) = \frac{1}{\sqrt{1-x^2}}\) \(\arccos(x) = -\frac{1}{\sqrt{1-x^2}}\) \(\arctan(x) = \frac{1}{1+x^2}\)证明 \(y = \arcsin(x)\) \(\sin(y) = x\) \(\cos(y)y = 1\) \(y = \frac{1}{\cos(y)}\) \(y = \frac{1}{\sqrt{1-\sin^2(y)}}\) \(y = \frac{1}…