KMP再理解

题意

定义 p p p 串和 s s s 串,下标均从 1 1 1 开始。

p p p 的长度为 n n n s s s 的长度为 m m m

p p p s s s 中出现的所有的起始下标。

题解

这里的 KMP 讲解仅为了方便后续快速回忆。

n e ne ne 的定义

n e [ i ] ne[i] ne[i] 是对于 p p p 串来说的,令 k = n e [ i ] k=ne[i] k=ne[i]
n e [ i ] ne[i] ne[i] 的解释为:是指 p [ 1 , k ] = p [ i − k + 1 , i ] p[1,k]=p[i-k+1,i] p[1,k]=p[ik+1,i] ,其中 p [ i , j ] p[i,j] p[i,j] 表示 p [ i ] , p [ i + 1 ] , ⋯   , p [ j ] p[i],p[i+1],\cdots,p[j] p[i],p[i+1],,p[j] 这些字符构成的 p p p 的子串,满足 i ≤ j i\leq j ij

即对于 n e [ i ] ne[i] ne[i] ,表示 p [ 1 , i ] p[1,i] p[1,i] 这个子串的前 k k k 个字符构成的前缀和后 k k k 个字符构成的后缀是相同的。

为什么要 n e ne ne

我们考虑正常匹配时:
假设已经匹配到了 s [ i ] s[i] s[i] p [ j ] p[j] p[j],此时已经匹配成功了 j − 1 j-1 j1 个字符。

如果 s [ i ] ≠ p [ j ] s[i]\neq p[j] s[i]=p[j] ,暴力匹配的做法是需要从 s [ i − j + 1 ] s[i-j+1] s[ij+1] p [ 1 ] p[1] p[1] 重新匹配。

我们不想重新匹配,因为这其中可能有一些可以利用的信息。

考虑对于 k ≥ i − j + 1 k\geq i-j+1 kij+1 的下标,如果可以快速判断 p [ k , k + n − 1 ] ≠ s p[k,k+n-1]\neq s p[k,k+n1]=s,则这些 k k k 可以跳过。

n e ne ne 的具体使用

因为我们当前已经枚举到了 s [ i ] s[i] s[i] ,如果 p p p 长度为 l e n 1 len_1 len1 的前缀可以和 s [ i − l e n 1 , i − 1 ] s[i-len_1,i-1] s[ilen1,i1] 匹配
那么我们就可以继续判断 p [ l e n 1 + 1 ] p[len_1+1] p[len1+1] s [ i ] s[i] s[i] 是否匹配了。如此就可以进行下去匹配。

所以就需要找到最长的前缀长度 l e n 1 len_1 len1 ,而 n e [ i ] ne[i] ne[i] 的定义恰好就是对于 p [ 1 , i ] p[1,i] p[1,i] 这个字符串,最长的 l e n 1 len_1 len1 ,满足 p [ 1 , l e n 1 ] = p [ i − l e n 1 + 1 , i ] p[1,len_1]=p[i-len_1+1,i] p[1,len1]=p[ilen1+1,i]

如果 p [ l e n 1 + 1 ] p[len_1+1] p[len1+1] s [ i ] s[i] s[i] 匹配,那皆大欢喜。
如果 p [ l e n 1 + 1 ] p[len_1+1] p[len1+1] s [ i ] s[i] s[i] 不匹配,则需要找到一个尽可能大的,但是小于 l e n 1 len_1 len1 l e n 2 len_2 len2

这个 l e n 2 len_2 len2 得满足 p [ 1 , l e n 2 ] = p [ i − l e n 2 + 1 , i ] p[1,len_2]=p[i-len_2+1,i] p[1,len2]=p[ilen2+1,i] ,而这个更小的 l e n 2 len_2 len2,由于 KMP 的定义,则为 n e [ l e n 1 ] ne[len_1] ne[len1]

因为 p [ 1 , l e n 1 ] = p [ i − l e n 1 + 1 , i ] p[1,len_1]=p[i-len_1+1,i] p[1,len1]=p[ilen1+1,i]
如果有 p [ 1 , l e n 2 ] = p [ i − l e n 2 + 1 , i ] p[1,len_2]=p[i-len_2+1,i] p[1,len2]=p[ilen2+1,i] ,其中 l e n 2 < l e n 1 len_2len2<len1 ,且 l e n 2 len_2 len2 尽可能大
本质上是在 p [ i − l e n 1 + 1 , i ] p[i-len_1+1,i] p[ilen1+1,i] 中选择一个长度为 l e n 2 len_2 len2 的相等的前缀和后缀,也就是在 p [ 1 , l e n 1 ] p[1,len_1] p[1,len1] 中选择一个长度为 l e n 2 len_2 len2 的相等的前缀和后缀,也就是 l e n 2 = n e [ l e n 1 ] len_2=ne[len_1] len2=ne[len1]

代码

void KMP(string p, string s) {
    int n = p.size();
    int m = s.size();
    // 首空格是为了下标从 1 开始,p 的尾空格是为了防止越界,下面标明了可能越界的地方
    // 加入为空格是用于避免越界的方法 2
    p = " " + p + " ";
    s = " " + s;
    
    vector<int> ne(n + 1, 0);
    for (int i = 2, j = 0; i <= n; ++i) {
        while (j > 0 && p[j + 1] != p[i]) j = ne[j];
        if (p[j + 1] == p[i]) j += 1;
        ne[i] = j;
    }
    
    /* 
    	这个循环是指当前已经匹配成功了 s[i-1] ,这是怎么匹配成功的呢
    	
    	是指当前已经匹配成功的长度为 len1
 	    那么 s[i-len1,i-1] 和 p[1,len1] 匹配成功
    	然后我们需要判断 p[len1+1] 与 s[i] 是否匹配
    	如果不匹配,则找到小于 len1 的最大的 len2,依旧需要满足 s[i-len2,i-1]=p[1,len2]
    	最后找到的为 j 
    	 1. 如果 p[j+1] != s[i] ,说明可以直接跳过 s[i] 这个字符了
    	 2. 如果 p[j+1] == s[i] ,说明匹配成功,
    	    p[j+1] == s[i] ,继续判断 p[j+2] 和 s[i+1] 即可
		
		继续我们的操作,直到 j==n ,说明找到了一个,此时对应 s 中的下标是 i-j+1
    */
    for (int i = 1, j = 0; i <= m; ++i) {
   		/* 
   		    这里当 j=n 时,j+1 可能就越界了,所以两种解决办法
   			1. 在下面 j==n 的判断中,加上 j = ne[j],因为 ne[j] < n
   			2. 在上面的 p 中,加入一个不属于 p 和 s 字符集中的字符,如空格space
   		*/
        while (j > 0 && p[j + 1] != s[i]) j = ne[j];
        if (p[j + 1] == s[i]) j += 1;
        if (j == n) {
            // p[1, n] 在 s 中出现的所有下标,从 1 开始计算 s 中的下标
            cout << i - j + 1 << " ";
            // j = ne[j]; 用于避免越界的方法 1
        }
    }
}

你可能感兴趣的:(算法竞赛,KMP,kmp)