在日常生活中我们常常会遇见在一篇文章中找关键词的事情。如果用程序来解决你会怎么做???
ps:假设文章字段为t, 关键词为p
枚举文章中的每一个点,然后往后匹配是非为关键字???
就像这样, 挨个匹配每个字符。
int lent = strlen(t); int lenp = strlen(p); for(int i = 0; i < lent; i++) { int flag = 1;//是否成功匹配 if(i + j >= lent) break; for(int j = 0; j < lenp; j++) { if(t[i+j] != p[j])//匹配失败 { flag = 0; break; } } if(flag) printf("%d", i-lenp+1);//输出匹配成功了的位置 }
显然复杂度是o(n^2)的,会耗费大量的时间。
ps:从下表为1得位置开始储存p和t。(这样会更加方便)
暴力算法之所以耗费大量的时间,是因为常常会发生这样的现象:经过千辛万苦匹配到了p的最后一个字符,结果这时候不匹配了,于是乎向右移动一格,然后重头开始匹配。
nex数组:
那么如果我们可以使得在匹配失败后向右移动很长的一段距离,那么就可以节省大量的时间了。
由此我们为p定义一个nex数组,nex[i]的含义为nex[i]的含义为以p[i]j结尾的后缀,能够匹配的从1开始非平凡前缀的最大坐标, 非平凡的意思是不能是p本身。也就是说,找到最大的长度,p的前缀和以p[i]为结尾的后缀相同。
例子:
比如对于abab
nex值分别为0 0 1 2
对于aaaa
nex值分别为0 1 2 3
算法过程:
如果现在正在尝试匹配t[i] 和 p[j+1],如果这个时候匹配失败,那么可以直接令j = nex[j], 然后继续判断t[i] 是否等于p[j+1], 直到他们相等或者j为0。
t[i] == p[j+1] 的意思已经通过nex[j]找到了了一个前缀仍然匹配了t以t[i-1]结尾的后缀,此时t[i] == p[j+1], 说明对于j+1已经匹配成功了,可以开始下一个字符的匹配了。
j == 0的意思是这个时候p无法继续向右移动了,已经到头了,然后也可以不需要继续匹配了。
匹配的代码:
void kmp() { int lent = strlen(t+1); int lenp = strlen(p+1); //尝试用t[i]与p[j+1]进行匹配 for(int i = 1, j = 0; i <= lent; i++) { while(j && t[i] != p[j+1]) j = nex[j]; if(t[i] == p[j+1]) j++; if(j == lenp) { printf("%d\n", i-lenp+1);//输出成功匹配的起始下表 j = nex[j]; } } }
假设现在在ABABABC上匹配ABA(下标都从1开始)
ABA的nex值为0 0 1
初始时i == 1, j == 0(因为用t[i] 与 p[j+1]进行匹配, 这样初始化表示从p和t第一个字符开始匹配)
匹配过程:
i == 1, j == 0 : t[i] == p[j+1]
i == 2, j == 1 : t[i] == p[j+1]
i == 3, j == 2 : t[i] == p[j+1] (输出下表1并j = nex[j])
i == 4,j == 1 : t[i] == p[j+1]
i == 5, j == 2: t[i] == p[j+1] (输出下表3并j = nex[j])
i == 6, j == 1: t[i] == p[j+1]
i == 7, j == 2: t[i] != p[j+1] (j = nex[j])
i == 7, j == 1: t[i] != p[j+1] (j = nex[j])
i == 7, j == 0: 跳出循环
结束匹配
最终i == 8, j == 0
注意项: 为什么nex的值时最长的前缀,而不能是比最长要小一点,因为j = nex[j]其实是相当与把p直接向右移动一大段距离,而是的nex值越长,其实是为了移动的距离小一点,防止造成匹配的遗漏。
nex数组的构建与匹配过程基本相同,我们是通过从下标1开始遍历,逐个获取每个位置的nex值,也就是说当我们需要求nex[i]时, nex[1] 到nex[i-1] 的值都已经求到了,如果我们可以用i之前的nex值求得i得nex值,那就可以了。。那么根据nex的定义,如果现在要求nex[i], 而j在每一次迭代后等于nex[i-1]。
说明现在以j结尾得前缀是仍然是和i-1结尾得后缀相同得,如过现在p[i]等于p[j+1],则nex[i] 等于j+1,否则继续j = nex[j]。
如果最后j = 0, 那么说明找不到一个长度至少为1得前缀与以p[i]为后缀的字符串相同, 此时nex[i] 等于0。
例如:
求ABABABC的第六个字符nex值。
前五个nex值为0 0 1 2 3
根据nex[5]我们可以找到一个前缀和与下标5为后缀相同,也就是ABA, 因为第四位恰好时B与第六位相同, 那么就找到了最大的前缀。所以nex[6]为4。
下面是代码
void getnex() { int lenp = strlen(p+1); for(int i = 2, j = 0; i <= lenp; i++) { while(j && p[i] != p[j+1]) j = nex[j]; if(p[i] == p[j+1]) j++; nex[i] = j; } }
模板题
例题:传送门
解析:
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 10; char t[N], p[N]; int nex[N]; void getnex() { int lenp = strlen(p+1); for(int i = 2, j = 0; i <= lenp; i++) { while(j && p[i] != p[j+1]) j = nex[j]; if(p[i] == p[j+1]) j++; nex[i] = j; } } void kmp() { int lent = strlen(t+1); int lenp = strlen(p+1); for(int i = 1, j = 0; i <= lent; i++) { while(j && t[i] != p[j+1]) j = nex[j]; if(t[i] == p[j+1]) j++; if(j == lenp) { printf("%d\n", i-lenp+1); j = nex[j]; } printf("%d %d\n", i , j); } } int main() { scanf("%s%s", t+1, p+1); getnex(); kmp(); int lenp = strlen(p+1); for(int i = 1; i <= lenp; i++) printf("%d ", nex[i]); }
这题有点小难。
例题:传送门
解析:
#include <bits/stdc++.h> using namespace std; const int N = 5e6+10; int nex[N]; char t[N], p[N]; int now; void get_nex() { int len = strlen(p+1); for(int i = 2, j = 0; i <= len; i++) { while(j && p[i] != p[j+1]) j = nex[j]; if(p[i] == p[j+1]) j++; nex[i] = j; } } int kmp(char *p, char *t) { int j = 0; int lent = now; int lenp = strlen(p+1); int st = max(lent - lenp+1, 1); for(int i = st; i <= lent; i++) { while(j && t[i] != p[j+1]) j = nex[j]; if(t[i] == p[j+1]) { j++; } } return j; } int main() { int n; scanf("%d", &n); scanf("%s", t+1); now = strlen(t+1); for(int i = 2; i <= n; i++) { scanf("%s", p+1); get_nex(); int j = kmp( p, t); int lenp = strlen(p+1); for(int i = j + 1; i <= lenp; i++) t[++now] = p[i];//从最终重复起始位置挨个把字符复制进t中 t[now+1] = '\0'; } printf("%s", t+1); return 0; }