「笔记」KMP 算法
写在前面
不是我吹,我是真的刚学会 KMP 啊(
引入
给定字符串 ,求 在 中的所有出现位置。
。
1S,128MB。
朴素的想法是枚举 在 中的开头位置,暴力枚举判断是否匹配。如果失配,则抛弃当前已匹配的部分,到下一位置再从开头匹配。时间复杂度为 。
而 KMP 算法可以在 的时空复杂度内解决上述问题,且常数较小。
定义
:字符串 的子串 。
真前/后缀:字符串 的真前缀定义为满足不等于它本身的 的前缀。同理就有了真后缀的定义:满足不等于它本身的 的后缀。
:字符串 的 定义为,满足既是 的真前缀,又是 的真后缀的最长的字符串 。
如 的 为 。
:字符串 的 是一个长度为 的整数数组,它又被称为 的失配指针。 表示前缀 的 的长度,即:
特别的,若不存在这样的 ,则 。如 的 。
原理
在朴素算法中,如果在某一位上失配,则会抛弃当前已匹配的部分,跳到下一个位置再从开头进行匹配。
而 KMP 利用了当前已匹配的部分,使得在下一个位置时不必从开头进行匹配,从而对朴素算法进行了加速。
举个例子,如下图所示:

失配指针
的失配指针 可以通过在 上按上述思想匹配自身求得。下述算法中枚举到第 位时即可求得 。
首先显然有 。设枚举到第 位,考虑已知 的情况下如何求得 。
设当前匹配部分为 ,即有 。则显然有 。接下来考察 是否成立。
若成立,则有 ,得 。
若不成立,一种朴素的想法是减小已匹配长度 并暴力检查,直到找到最大的一个 ,满足 且 ,此时 。考虑利用已匹配部分的 border 加速上述过程。
引理:满足 且 的 的最大的 是 。
证明:考虑反证法,设存在 满足 是最大的满足条件的 。
根据条件,有 ,又 ,则 是 的一段后缀, 是 的一段前缀。则有 成立。
又 ,根据 border 的定义,则 应为 ,这与已知矛盾,反证原结论成立。直观的理解如下所示:
若 仍不满足 ,则一直令 ,直到满足条件或 。
模拟上述过程,可以得到下述代码:
复制复制fail[1] = 0; for (int i = 2, j = 0; i <= n2; ++ i) { //j 为匹配长度 while (j > 0 && s2[i] != s2[j + 1]) j = fail[j]; //找到满足条件的 border if (s2[i] == s2[j + 1]) ++ j; //匹配成功 fail[i] = j; }
匹配
按照上述过程实现即可,代码如下:
for (int i = 1, j = 0; i <= n1; ++ i) { //j 为匹配长度 while (j > 0 && (j == n2 || s1[i] != s2[j + 1])) j = fail[j]; //找到满足条件的 border,注意当整个串匹配成功的特判。 if (s1[i] == s2[j + 1]) ++ j; //第 j 位匹配成功 if (j == n2) printf("%d\n", i - n2 + 1); //整个串匹配成功 }
完整代码
//知识点:KMP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #define LL long long const int kN = 1e6 + 10; //============================================================= char s1[kN], s2[kN]; int n1, n2; int fail[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir, int sec) { if (sec > fir) fir = sec; } void Chkmin(int &fir, int sec) { if (sec < fir) fir = sec; } //============================================================= int main() { scanf("%s", s1 + 1); scanf("%s", s2 + 1); n1 = strlen(s1 + 1), n2 = strlen(s2 + 1); fail[1] = 0; for (int i = 2, j = 0; i <= n2; ++ i) { while (j > 0 && s2[i] != s2[j + 1]) j = fail[j]; if (s2[i] == s2[j + 1]) ++ j; fail[i] = j; } for (int i = 1, j = 0; i <= n1; ++ i) { while (j > 0 && (j == n2 || s1[i] != s2[j + 1])) j = fail[j]; if (s1[i] == s2[j + 1]) ++ j; if (j == n2) printf("%d\n", i - n2 + 1); } for (int i = 1; i <= n2; ++ i) printf("%d ", fail[i]); return 0; }
复杂度
求失配指针与匹配两部分的代码类似,仅解释其中一部分。
for (int i = 2, j = 0; i <= n2; ++ i) { while (j > 0 && s2[i] != s2[j + 1]) j = fail[j]; if (s2[i] == s2[j + 1]) ++ j; fail[i] = j; }
代码中仅有 while
的执行次数是不明确的。但可以发现,在 while
中 每次至少减少 1,每层循环中 每次至多增加 1。
又时刻保证 ,则 的减少量不大于 的增加量,即 。故 while
最多执行 次,则整个循环的复杂度为 级别。
例题
CF126B Password
给定一字符串 ,求一个字符串 ,满足 既是 的前缀,又是 的后缀,同时 还在 中间出现过(即不作为 的前后缀出现)。
。
2S,256MB。
既是 的前缀,又是 的后缀的串可以通过枚举 , 获得。
在 中间出现过的所有 的前缀为 ,用桶判断这两部分有无重复元素即可。
代码:A submission。
P4391 [BOI2009]Radio Transmission 无线传输
给定一字符串 ,已知它是由某个字符串 不断自我连接形成的,即有:
求字符串 的最短长度。
。
1S,128MB。
考虑一个更简单的问题,如何判断 的一个前缀 是否为 的循环节?
考虑求 的 ,显然当 且 时 为循环节。
正确性显然,若该条件成立,则保证了 如下所示:
发现呈现错位相等的关系,对应的,则有 ,可得 是一个循环节。
由上,可以得到两种做法。
第一种是暴力枚举前缀 ,判断 是否等于 ,且 。
第一个条件保证了 是 部分的循环节,第二个条件保证了剩下的部分是 的一个前缀。
第二种是直接输出 。原理如下所示:
显然可知最后的不完整部分是 的一个前缀。又保证了 是最长的既是 的前缀又是 的后缀的字符串,则 即为答案。
总复杂度均为 级别。
//知识点:KMP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #define LL long long const int kN = 1e6 + 10; //============================================================= char s[kN]; int n, fail[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } //============================================================= int main() { n = read(); scanf("%s", s + 1); fail[1] = 0; for (int i = 2, j = 0; i <= n; ++ i) { while (j && s[i] != s[j + 1]) j = fail[j]; if (s[i] == s[j + 1]) ++ j; fail[i] = j; } //Sol 2: printf("%d\n", n - fail[n]); return 0; //Sol 1: for (int i = 1; i <= n; ++ i) { int lth = n - (n % i); if (fail[lth] == lth - i && fail[n] >= n % i) { printf("%d\n", i); return 0; } } return 0; }
「NOI2014」动物园
组数据,每次给定一字符串 。
定义 表示是 的前后缀,且长度不大于 的字符串的个数。
求:,。
1S,512MB。
做法是自己 YY 的,效率被爆踩但是能过(
记 表示满足既是前缀 的真前缀,又是其真后缀的字符串组成的集合。
先不考虑长度不大于 这一限制,对于前缀 ,显然 的值为 。则显然有 ,表示在 的基础上计入 的 的贡献。 可在 KMP 算法中顺便求得。
再考虑限制,若前缀 的 的长度大于 ,则需要不断跳 ,跳到第一个满足长度合法的位置 ,再统计其贡献 。
暴跳实现可以获得 50pts 的好成绩。
发现跳 过程中对应的字符串长度会缩短(废话),考虑倒序枚举各位置 ,使得 也呈现递减的状态。
考虑暴跳过程,显然是由于某些 的转移被重复统计,导致暴跳效率较低。考虑并查集的思路,将重复的转移进行路径压缩。
设 表示前缀 在跳 之后对应的最大的第一个满足长度合法的 中的元素,初始值为 。在暴力跳 时,更新沿途遍历到的 即可。
这个路径压缩的复杂度我并不会证,但是感觉跑的还蛮快的= =
//知识点:KMP /* By:Luckyblock */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #define LL long long const int kN = 1e6 + 10; const int mod = 1e9 + 7; //============================================================= int n, ans, next[kN], num[kN], pos[kN]; char s[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Init() { ans = 1; scanf("%s", s + 1); n = strlen(s + 1); } void KMP() { for (int i = 2, j = 0; i <= n; ++ i) { while (j > 0 && s[i] != s[j + 1]) j = next[j]; if (s[i] == s[j + 1]) ++ j; next[i] = j; if (! j) continue ; pos[j] = j; //初始化 num[j] = 1ll * (num[next[j]] + 1ll) % mod; } } int Find(int x_, int lth_) { if (pos[x_] <= lth_ / 2) return pos[x_]; return pos[x_] = Find(next[pos[x_]], lth_); //路径压缩 } //============================================================= int main() { int t = read(); while (t --) { Init(); KMP(); for (int i = n; i >= 2; -- i) { pos[next[i]] = Find(next[i], i); //找到贡献位置 if (! pos[next[i]]) continue ; //特判无贡献情况 ans = 1ll * ans * (num[pos[next[i]]] + 1) % mod; } printf("%d\n", ans); } return 0; }
写在最后
参考资料:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix