题目
给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。
输入格式
第一行输入整数N,表示字符串P的长度。
第二行输入字符串P。
第三行输入整数M,表示字符串S的长度。
第四行输入字符串S。
输出格式
共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2
题目分析
给出一个较长的字符串和短些的模板串,目的是在字符串中找到模板串,并返回匹配到的串中的首索引,如果有多个匹配,那么要返回多个数值.
令字符串的索引为i
,模板串的索引为j
朴素算法是i遍历,在每个i中,比较i指针和j指针的值是否相同,相同则j指针前移,i继续遍历(也就是i和j同时前行),不同则i继续遍历,j退回起点.
朴素做法的问题在于,每次j都是退回起点,从而浪费了匹配到的信息,如果能够少退回几格,那么效率就能得到很大提升.
算法分析
- next数组
明确数组的含义: 数组针对的是模板串,数组的索引表示的是模板串的某个位置的索引,数组的值表示的是模板串以某位置(i)为边界,前面的字符串的最长匹配的前后缀的长度.
eg: ababab|
在|前,最长的前后缀长度是4(abab),表示的含义是i前的字符串前面n个字符和后面n个字符完全相同.
Q:那么为什么要做这个工作呢?找到模板串任意位置的最长前后缀有什么意义呢?
A:充分利用之前匹配成功的信息,减少需要对比的次数. - 匹配过程
让i依次遍历,知道遍历到字符串的最后一个,表示字符串所有的字符都考虑完全.
首先要知道j指针的含义:表示j之前所有的字符都匹配了
在每次遍历中,观察i指针与j指针(代码的含义是j指针的下一个)所指的字符是否相同,如果相同,j指针前移,i本次遍历结束(i指针前移),最好的情况是有很多都匹配,这说明i和j都前移了很多字符,也就意味着此刻j前的所有字符都是匹配的
;如果不同,我们让j前移变为next[j],也就是说,我们的期望降低了,我们只认为是前缀匹配了,然后再次进行匹配,如果下一个字符匹配成功,那么继续前移;如果下一个字符匹配失败,说明我们还需要继续降低期望,变成"前缀的前缀"来进行匹配.依次类推.
算法实现
- next数组是如何实现的?
我们给出两个指针i=0,j=2
,j表示的就是边界,也即是我们要看的就是j前的最长前后缀,而i表示的是进行匹配的指针.
模板串下标从1开始
j之所以选择2是因为从2开始才有两个字符进行匹配,才有意义,i选择0是因为我们比较的是i+1和j的值,如果相同了i才进行移动.
现在,我们制定一个规则,只有当i+1和j的值相同时,两指针才进行移动,表示匹配成功;否则,i前移至next[i],继续进行匹配,这样,i一直指向的是匹配后缀的字符串,并且保证能增加时增加(最长),不能增加(不匹配)就退而求其次,继续匹配.
2.整体排序是如何实现的?
令字符串的索引为i
,模板串的索引为j
当i和j匹配时,j++,i到下一个循环(i++);
当i个j不匹配时,j退到next[j],继续进行匹配(while语句)
当j到达模板串的末尾,表示全部匹配,此时进行要求操作(输出,如果要匹配所有则强制让j进行回退到next[j]即可)
源代码
import java.io.*;
class Main{
public static void main(String[] args)throws Exception{
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
int n=Integer.parseInt(br.readLine());
String p1=br.readLine();
int[] ne=new int[100010];
char[] p=new char[100010];
char[] s=new char[1000010];
for(int i=1;i<=n;i++){
p[i]=p1.charAt(i-1);
}
int m=Integer.parseInt(br.readLine());
String s1=br.readLine();
for(int i=1;i<=m;i++){
s[i]=s1.charAt(i-1);
}
//建立ne数组,存储对于模式数组,所有i对应 匹配后缀的 最长前缀的 结尾.
for(int i=2,j=0;i<=n;i++){
//下一个J没匹配上
while(j>0&&p[i]!=p[j+1])
//说明J太长了,那么我们还要找满足i附近的后缀,而恰J能满足,所以J的自己也是有戏的,因此J回退
//换句话说,一开始的J能匹配i向前很多个字符,但是再往下1个就不能满足了(不能满足后缀前缀匹配,而且能变的其实只有后缀前缀的长度,i其实是不能动的)
//因此当前的J是不满足条件的,要向前搜索,缩小前后缀的长度,以J为后缀端点,
j=ne[j];
//这句的含义是如果J有值才让j++,否则J是不应该加的
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
//进行匹配
//字符串下角标从1开始,i表示字符串,J表示模板串
for(int i=1,j=0;i<=m;i++){
//同样在没有匹配时不应该进行回退操作(因为没有回退可退了)
while(j>0&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n){
bw.write((i-n)+" ");
j=ne[j];
}
}
bw.flush();
br.close();
bw.close();
}
}
说明
由于测试序列比较多,为了防止TLE,不能使用Scanner,而要用BF/BW,而且,对于bw的flush操作,不要每次循环都flush,这样会多很多次无用的操作,在全写入缓冲区后统一进行flush即可
//这里感谢帮忙的小伙伴找到问题
之前说进行一次操作flush一次主要是保存数据,防止数据丢失,算法题是不需要考虑这些的.