kmp算法,主串扫描指针不后退
算法思路acwing 831. kmp字符串
暴力枚举如何做 如何优化 暴力做法
双层循环,遍历模式串 S \textit{S} S 和模板串 P \textit{P} P
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int i = 1 ; i <= n; ++ i) { int _i = i, j = 0 ; while (j <= m) { if (s[_i] != p[j]) break ; _i++; j++; } if (j == m) return true ; }
假设匹配到图中绿线处,失配。若按照暴力匹配的思路,当前模式串 S S S 的扫描指针会指向下一个匹配开始点 (会后退),模板串 P P P 则会回退到起始处 ,重新进行匹配。
举个🌰,主串为s="a"
问题:如何利用已经获得的信息,减少重复工作?
考虑,模版串 P \textit P P 最多 向后移动到什么位置,可以继续匹配
这个值,只和模板串 P \textit P P 有关。(对 P \textit P P 进行预处理)
即:以某点为终点的后缀,和前缀相等,这个相等的前/后缀的长度最大值
关于next数组的含义,将 n e x t [ i ] next[i] n e x t [ i ] 看作一个集合:
集合:和以 1 1 1 开始的前缀相同的,终点为 i i i 的后缀。 属性:最大的长度 用下标来表示,假设 n e x t [ i ] = j next[i] = j n e x t [ i ] = j
其含义为:p [ 1 … j ] = p [ i − j + 1 … i ] p[1 \dots j] = p[i-j+1 \dots i] p [ 1 … j ] = p [ i − j + 1 … i ]
据此,可以使得模式串 S \textit S S 在匹配过程中不用后退
以下的 i i i 和 j j j 和上一节的含义不同,分别指模式串和模板串的扫描指针。
假设,主串(模式串) S S S 扫描到 i i i ,失配( s [ i ] ! = p [ j + 1 ] s[i] != p[j+1] s [ i ] ! = p [ j + 1 ] ),而 i − 1 i-1 i − 1 之前的均已经匹配
此时要移动模板串 P \textit P P 向前移动,将模板串的指针指向 n e x t [ j ] \textit next[j] n e x t [ j ] 即可
然后循环此过程,直到匹配,或者 j \textit j j 退到起点
代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <iostream> using namespace std;const int N = 100010 , M = 1000010 ;int n, m;int ne[N]; char s[M], p[N];int main () { cin >> n >> p + 1 >> m >> s + 1 ; for (int i = 2 , j = 0 ; i <= n; i ++ ) { while (j && p[i] != p[j + 1 ]) j = ne[j]; if (p[i] == p[j + 1 ]) j ++ ; ne[i] = j; } for (int i = 1 , j = 0 ; i <= m; i ++ ) { while (j && s[i] != p[j + 1 ]) j = ne[j]; if (s[i] == p[j + 1 ]) j ++ ; if (j == n) { printf ("%d " , i - n); j = ne[j]; } } return 0 ; }
求解next数组简单举例 匹配过程举例匹配到 i = 7 i = 7 i = 7 和 j + 1 = 7 j+1=7 j + 1 = 7 时,失配, j j j 跳到 n e x t [ 6 ] next[6] n e x t [ 6 ] , j j j 更新为 4
后记更新@2022年11月 3日 星期四 18时08分26秒 CST
本文之前的思路,字符串下标是从1开始的,前缀长度为几,前缀的右端下标就是几
n e x t [ j ] next[j] n e x t [ j ] 数组的含义,是模式串 p [ 1 , j ] p[1,j] p [ 1 , j ] 的前后缀相等的长度,因此每一轮主串 s s s 的第 i i i 个字符和模式串 p p p 的第 j + 1 j+1 j + 1 字符进行比较
一旦失配,直接移动 j j j 到和 已经匹配的后缀 相同 的前缀右端,接着比较匹配的下一位(j+1)即可
如果字符串的下标从0开始呢?
每一轮比较 s [ i ] s[i] s [ i ] 和 p [ j ] p[j] p [ j ]
并且需要稍微改造一下 n e x t next n e x t 数组的含义,n e x t [ j ] next[j] n e x t [ j ] 表示模式串 p [ 0 , j p[0,j p [ 0 , j -1 ] 1] 1 ] (不包含失配的下标为j的字符 )的前后缀相等的长度
由于下标从 0 开始,这个长度直接就是前缀匹配部分下一位的下标
代码示意
注意:next数组会多一位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int [] getNextArray(String p) { if (p.length() == 0 ) return new int []{}; int [] next = new int [p.length() + 1 ]; for (int i = 1 , j = 0 ; i < p.length(); ++i) { while (j > 0 && p.charAt(i) != p.charAt(j)) j = next[j]; if (p.charAt(i) == p.charAt(j)) j++; next[i + 1 ] = j; } return next; } int kmp (String s, String p) { if (s.length() == 0 || p.length() == 0 ) return -1 ; int [] next = getNextArray(p); for (int i = 0 , j = 0 ; i < s.length(); ++i) { while (j > 0 && s.charAt(i) != p.charAt(j)) j = next[j]; if (s.charAt(i) == p.charAt(j)) j++; if (j == p.length()) { j = next[j]; } } return -1 ; }
完整代码
831. KMP字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.io.*;class Main { static final int N = 100010 ; static final int M = 1000010 ; static BufferedReader reader = new BufferedReader (new InputStreamReader (System.in)); static BufferedWriter log = new BufferedWriter (new OutputStreamWriter (System.out)); static int [] next = new int [N]; static char [] s = new char [M]; static char [] p = new char [N]; public static void main (String[] args) throws Exception { int n = Integer.parseInt(reader.readLine()); String pstring = reader.readLine(); int m = Integer.parseInt(reader.readLine()); String sstring = reader.readLine(); for (int i = 0 ; i < m; ++i) s[i] = sstring.charAt(i); for (int j = 0 ; j < n; ++j) p[j] = pstring.charAt(j); next[0 ] = next[1 ] = 0 ; for (int i = 1 , j = 0 ; i < n; ++i) { while (j > 0 && p[i] != p[j]) j = next[j]; if (p[i] == p[j]) j++; next[i + 1 ] = j; } for (int i = 0 , j = 0 ; i < m; ++i) { while (j > 0 && s[i] != p[j]) j = next[j]; if (s[i] == p[j]) j++; if (j == n) { log.write(Integer.toString(i - j + 1 ) + " " ); j = next[j]; } } log.flush(); reader.close(); log.close(); } }
举个实际例子,求p="aabbaa"
的 next
数组
如果 p
的下标从 1
开始,求出来的 next
数组应当是如下所示:
如果 p
的下标从 0
开始,求出的 next
数组:
其实结合实际意义,算是整体向右平移了一个单位
参考文章leetcode 28. 实现strStr()【宫水三叶】简单题学 KMP 算法
AcWing 831. 字符串查找—用16幅图从暴力一步步优化到KMP
📔博文图谱
提及本博文的链接