KMP算法和next数组的性质
目录
1.匹配字符串:831. KMP字符串 - AcWing题库
2.next周期的应用:141. 周期 - AcWing题库
3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库
4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库
模板
next[i]表示以i结尾的后缀中与其匹配的最大前缀的长度
求Next数组:
// s[]是模式串,p[]是模板串, n是s的长度,m是p的长度
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j]; //注意是while循环,j=2开始
if (p[i] == p[j + 1]) j ++ ;//这里的j可以表示为以i为终点的串中的以i为终点的后缀中与模板穿最大匹配长度,即匹配时一定包含了i这个位置的字符
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
例题:
1.匹配字符串:831. KMP字符串 - AcWing题库
AC代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000007, M = 100007;
//next在某些头文件中是关键词
int ne[M]; //next数组表示在当前位置不匹配,需要移动的距离,就是最大前缀的长度
char p[M], s[N];
int m, n;
int main()
{
cin >> m >> (p + 1) >> n >> (s + 1);//+的优先级比>>高,不加括号会报错
//求next数组
for(int i = 2, j = 0; i <= m; i ++ ) //i从2开始,因为next[1]初始化为0
{
while(p[i] != p[j + 1] && j) j = ne[j]; //因为我们每次比较的是查找串S的第i位和模板串P的第j+1位,所以我们要回退next[j]的距离,不是next[j+1],即j+1位置之前的最长后缀距离
if(p[i] == p[j + 1]) j ++ ; //如果匹配成功,j进一位,保存当前位置的next值,否则只能保存0,因为while中肯定是循环到j=0结束的
ne[i] = j;
}
//匹配
for(int i = 1, j = 0; i <= n; i ++ )
{
while(s[i] != p[j + 1] && j) j = ne[j];//如果不匹配,j回退,直到j退无可退,即j=0到达原点的时候
/*
为什么要设置成i匹配j的下一位,如果设置成i匹配j,那么当i与j的第一位就不匹配时,即s[i]!=p[1],我们是要回退next[0]的距离的,
但next[0]是规定成0的,那么就会死循环,因为j回退0等于不回退,除非我们更改next[0]的值,但这显然是不可能的
所以我们让i匹配j的下一位,并设置j=0的位置为j的起点,这样当i与j的第一位就不匹配时,我们不让他回退了,因为这时显然退无可退
所以j只能进位,毕竟退无可退
*/
if(s[i] == p[j + 1]) j ++ ; //如果匹配,j进一位
if(j == m) //整个模板串都匹配成功了
{
cout << i - m << " ";
}
}
cout << endl;
return 0;
}
next数组具有周期性
2.next周期的应用:141. 周期 - AcWing题库
这里假设在n处不匹配,模板串回退到next[n]即b的位置,那么b就是最大前缀的终点,假设a为最大后缀的起点
就有区间长度[1, b]=[a, n],因为[a, b]是他们的公共部分,所以区间长度[1, a]=[b, n]
假设下面的区间就是模板串移动后的位置,沿垂直方向做下一个移动后的模板串a在原位置的a',有原区间[a, a' ]=[1, a]
又因为[1, a]=[b, n],所以说[a, a' ] = [a' , b]
所以区间a[1, a] = [a, a' ] = [a' , b] = [b, n],即[1, n]可以被均分为四个子区间,区间长度为n-next[i]
一个重要的点是不要被这个图误导了,误认为next周期一定为4,其实当next[n]与a重合时,周期为3,当next[n]与1重合时,周期为1……
AC代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000010;
char s[N];
int ne[N], n, T;
void get_next()
{
ne[1] = 0;
for(int i = 2, j = 0; i <= n; i ++ )
{
while(s[i] != s[j + 1] && j) j = ne[j];
if(s[i] == s[j + 1]) j ++ ;
ne[i] = j;
}
}
int main()
{
while(cin >> n, n)
{
cout << "Test case #" << ++T << endl;
cin >> (s + 1);
get_next();
for(int i = 1; i <= n; i ++ )
{
int t = i - ne[i]; // t就对应上图[1,a]中的a,如果有周期的话,[1,t]就是最小的周期,周期长度为t(t-1+1)
// 这里判断 i>t 主要是判断 i==t 的情况,此时 i%t==0,i不可能大于t
// 因此下面也可以写为 if(t != t && i % t == 0) ...
if(i > t && i % t == 0) cout << i << " " << i/t << endl;//
}
cout << endl;
}
return 0;
}
3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库
思路参考:AcWing 160. 匹配统计 - AcWing
求Next数组:
// s[]是模式串,p[]是模板串, n是s的长度,m是p的长度
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ; //深刻理解:这里的j可以表示为以i为终点的串中的 以i为终点的后缀中与模板穿最大匹配长度,即匹配时一定包含了i这个位置的字符和前面匹配成功的j-1个字符
ne[i] = j;
}
AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 200010;
char s[N], t[N];
int ne[N];
int f[N]; //因为我们无法求出确定的前缀长度,只能求出最小的前缀长度,所以f[i]表示一个范围,f[i]表示所有后缀中匹配长度大于等于i的后缀数
int main()
{
int n, m, q;
cin >> n >> m >> q;
scanf("%s%s", s + 1, t + 1);
//初始化next数组
for(int i = 2, j = 0; i <= m; i ++ )
{
while(t[i] != t[j + 1] && j) j = ne[j];
if(t[i] == t[j + 1]) j ++ ;
ne[i] = j;
}
//匹配
for(int i = 1, j = 0; i <= n; i ++ )
{
while(s[i] != t[j + 1] && j) j = ne[j];
if(s[i] == t[j + 1]) j ++ ;
f[j] ++;
}
for(int i = m; i ; i -- ) f[ne[i]] += f[i];
while(q --)
{
int t;
cin >> t;
cout << (f[t] - f[t + 1]) << endl; //f数组是一个范围
}
return 0;
}
4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库
思路
- 如果我们要求得一个最小的覆盖子矩阵,设他的长为width,宽为height,那么height*width的积最小
- 因为列的范围为75,是一个很小的数,所以我们可以暴力求解矩阵的长width,再由width求解height,但要注意的一点是,某一行的一个长度width具有周期性,但到了下一行不一定还是满足,所以求width时要遍历所有行
- 因为我们显然可以求出多个满足周期性质的width,但我们不可能每一个都尝试求该width条件下的height,所以我们只会找一个最优的width,那么哪一个width满足最优条件,即矩阵积最小的情况呢
- 我们从定义考虑,已知矩阵面积为height*width,width假设已知(我们已经找到了那个最优的解),而height=n-next[n](由next数组的周其性质可得,我们要找的是整列的周期,所以是n-next[n]),因为n已经固定,那么我们只要让next[n]尽可能的大,那么height就会尽可能地小,矩阵面积就会尽可能的小。
- 我们通过next数组的定义可以知道,next[i]是在i位置最大的与后缀相同的前缀的长度,所以说next[i]越大,说明这个i位置之前的字符组成的串匹配度越高,例如:aaaaa的匹配程度最高,abcde匹配程度最低,当然也会受长度的影响,aaaaaaa的匹配度高于aaa。
- 那么问题就转化成了求如何才能使字符串的匹配程度更高,但是到这里别忘了,我们这里匹配的是一个width长度的串,不再是一个单个的字符了,如果时刻记得这一点,这个问题就很清晰了,当然是width最小的时候,匹配程度最高了,为什么呢?
- 假设,width=4时每次匹配的width串都是满足的,那么width=5时呢?这就不一定了,但width=3时肯定时满足的,因为width=4都满足,width=2肯定也满足,因为不满足就代表着匹配程度低,next[i]值小,n-next[i]大,矩阵乘积大,所以假象就成立
AC代码
初始化矩阵的行标都从1开始,前代码列标从0开始,后代码从1开始,理解两个代码的不同之处
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 10010, M = 80;
int n, m;
char str[N][M];
bool st[M];
int ne[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
{
cin >> str[i];
for (int j = 1; j <= m; j ++ )
{
bool is_match = true;
for (int k = j; k < m; k += j)
{
for (int u = 0; u < j && k + u < m; u ++ )
if (str[i][u] != str[i][k + u])
{
is_match = false;
break;
}
if (!is_match) break;
}
if (!is_match) st[j] = true;
}
}
int width;
for (int i = 1; i <= m; i ++ )
if (!st[i])
{
width = i;
break;
}
// cout << "widht" << width << endl;
for (int i = 1; i <= n; i ++ ) str[i][width] = 0;
for (int j = 0, i = 2; i <= n; i ++ )
{
while (j && strcmp(str[j + 1], str[i])) j = ne[j];
if (!strcmp(str[j + 1], str[i])) j ++ ;
ne[i] = j;
}
int height = n - ne[n];
// cout << "height: " << height << endl;
cout << width * height << endl;
return 0;
}
#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10007;
char str[N][100];
int ne[N];
int n, m;
bool check[N];
int main()
{
cin >> n >> m; //n行m列
memset(check, true, sizeof check); //初始化所有width长度为true,即是可行的
for(int i = 1; i <= n; i ++ ) //n从1开始
{
scanf("%s", str[i] + 1); //m从1开始
for(int j = 1; j <= m; j ++ ) //对每一行枚举width长度
{
if(check[j]) //为false就不用检查了
{
for(int k = j + 1; k <= m; k += j ) //将j后面的所有区间长度为j的区间和区间[1, j]比较
{
for(int u = 1; u <= j && u + k - 1 <= m; u ++ ) //和[1, j]作比较,u+j<=m防止越界比较
{
if(str[i][u] != str[i][k + u - 1]) //每次比较第u位和第k+u-1位即第一个区间的第u为和当前区间的第u位
{
check[j] = false;
break;
}
}
if(!check[j]) break;
}
}
}
}
int width;
for(int i = 1; i <= m; i ++ )
{
if(check[i]) //找到最小满足条件的width就结束
{
width = i;
break;
}
}
// cout << width << endl;
for(int i = 1; i <= n; i ++ ) str[i][width + 1] = 0; //相当于截取一个字符串
//KMP匹配列height
//先写出模板,然后修改,因为我们比较的不是单个的字符了,而是一个长度为width串
// for(int i = 2, j = 0; i <= n; i ++ )
// {
// while(str[i] != str[j + 1] && j) j = en[j];
// if(str[i] == str[j + 1]) j ++ ;
// ne[i] = j;
// }
for (int j = 0, i = 2; i <= n; i ++ )
{
while (j && strcmp(str[j + 1] + 1, str[i] + 1)) j = ne[j];
if (!strcmp(str[j + 1] + 1, str[i] + 1) ) j ++ ;
ne[i] = j;
}
int height = n - ne[n];
// cout << height << endl;
cout << width * height << endl;
return 0;
}
一些小知识
- sizeof是一个运算符,不是一个函数,所以后面的对象可以不用加括号,直接 sizeof a 即可
- 如果想要将一个char[n][n]的字符数组从开头截取一部分,只需要在截取的末尾处让a[n][p] = 0,那么第n行就截取了一个[0,p]的串,并且如果遍历字符数组,p之后的位置就无法遍历了,因为'0'表示一个字符串的结尾 (‘0’的ascall码就是0)
- 虽然KMP具有周期性,但也不能乱用,只有当循环节完全循环完时,即不缺也不少,才是一个真正的周期
- 用strcmp函数将KMP匹配字符转化为匹配串,当两个字符串相等时返回0,否则返回1或者-1
详细Next数组性质参考: 困扰已久的KMP - AcWing
KMP算法视频讲解:找不到页面 - AcWing