KMP字符串
给定一个字符串 S S S,以及一个模式串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P P P 在字符串 S S S 中多次作为子串出现。
求出模式串 P P P 在字符串 S S S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N N N,表示字符串 P P P 的长度。
第二行输入字符串 P P P。
第三行输入整数 M M M,表示字符串 S S S 的长度。
第四行输入字符串 S S S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 0 0 开始计数),整数之间用空格隔开。
数据范围
1
≤
N
≤
1
0
5
1 \le N \le 10^5
1≤N≤105
1
≤
M
≤
1
0
6
1 \le M \le 10^6
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2
y总AC代码
#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); // 输入长度和字符串
// 构造 next 数组
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;
}
// KMP 匹配过程
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)
{
cout << i - n << " ";
j = ne[j];
}
}
cout << endl;
return 0;
}
第一段:构建 next
数组
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;
}
i=2, j=0
:
- 我们从
i=2
开始计算,因为ne[1]=0
已知(长度为1的字符串,前后缀必然为空集)。 j
表示“当前已匹配的前缀长度”。
while (j && p[i] != p[j+1])
:
- 如果
p[i] ≠ p[j+1]
,就尝试缩短前缀长度:- 退回到上一个最长公共前后缀:
j = ne[j]
。
- 退回到上一个最长公共前后缀:
- 目的:找到一个次长公共前后缀,使得尝试继续匹配。
if (p[i] == p[j+1]) j++;
:
- 如果字符相等,说明可以把当前最长公共前后缀长度
j
增加1。
ne[i] = j;
:
- 保存长度为
i
的子串的最长公共前后缀长度。
第二段:主串匹配
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)
{
cout << i - n<<" ";
j = ne[j];
}
}
i=1, j=0
:
i
遍历主串s
。j
表示当前匹配的模式串长度。
while (j && s[i] != p[j+1]) j = ne[j];
- 如果
s[i] ≠ p[j+1]
:- 说明失配了。
- 根据
ne[j]
快速回退模式串,找到次长公共前后缀。
if (s[i] == p[j+1]) j++;
- 如果字符匹配成功,继续比较下一个字符。
if (j == n)
- 当
j==n
,说明找到一个完整的匹配:- 输出匹配起始位置:
i-n
。 - 重置
j = ne[j]
,准备寻找下一个匹配。
- 输出匹配起始位置:
要点1:
为什么 p+1
/ s+1
,从下标 1 开始?
C++ 默认数组是 0-based,但 KMP 代码里用了 1-based,原因:
1.从 1 开始,可以让:
- 模式串第一个字符是
p[1]
- 前缀长度自然从 1 计数
- 代码里
j+1
和数组位置一致。
2. 与 next
逻辑对齐
next
数组中ne[i]
表示:- “p[1…i] 的最长公共前后缀长度”
- 如果下标从0开始:
- 就需要写成
p[0..i-1]
,会导致逻辑偏移,计算j+1
时也不直观。
- 就需要写成
3. 避免 -1 越界问题
- 例如匹配失败时,
while (j && s[i] != p[j+1])
:- 如果下标从 0 开始,当
j=0
还要访问p[j]
,可能出现越界。
- 如果下标从 0 开始,当
- 从 1 开始就不会有这个问题。
下标从1开始的好处 | 原因 |
---|---|
代码逻辑简化 | 不需要频繁调整偏移量 |
容易处理边界情况 | 避免-1越界问题 |
更符合算法思想 | next[j] 和 p[j+1] 对齐 |
要点2:
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;
}
怎么保证 ne[i]=j的时候 p[1…j] == p[i-j+1…i]?
如果 p[i] == p[j+1]
:
-
当前前缀长度
j
可以扩展 1。- 说明:
p[1..j] == p[i-j..i-1]
并且p[j+1] == p[i]
所以:
p[1..j+1] == p[i-j..i]
- 说明:
如果 p[i] != p[j+1]
:
- 不断回退
j = ne[j]
- 直到找到一个较短的前后缀,使得
p[1..j] == p[i-j+1..i-1]
- 再尝试
p[i] == p[j+1]
要点3
char s[M], p[N];
cin >> n >> p + 1 >> m >> s + 1;
p+1
/s+1
表示什么?
p+1
的意思是 指针偏移:
p+1 == &p[1]
它指向数组的第二个元素(即 p[1]
)
效果:让用户输入的字符串存放在 p[1]
开始的位置。
要点4
-
在
<iterator>
头文件中,C++11 引入了一个叫做std::next
的模板函数:template <class ForwardIterator> ForwardIterator next(ForwardIterator it, typename iterator_traits<ForwardIterator>::difference_type n = 1);
-
它的作用是:返回一个迭代器,表示在
it
基础上前进n
步后的位置。
所以 next
在全局作用域中被占用了。
如果直接写:
int next[N];
编译器可能会报错或者和 <iterator>
里的 std::next
冲突。