KMP算法 + 边界处理
https://www.acwing.com/problem/content/description/4315/
本题的难点主要在于给定你区间的左右端点,如何求出来这个区间内成功匹配的个数
这里肯定要用前缀和,不然1e8的时间复杂度很悬
但是用前缀和的时候,我们要对左右端点稍微处理一下
在使用KMP算法匹配的时候,如果在位置原字符串的位置pos成功匹配(这里很关键),我们有两个选择:
- s[pos] ++,在匹配成功位置的右端点+1
- s[pos - m + 1] ++ (m表示匹配串的长度),在匹配成功位置的左端点+1
对于选择1,有这么一种情况ababcabcd ,假如ab 是我们要匹配的字符串,由于我们在匹配成功位置的右端点+1,那么,上串标下划线的地方s[pos]=1,如果我们选定红色区间的字符串(下标从1开始)s[8] - s[6] = 1 ,我们发现问题了!明明bc 不可能与ab匹配,但是我们得到的答案是1,这是由于我们匹配到了匹配串ab的最后一个元素b,但我们没有完全匹配到ab导致的,那么如何解决这个问题呢?
我们可以让左端点L 的位置变为L+m-1,什么意思呢?
对应上面的例子,我们发现,如果L=L+m-1 ,那么此时L和R重合,s[8]-s[7]=0
这时候,整个区间*[L,R]中的每一个点,就表示我们匹配到的一个长度为m的串的右端点,注意,是右端点!而我们在前面匹配成功的时候,也是在右端点+1*,这样,就可以保证不会匹配到一个后缀,导致出现错误答案。
同理,如果我们在左端点+1,那么给定区间的左端点就要减去*(m-1),区间中的每个点就表示匹配到的一个长度为m*的串的左端点。
同时注意,由于我们修改了左右端点,此时可能会出现L>R的情况,所以我们要判断一下
在左端点+1
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m, q;
int ne[N], s[N];
char str[N], t[N];
int main()
{
cin >> n >> m >> q;
cin >> (str + 1) >> (t + 1);
for(int i = 2, j = 0; i <= m; i ++ )
{
while(j && t[i] != t[j + 1]) j = ne[j];
if(t[i] == t[j + 1]) j ++ ;
ne[i] = j;
}
for(int i = 1, j = 0; i <= n; i ++ )
{
while(j && str[i] != t[j + 1]) j = ne[j];
if(str[i] == t[j + 1]) j ++ ;
if(j == m)
{
s[i - m + 1] ++ ;//在左端点+1
j = ne[j];
}
}
for(int i = 1; i <= n; i ++ ) //由于修改左端点的时候我们是在当前位置的前面+1,所以不能直接在匹配过程中求前缀和
s[i] += s[i - 1];
while(q -- )
{
int l, r;
cin >> l >> r;
r = r - m + 1;//右端点左移
if(l > r) cout << 0 << endl;
else cout << s[r] - s[l - 1] << endl;
}
return 0;
}

在右端点+1
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m, q;
int ne[N], s[N];
char str[N], t[N];
int main()
{
cin >> n >> m >> q;
cin >> (str + 1) >> (t + 1);
for(int i = 2, j = 0; i <= m; i ++ )
{
while(j && t[i] != t[j + 1]) j = ne[j];
if(t[i] == t[j + 1]) j ++ ;
ne[i] = j;
}
for(int i = 1, j = 0; i <= n; i ++ )
{
s[i] = s[i - 1];
while(j && str[i] != t[j + 1]) j = ne[j];
if(str[i] == t[j + 1]) j ++ ;
if(j == m)
{
s[i] ++ ;
j = ne[j];
}
}
// for(int i = 1; i <= n; i ++ ) cout << s[i] << " ";
// cout << endl;
while(q -- )
{
int l, r;
cin >> l >> r;
l += m - 1;
if(l > r) cout << 0 << endl;
else cout << s[r] - s[l - 1] << endl;
}
return 0;
}

KMP算法和next数组的性质
目录
1.匹配字符串:831. KMP字符串 - AcWing题库\](#1.匹配字符串:831. KMP字符串 - AcWing题库)
[next数组具有周期性](#next数组具有周期性)
\[2.next周期的应用:141. 周期 - AcWing题库\](#2.next周期的应用:141. 周期 - AcWing题库)
\[3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库\](#3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库)
\[4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库\](#4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库)
*** ** * ** ***
### 模板
**next\[i\]表示以i结尾的后缀中与其匹配的最大前缀的长度**
```cpp
求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题库](https://www.acwing.com/problem/content/833/)
**AC代码**
```cpp
#include
假设下面的区间就是模板串移动后的位置,沿垂直方向做下一个移动后的模板串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代码
cpp
#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];
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代码
cpp
#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开始,理解两个代码的不同之处
cpp
#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;
}

cpp
#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
题目
https://www.acwing.com/activity/content/problem/content/869/
https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/
https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/