KMP 算法 (Knuth&Morris&Pratt Algorithm)

KMP 算法 (Knuth&Morris&Pratt Algorithm)

找到所有 s2 作为字符串 s1 的字串的位置

朴素

枚举 i 作为 s1 子串起点, j 作为正在比较的字符在 s2 中的位置, 复杂度 O(mn)

Border

定义 border 为一个字符串的真子串, 既是母串的前缀, 又是母串的后缀

ai 为字符串 s2 的前 i 个字符组成的字符串的最长 border, 尝试 O(m) 求出 ai

首先, 一定有 a1=0, 假设对于 i, ai 已经求出, 则 aiai1+1

首先考虑取等的情况, 当且仅当 s2ai1+1=s2i, 这时可以想象在 [1,i1] 的最长 border 之后接上字符 s2ai1+1, 构成了一个长度为 ai1+1 的 border

举例

AABAA                 // [1, 5]
AA___ ___AA           // a[5] = 2
AABAAB AAB___ ___AAB  // a[6] = 3

对于原不等式的证明, 使用反证法, 假设 [1,i] 存在长度大于 ai1+1 的 border, 即 ai>ai1+1. 则去掉 s2i 后, [s21,s2ai1], 仍是 [1,i1] 的一个 border, 长度为 ai1, 则 ai1=ai1<ai, 假设不成立, 原式得证.

对于 s2ai1+1s2i 的情况, 根据 aiai1+1, 不存在长度为 ai1+1 的 border, 所以只会存在长度小于 ai1+1 的 border. 由于 ai 的 border 已经求出, 而已知 [1,ai1][iai1,i1] 是匹配的, 所以可以知道 [1,i1] 的最长 border 的最长 border 也是相匹配的, 即 [1,aai1][iaai1,i1] 是匹配的, 只要判断 s2aai1+1s2i 是否相等即可

举例

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 的正确性证明, 和 aiai1+1 的证明同理, 只要每次 border 末尾字符匹配失败时使枚举的长度 k=a[k] 即可.

接下来是复杂度分析, 由于每次递推时, ai 最多会比 ai1 增加 1, 所以对于最坏的情况, 即对于所有 i, 有 ai=i1, 对于一个每次都匹配失败的 i, 需要循环 i 次, 然后使 ai=0, i 之后的子循环次数再次变成常数. 所以均摊复杂度 O(m)

匹配

枚举 i 作为 s21s1 中的对应字符位置, 每次从匹配的字符往后扫, 直到字符不匹配或匹配成功. 这时, 已知 s1[i,i+k1]s2[1,k] 匹配, 所以 s1[i+kak,i+k1]s2[1,ak] 一定匹配, 此时对应 s21 的字符是 s1i+kak, 即 i=i+kak. 而对于 i(i,i+kak), 一定不存在和 s2 合法的匹配, 下面给出证明

仍是反证法, 对于 s1[i,i+k1]s2[1,k] 已经匹配, 而 s1i+ks2k+1 的情况, 假设存在 i(i,i+kak) 使得 s1[i,i+m1]s2 匹配, 则 s1[i,i+k1]s2[1,i+ki] 匹配, 又因为 i<i+kak, 所以 i+ki>ak. 这样一来, s1[i,i+k1]s2[1,i+ki] 匹配和 s1i+ks2k+1 矛盾, 假设不成立, 原结论得证

所以每次匹配结束 (失败/成功) 后, 设已经匹配的长度为 k, 则直接使 i=i+kak, k=ak, 进行下一轮匹配, 保证不遗漏任何一个可行匹配

分析复杂度, 每次 s1 有一个字符被成功匹配, s1i不会再和 s2 中任何字符比较第二次. 所以分析 s1 中字符匹配失败的概率. 发现失败次数和 k=ak 递归层数有关, 根据上面求 a 数组时的分析, 发现失败次数均摊 O(1), 所以匹配的复杂度 O(n), 整个算法加上预处理的复杂度为 O(n+m)

代码

a 数组, 枚举 s2 前缀长度 i

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 的含义略有不同, 结合注释理解, 某些注释英语水平感人, 英文注释是出于打代码时害怕各种编辑器对中文的编码兼容性问题

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

posted @   Wild_Donkey  阅读(121)  评论(1编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示