KMP

这一篇居然是错的!情急之下,先背板子吧!

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

口诀
5行先设nxt[1]=0
先2后1j为0
某i不等j+1
一改不等j为nxt
只需再加j==m

  • 5行KMP
  • 下标1开始
  • nxt[1]=0
  • i=2,j=0;i<=m
  • j&&[i]!=[j+1]
  • j=nxt[j]
  • [i]==[j+1]
  • nxt[i]=j
  • i=1,j=0;i<=n
  • j&&(j==m||s[i]!=t[j+1])

简介

KMP 之所以叫 KMP,是因为它是由三个人,Knuth、Morris、Pratt,联合发表的。它的功能是求出一个模式串 \(t\) 在文本串 \(s\) 中每次出现的位置。当然字符串哈希能以相近的复杂度求出同样的信息,但 KMP 还是会稍快一些,并且在一些情况下可以做到哈希不好办到的事情。

在此声明,本文中的字符串从 \(0\) 开始编号。

求解

共分为两大部分:

  1. \(t\) 本身求出它的每个长度为 \(i\) 的前缀 \(t'\),找到最长的字符串 \(t''\)\(t''\neq t'\),使得它同时是 \(t'\) 的前缀和后缀。这个值记为 \(next_i\)
  2. \(s\) 中找到每次 \(t\) 出现的位置。

递推法求 \(next\) 数组

\(next_0=-1\)

假想我们已经知道了 \(next_{0\sim i-1}\),现在想要知道 \(next_i\)。我们从 \(next_{i-1}\) 着手。那么就是 \(t_{0\sim i-2}\) 的前 \(i-1\) 个字符和后 \(i-1\) 个字符相等,这时如果在这两个前缀后缀的后面再加上一个相等的字符,两个前缀后缀依然是相等的,也就是说,当 \(t_{next_{i-1}=t_{i-1}}\) 时,\(next_i=next_{i-1}+1\)。下面讨论不相等的情况。这时找到 \(next_{next_{i-1}}\),也就是 \(t_{1\sim next_{i-2}}\) 的最长前缀后缀,为了叙述方便,我们把 \(next_{next_{i-1}}\) 记为 \(x(i-1)\) 那么那个后缀就可以平移到 \(t_{0\sim i-1}\) 的后 \(x(i-1)\) 个字符那里,这时在两个长度为 \(x(i-1)\) 的前缀后缀之后看看加上的字符是否相等,如果相等那么答案就是此时前缀后缀的长度,如果不是就继续观察长度为 \(next_{x(i-1)}\) 的前缀后缀……如果直到最后还无法找到一组相等的前缀后缀,而且 \(t_{0\sim i-1}\) 的第一个和最后一个字符也不相同,那么 \(next_i\) 就只能等于 \(next_0+1=-1+1=0\) 了。

图片.png

< Code >

    string s,t;
    cin>>s>>t;
    int n=s.length(),m=t.length();
    next[0]=-1;
    for(int i=1;i<=m;i++){
        int j=next[i-1];
        while(j>=0 && t[j]!=t[i-1]) j=next[j];
        next[i]=j+1;
    }

寻找出现位置

假想我们在某一时刻,已经确定了 \(t_{0\sim j-1}\)\(s_{i\sim i+j-1}\) 匹配,而此时发现 \(t_j\neq s_{i+j}\),怎么办?朴素想法是放弃这一次尝试,把 \(t\) 往后移动一位,让 \(t\) 跟下一个 \(s\) 的子串进行匹配,但是这样我们的总复杂度就是 \(O(|s|\cdot|t|)\) 了,多高啊!我们可不可以想办法让 \(i\) 直接跳过一大块绝对不可能跟 \(t\) 匹配的位置,来到一个有可能的地方呢?当然可以。因为 \(t\) 的前面 \(len=j\) 段是跟 \(s\) 匹配的,那么也就是说这一段的一个后缀是可以天衣无缝地压到 \(s\) 上的,这时如果把一个等长的前缀压到这个后缀上,且这两个前缀后缀是相同的,那么就只需要比较前缀之后的 \(t\)\(s\) 的匹配关系,复杂度减低很多,那么什么长度的前缀后缀可以满足,而且是 \(t\) 从左向右的移动过程中第一个可以满足的位置呢?那就是 \(next_j\) 了,此时相当于直接把 \(t\) 向右移动 \(j-next_j\) 位。现在考虑一个问题,如果 \(t\) 可以完全在某个位置跟 \(s\) 匹配那还要移动它吗?肯定还是要移的,移动的位数同理是 \(next_{|t|}\)

图片.png

< Code >

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

最小循环节

学完模板,最小循环节也是很重要的一个知识点,如果一个串 \(t\)\(s\) 内首尾相邻地出现连续次且刚好可以把 \(s\) 给全部覆盖完,那么 \(t\)\(s\) 的循环节。我们发现,长为 \(L=|s|-next_{|s|}\) 的前缀一定是 \(s\) 的最小循环节;如果 \(|s|\bmod L=0\),则 \(t\) 刚好可以覆盖完 \(s\),否则需要加上 \(t\) 的 一部分才可以完全匹配。自己推导一下就可以明白这个道理。

练习

  • 模板:1
  • 奶牛矩阵:2
posted @ 2021-06-30 18:48  pengyule  阅读(138)  评论(0编辑  收藏  举报