KMP 算法 (Knuth&Morris&Pratt Algorithm)
KMP 算法 (Knuth&Morris&Pratt Algorithm)
找到所有 作为字符串 的字串的位置
朴素
枚举 作为 子串起点, 作为正在比较的字符在 中的位置, 复杂度
Border
定义 border 为一个字符串的真子串, 既是母串的前缀, 又是母串的后缀
设 为字符串 的前 个字符组成的字符串的最长 border, 尝试 求出
首先, 一定有 , 假设对于 , 已经求出, 则
首先考虑取等的情况, 当且仅当 , 这时可以想象在 的最长 border 之后接上字符 , 构成了一个长度为 的 border
举例
AABAA // [1, 5]
AA___ ___AA // a[5] = 2
AABAAB AAB___ ___AAB // a[6] = 3
对于原不等式的证明, 使用反证法, 假设 存在长度大于 的 border, 即 . 则去掉 后, , 仍是 的一个 border, 长度为 , 则 , 假设不成立, 原式得证.
对于 的情况, 根据 , 不存在长度为 的 border, 所以只会存在长度小于 的 border. 由于 的 border 已经求出, 而已知 和 是匹配的, 所以可以知道 的最长 border 的最长 border 也是相匹配的, 即 和 是匹配的, 只要判断 和 是否相等即可
举例
ABAABA // [1, 6]
ABA___ ___ABA // a[6] = 3
ABAABAB // [1, 7]
___A___ != ______B // s[4] != s[7]
ABA A__ __A // a_[a[6]] = a[3] = 1
_B_____ = ______B // s[2] = s[7]
AB_____ _____AB // a[7] = 2
对于这样求最长 border 的正确性证明, 和 的证明同理, 只要每次 border 末尾字符匹配失败时使枚举的长度 即可.
接下来是复杂度分析, 由于每次递推时, 最多会比 增加 , 所以对于最坏的情况, 即对于所有 , 有 , 对于一个每次都匹配失败的 , 需要循环 次, 然后使 , 之后的子循环次数再次变成常数. 所以均摊复杂度
匹配
枚举 作为 在 中的对应字符位置, 每次从匹配的字符往后扫, 直到字符不匹配或匹配成功. 这时, 已知 和 匹配, 所以 和 一定匹配, 此时对应 的字符是 , 即 . 而对于 , 一定不存在和 合法的匹配, 下面给出证明
仍是反证法, 对于 和 已经匹配, 而 的情况, 假设存在 使得 和 匹配, 则 和 匹配, 又因为 , 所以 . 这样一来, 和 匹配和 矛盾, 假设不成立, 原结论得证
所以每次匹配结束 (失败/成功) 后, 设已经匹配的长度为 , 则直接使 , , 进行下一轮匹配, 保证不遗漏任何一个可行匹配
分析复杂度, 每次 有一个字符被成功匹配, 不会再和 中任何字符比较第二次. 所以分析 中字符匹配失败的概率. 发现失败次数和 递归层数有关, 根据上面求 数组时的分析, 发现失败次数均摊 , 所以匹配的复杂度 , 整个算法加上预处理的复杂度为
代码
求 数组, 枚举 前缀长度
unsigned k(1);
for (register unsigned i(2); i <= lb; ++i) { // Origin_Len
while ((B[k] != B[i] && k > 1) || k > i) {
k = a[k - 1] + 1; // 这里 k 相当于如果本次匹配成功得到的 border 长度
}
if(B[k] == B[i]) { // 是匹配成功跳出而不是 a[k] = 0 溢出的
a[i] = k;
++k;
}
}
匹配两个字符串, 代码中 的含义略有不同, 结合注释理解, 某些注释英语水平感人, 英文注释是出于打代码时害怕各种编辑器对中文的编码兼容性问题
k = 1;
for (register unsigned i(1); i + lb <= la + 1;) { // Origin_Address
while (A[i + k - 1] == B[k] && k <= lb) {
++k; // 代码中 k 是待匹配的字符在 s_2 中的位置, 所以比实际匹配的长度多 1
}
if(k == lb + 1) { // 匹配成功
printf("%u\n", i);
}
if(a[k - 1] == 0) { // k = a[k] 的递归边界
++i;
k = 1;
continue;
}
--k; // k 是匹配失败的字符在 s_2 中的位置, 所以实际有 k - 1 长度的子串匹配成功
i += k - a[k]; // Substring of Len(k - 1) has already paired, so the next time, start with the border of the (k - 1) length substring
k = a[k] + 1; // 为什么我要写英文啊, 这是对 i 和 k 的同时移动, k = a[k] + 1 意思是在新起点已经匹配的 a[k] 长度的前缀之后的第一个字符开始比较
}
模板 Luogu P3375
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)