简单来说,该题是给一些字符串,字符串长度最长为一百万,要求——如果找到这些字符串各个前缀的最小循环节,输出该前缀的长度和该前缀的循环节循环的次数。
循环节:若一个字符串由重复出现的子串组成,该子串就是循环节。
注意字符串并不是其本身的循环节,因为并没有子串并没有重复。因此对于只有单个字符的前缀必然没有循环节
如果用暴力算法会超时,我自己起初编了一个不带脑子的暴力算法,检测字符串长度为1000时就超时了。为甚么会用kmp算法,kmp算法怎么用在这个上面
import java.util.ArrayList;
import java.util.Scanner;
public class Main{
public static void main(String[]args) {
Scanner sc=new Scanner(System.in);
int n=1;
ArrayList<String> s = new ArrayList<String>();
while(true) {
n=sc.nextInt();
if(n==0) {
break;
}
s.add(sc.next());
}//读取,并存入了字符串
for(int i=0;i<s.size();i++) {//遍历各个字符串
System.out.println("Test case #"+(i+1));
for(int j=1;j<s.get(i).length();j++) {//遍历某字符串的前缀(除去最短的那个)
String str=s.get(i).substring(0, j+1);
for(int k=1;k<str.length();k++) {//遍历找子串
String stri=str.substring(0,k);
int fir=0,child=0;//准备指针进行匹配
int sum=0;//记下子串的重复次数
while(fir!=str.length()) {
if(str.charAt(fir)!=str.charAt(child)) {
child=-1;//标记该子串不是重复字串
break;
}
child++;
fir++;
if(child==stri.length()) {
child=0;
sum++;
}
}
if(child!=-1&sum>1&child==0) {//找到,输出
System.out.println(str.length()+" "+sum);
break;
}
}
}
System.out.println();
}
}
}
AcWing推荐该题用KMP算法解决。
KMP算法
应用场景是找模式串在主串中的位置
以上链接是大佬的kmp算法原理讲解
实现kmp算法首先要找到最长公共前后缀,将模式串各个前缀的最长公共前后缀长度计入一个数组,通常该数组被称为next数组。
java实现的next数组模板代码
public static int[] CreateNext(String str) {
int[] next = new int[str.length()];
next[0] = 0;//单个字符没有公共前后缀
for(int i = 1, j = 0; i < str.length(); i++) {//j指向前缀末位,i指向后缀末位
while(j > 0 && str.charAt(i) != str.charAt(j)) {
j = next[j - 1];
}//该前缀的末位字符不等于该后缀的末位字符,回溯前缀末位直到两者末尾相等为止
if(str.charAt(i) == str.charAt(j)) {
j++;
}//找到最长公共前后缀,j自增1,既能表示最长公共前缀长度,又能指向下一个前缀的末位
next[i] = j;
}
return next;
}
为什么模式串最短前缀最长公共前后缀一定为0:
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。个人理解主串最短前缀长度为1,开头即是结尾
为什么next数组代码中只比较前后缀末尾字符就能得到最大公共前后缀
因为是字符串由短到长取前缀比较的,举个例子,"aaa",取前缀"aa",前缀末位a(下标0)等于后缀末位a(下标1),最长公共前后缀长度1;取前缀"aaa",此时前缀从a到aa,后缀从a到aa,前缀末位a(下标1)等于后缀末位a(下标2)。那么前后缀的首位用不用比较?不用,在上一次比较中就确定了a(下标0)等于a(下标1)。
若题目为找模式串在主串中的位置,则利用next数组,当发现不匹配的时候从模式串的最长公共前缀部分滑动到后缀
public static int Kmp(String str1, String str2, int[] next) {
for(int i = 0, j = 0; i < str1.length(); i++) {
while(j > 0 && str1.charAt(i) != str2.charAt(j)) { //不匹配的情况下
j = next[j -1]; //通过next数组回溯j
}
if(str1.charAt(i) == str2.charAt(j)) { //匹配的情况下
j++;
}
if(j == str2.length()) { //匹配成功返回第一次出现该字符串的位置
return i - j +1;
}
}
return -1; //匹配不成功返回-1
}
回到这个题。。。本题利用的next数组和循环节的关系:1.T=i-next[i],最短循环节的长度等于子串长度减去该字串最长公共前后缀的长度2.若子串有循环节,那么子串长度是循环节的倍数。
性质的证明以及题解看这里
AcWing 141. 周期(蓝桥杯集训·每日一题) - AcWing
AC代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=1,i=0;
String s,str;
while(true) {
n=sc.nextInt();
if(n==0) {
break;
}
s=sc.next();
System.out.println("Test case #"+(++i));
int []next=CreateNext(s);
for(int j=1;j<s.length();j++) {
if(j+1-next[j]!=j+1&(j+1)%(j+1-next[j])==0) {
int sum=(j+1)/(j+1-next[j]);
System.out.println(j+1+" "+sum);
}
}
System.out.println();
}
}
public static int[] CreateNext(String str) {
int[] next = new int[str.length()];
next[0] = 0;
for(int i = 1, j = 0; i < str.length(); i++) {
while(j > 0 && str.charAt(i) != str.charAt(j)) {
j = next[j - 1];
}
if(str.charAt(i) == str.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
下面是我一开始用的代码,与上面AC的代码不同之处就是在第二层循环里多用了一个substring()方法,虽然输出结果一样但是超时。。。
while(true) {
n=sc.nextInt();
if(n==0) {
break;
}
s=sc.next();
System.out.println("Test case #"+(i+1));
int []next=CreateNext(s);
for(int j=1;j<s.length();j++) {
str=s.substring(0, j+1);
if(j+1-next[j]!=j+1&(j+1)%(j+1-next[j])==0) {
int sum=(j+1)/(j+1-next[j]);
System.out.println(str.length()+" "+sum);
}
}
}
}