题意整理
- 给定两个字符串S1和S2。
- 求S2在S1的哪一个循环同构串中出现次数最多。
- 如果有多个这样的同构串,输出字典序最小的那一个。
方法一(kmp)
1.解题思路
- 首先初始化计数数组。
- 通过kmp算法,获取S2在同构串中出现的次数。
- 记录最大的出现次数。
- 通过最大出现次数定位到同构串起点索引,找到字典序最小的那一个。
通过kmp算法计算模式串在主串中出现次数,可参考我在牛客的另一篇题解:kmp题解入口
动图展示:
2.代码实现
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param S1 string字符串 S1 * @param S2 string字符串 S2 * @return string字符串 */ public String CycleString (String S1, String S2) { //如果S2比S1长,S2不可能出现在S1的循环同构串中 if(S1.length()<S2.length()) return "IMPOSSIBLE"; int n=S1.length(); int m=S2.length(); //初始化计数数组 int[] cnt=new int[n]; //S1拷贝一份拼接在后面,用于获取循环同构串 S1=S1+S1; int max=0; for(int i=0;i<n;i++){ //通过kmp算法,获取S2在同构串中出现的次数 cnt[i]=getCnt(S2,S1,i); max=Math.max(max,cnt[i]); } if(max==0) return "IMPOSSIBLE"; String res=""; //选择最大出现次数中,字典序最小的同构串 for(int i=0;i<n;i++){ if(cnt[i]==max){ if(res.equals("")||res.compareTo(S1.substring(i,i+n))>0){ res=S1.substring(i,i+n); } } } return res; } //T是主串,S是模式串,计算S在T中出现次数(限制T的范围在start到start+n-1) private int getCnt(String S, String T,int start) { //特殊情况判断 int m=S.length(),n=T.length()/2; if(m>n||n==0) return 0; //初始化计数,获取next数组 int cnt=0; int[] next=getNext(S); //遍历主串和模式串 for(int i=start,j=0;i<start+n;i++){ //只要不相等,回退到next数组记录的下一位 while(j>0&&T.charAt(i)!=S.charAt(j)){ j=next[j-1]; } if(T.charAt(i)==S.charAt(j)) j++; //如果j为m,说明完全匹配一次 if(j==m){ //计数加一,索引回退到next数组记录的下一位 cnt++; j=next[j-1]; } } return cnt; } //获取next数组 private int[] getNext(String S){ int m=S.length(); int[] next=new int[m]; for(int i=1,j=0;i<m;i++){ //只要不相等,回退到next数组记录的下一位 while(j>0&&S.charAt(i)!=S.charAt(j)){ j=next[j-1]; } //前缀索引后移 if(S.charAt(i)==S.charAt(j)) j++; //确定应该回退到的下一个索引 next[i]=j; } return next; } }
3.复杂度分析
- 时间复杂度:假设S1的长度为n,S2的长度为m,kmp算法获取出现次数的时间复杂度是,总共有n个同构串,所以最终的时间复杂度是。
- 空间复杂度:需要额外大小为n的cnt数组,kmp中需要额外大小为m的next数组,所以空间复杂度是。
方法二(自定义哈希值)
1.解题思路
简要分析:由于所有的循环同构串都可以通过截取S1+S1中以某一下标为起点,长度为n(n是原S1串的长度)的子串获取,只要将字符串通过某种方式映射为哈希值,通过哈希值前缀和数组,就可以得到任意子串的对应哈希值,于是就可以找到与S2哈希值相等的长度为m的所有子串的起点,然后再构造一个记录匹配起点次数的前缀和数组,就可以得到S2在所有同构串中出现的次数,找到最大的出现次数。在所有最大的出现次数对应的同构串中,选择哈希值最小的,即是字典序最小的那个同构串。
基本步骤:
- 初始化hash数组和seed数组。
- 通过自定义的sed和哈希计算给hash数组和seed数组赋值。
- 计算S2在S1的循环同构串中出现的起点位置次数的前缀和,通过这个前缀和数组统计最多出现次数。
- 根据最多出现次数,找到对应的同构串,再通过哈希值得到字典序最小的同构串。
动图展示(哈希值计算):
2.代码实现
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param S1 string字符串 S1 * @param S2 string字符串 S2 * @return string字符串 */ //用于记录哈希值前缀和 int[] hash; //记录每一位的种子值 int[] seed; //设置种子值为26 int sed=26; public String CycleString (String S1, String S2) { //如果S2比S1长,S2不可能出现在S1的循环同构串中 if(S1.length()<S2.length()) return "IMPOSSIBLE"; //初始化hash数组和seed数组 int n=S1.length(); hash=new int[2*n+1]; seed=new int[2*n+1]; S1=S1+S1; char[] arr=S1.toCharArray(); seed[0]=1; //给hash数组和seed数组赋值 for(int i=1;i<=2*n;i++){ hash[i]=hash[i-1]*sed+arr[i-1]-'a'; seed[i]=seed[i-1]*sed; } int m=S2.length(); int hashOfS2=0; //计算S2的哈希值 for(int i=1;i<=m;i++){ hashOfS2=hashOfS2*sed+S2.charAt(i-1)-'a'; } //用于记录S2在S1的循环同构串中出现的起点位置次数的前缀和 int[] sum=new int[2*n+1]; for(int i=1;i+m-1<=2*n;i++){ if(hashOfS2==getHash(i,i+m-1)){ sum[i]=sum[i-1]+1; } else{ sum[i]=sum[i-1]; } } //记录在循环同构串中出现的最多次数 int maxCnt=0; for(int i=1;i<=n;i++){ //起点为i终点为i+n的同构串,它最后一次可能匹配到S2的起点在i+n-(m-1) maxCnt=Math.max(maxCnt,sum[i+n-(m-1)]-sum[i-1]); } //如果为0,直接返回"IMPOSSIBLE" if(maxCnt==0) return "IMPOSSIBLE"; //记录字典序最小同构串的起点位置 int start=-1; for(int i=1;i<=n;i++){ if(maxCnt==sum[i+n-(m-1)]-sum[i-1]){ if(start==-1||getHash(start,start+m-1)>getHash(i,i+m-1)){ start=i; } } } //因为计算哈希值时,下标是从1开始的,这里要还原 return S1.substring(start-1,start-1+n); } //计算起点为l,终点为r的字符串的哈希值 private int getHash(int l,int r){ //因为l-1在左边,是高位,要补齐到和r一样的长度 return hash[r]-hash[l-1]*seed[r-l+1]; } }
3.复杂度分析
- 时间复杂度:假设S1的长度为n,S2的长度为m,所有的循环都是线性的,所以时间复杂度是。
- 空间复杂度:需要额外大小为的hash数组、seed数组和sum数组,空间复杂度是。