题目描述
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 PP。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤10^5
1≤M≤10^6输入样例:
3 aba 5 ababa
输出样例:
0 2
思路分析
(默认输入字符串时,第一个字符存在下标为一的位置)首先最先先想到暴力法,假设当i=3时,s[i]==p[1],就依次来判断接下来的字符是否全部匹配,若中间有某个字符不匹配,指针就i++,即从i=4去判断s[i]与p[1]是否相同,再次重复上述步骤。
这种做法时间复杂度较大,所以我们要优化(上述红字的过程)当寻找到首字符匹配,但中间有一段不匹配时,i重新选择开始匹配的位置,不要i++,因为这样会重复判断。
举一个例子先
假设有一串P为: abacabd
我们在一串S为 a b a c a b a c a b d找到与P相匹配的子串。
j: 1 2 3 4 5 6 7 8 9 10 11
S: a b a c a b a c a b d
P: a b a c a b d
!!!
在!!!的地方出现不匹配后,如果暴力,将会从j=3的位置开始重新搜索(这时候是ac开头,不匹配),但是我们可以发现实际上可以直接从j=5的地方开始,因为以j=6处为结尾的最大后缀ab(j=5,j=6)与开头前缀ab(j=1,j=2)相同,那么我们可以直接从j=5的地方开始,这是一个技巧,避免中间多次无意义的搜寻。我们令ne[j]为以j位置的字符为结尾的,与以p[1]开头的相同字符串的最大后缀字符串的首位置。
1 2 3 4 5 6 7
例如: a b c d a b c
那么此时j=7 的ne[7]=5;
最大后缀字符串为[5~7]
因为[1~3]=[5~7]={a b c};
但可能有疑问的地方是万一中间位置也有与P相同开头的子串呢
例如
P:a b c d
i : 1 2 3 4 5 6 7 8 9
S: a b c a b c a b c d
!!!
那么中间 4 5 6位置与P开头的a b c 也相同,所以应该从4位置开始,我们求的ne[9]也是4,因为ne求的是最大后缀,所以[1~6]==[4~9],[4~9]为最大后缀子串;
假设ne已知,我们可以写出KMP主体代码。
代码中的j初始值设为0,当下一个位置匹配时,再j++,例如j=2时说明第2个字符已完成匹配成功。
#include<iostream>
using namespace std;
int n,m;
char p[100010];
char s[1000010];
int ne[100010];
int main()
{
cin>>n>>p+1>>m>>s+1;
for(int i=1,j=0;i<=m;i++)
{
//当P已经开始匹配时,中间遇到不匹配的字符时,从ne[j]开始再次匹配
while(j&&s[i]!=p[j+1]) j=ne[j];
//如果S字符串第i位置和P字符串待匹配的j+1位置(j位置是匹配过的)j就继续往右移
if(s[i]==p[j+1]) ++j;
//j等于n时,说明匹配成功
if(j==n)
{
cout<<i-n<<" ";
j=ne[j];//这里与上述图中效果相同,直接找到ne[j]位置与字符串P继续新的匹配。
}
}
return 0;
}
那么接下来只要补全ne的代码就可以了
//i直接从2开始,因为1的时候前面是空的,ne[1]=0;
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的过程仔细想想和上面S和P串的配对过程是一样的,只不过是变成了P和P自己配对的过程
举例说明一下ne的执行过程就好理解一点
假设P字符串为 a b a c a b a c
1.
a b a c a b a c
i
abacabac
j=0
- 因为j=0,所以while循环不进;
- i=2,j=0,因为p[i]=b,p[j+1]=a;
- 所以ne[2]=j=0;
2.i++;
a b a c a b a c
i
abacabac
j=0
- 因为j=0,所以while循环不进;
- i=3,j=0,因为p[i]=a,p[j+1]=a,所以j++;
- 所以ne[3]=j=1;
3.i++;
a b a c a b a c
i
abacabac
j=1
- 因为j=1,且p[i]=c,p[j+1]=b,所以进入while循环,j=ne[j]=ne[1]=0,所以j从0开始,回到开头;(这一步和一开始上文中思路分析的原理图所做的步骤一样且目的也相同)
- i=4,j=1~>0,因为p[i]=c,p[j+1]=a;
- 所以ne[4]=j=0;
4.i++;
a b a c a b a c
i
abacabac
j=0
- 因为j=0,所以while循环不进;
- i=5,j=0,因为p[i]=a,p[j+1]=a,所以j++;
- 所以ne[5]=j=1;
5.i++;
a b a c a b a c
i
abacabac
j=1
- 因为j=1,p[i]=b,p[j+1]=b,所以while循环不进;
- i=6,j=1,因为p[i]=b,p[j+1]=b,所以j++;
- 所以ne[6]=j=2;
6.i++;
a b a c a b a c
i
abacabac
j=2
- 因为j=2,p[i]=a,p[j+1]=a,所以while循环不进;
- i=7,j=2,因为p[i]=b,p[j+1]=b,所以j++;
- 所以ne[7]=j=3;
7.i++;
a b a c a b a c
i
abacabac
j=3
- 因为j=3,p[i]=c,p[j+1]=c,所以while循环不进;
- i=8,j=3,因为p[i]=c,p[j+1]=c,所以j++;
- 所以ne[8]=j=4;
至此整个操作结束。可以复盘一下整个过程是否正确。
个人认为此题难以讲解透彻,多点画实际图去理解,背下代码,因为代码几乎无需改动,可直接应用。