常用API
String m = "abcde";
char ch = m.charAt(String n);//获取字符串m的第(n+1)个字符
int length = m.length();//获取字符串m的长度
boolean flag = m.equals(String n);//判断字符串m和n是否相等,严格区分大小写
boolean flag = m.equalsIgnoreCase(String n);//判断字符串m和n是否相等,不区分大小写
int len = m.index(String s);//返回字符串s在m中第一次出现的位置
int compare = m.compareTo(String anotherString);//按字典序比较两个字符串,若compare>0,m大,若compare<0,m小
String s = m.concat(n);//将字符串n拼接到字符串m的结尾
boolean flag = m.contains(String n);//判断字符串m是否包含字符串n
boolean flag = m.endsWith(String s);//判断字符串m是否以字符串s结尾
String[] s = m.split(" ");//根据正则表达式拆分字符串m
String s = m.trim();//删除字符串m的前导空格和尾部空格
String s = m.subString(int i,int j);//截取字符串m中下标为i至下标为j-1的部分,即[i,j);
...
周期串
思路:从 1 1 1 开始枚举周期 T T T 的大小,然后判断每个周期内的对应字符是否相同,如果不同,则直接判断下一个 T T T 。
public static int cycle(String s){
char[] ch = s.toCharArray();
int T;
for(T=1;T<=ch.length;T++){
if(ch.length%T==0){//周期串的长度一定是周期T的倍数
boolean flag = true;
for(int start = T;start<ch.length;start++){
if(ch[start]!=ch[start%T]){
flag = false;
break;
}
}
if(flag){
break;
}
}
}
return T;
}
思路: p o s pos pos 表示第二行的字符串向右移动的格数,如果移动后,第二行的字符串与第一行字符串对应位置的字符全部相同,则 p o s pos pos 就是这个字符串的周期。
public static int cycle(String s){
String m = s+s;
int pos;
for(pos=1;pos<=s.length();pos++){
if(s.length()%pos!=0)
continue;
String x = m.substring(pos,pos+s.length());
if(x.equals(s))
break;
}
return pos;
}
思路:如果一个字符串 s u b sub sub 是字符串 s s s 的周期,那么将字符串 s s s 中所有的 s u b sub sub 全部替换为空字符串之后,字符串的长度如果为 0 0 0 ,就表示字符串 s u b sub sub 是字符串 s s s 的周期。
public static int cycle(String s){
for(int i=1;i<=s.length();i++){
if(s.length()%i==0){
String sub = s.substring(0,i);
if(s.replace(sub,"").length()==0)
return i;
}
}
return 0;
}
例题:重复字符串
题目链接:重复字符串 - 蓝桥云课 (lanqiao.cn)
思路:已知重复次数为 K K K ,那么周期就是 S . l e n g t h ( ) / K S.length() / K S.length()/K ,然后只需要求出每一个周期的第 i i i 个字符,出现次数最多的字符是哪个,然后将其余字符全部改为它,那么就将 S S S 改为了重复 K K K 次的字符串,此时修改次数也是最少的。以 a b d c b b c a a b c a abdcbbcaabca abdcbbcaabca , 重复 3 3 3 次为例:
将此字符串拆分为三个部分后,每个周期写在一行,结果为:
a
b
d
c
abdc
abdc
b
b
c
a
bbca
bbca
a
b
c
a
abca
abca
只需要求出每一个竖列出现次数最多的字符出现的次数,然后将其余字符全部改为它,那么这一列修改次数为( K − m a x K - max K−max),然后将每一列的结果加起来,即为答案。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
public class 重复字符串 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws IOException {
int n = Integer.parseInt(in.readLine());
String a = in.readLine();
if(a.length()%n!=0 || n>a.length()) {
System.out.println(-1);
return;
}
int t = a.length() / n;
int index = 0;
char[][] ch = new char[n][t];
for(int i=0;i<n;i++)
for(int j=0;j<t;j++)
ch[i][j] = a.charAt(index++);
int ans = 0;
for(int i=0;i<t;i++) {
int[] num = new int[26];
int max = 0;
for(int j=0;j<n;j++) {
num[ch[j][i]-'a']++;
if(max<num[ch[j][i]-'a']) {
max = num[ch[j][i]-'a'];
}
}
ans = ans + (n-max);
}
System.out.println(ans);
}
}
最长回文子串
字符串的最长回文子串,是指一个字符串中包含的最长的回文子串。
如果在 " a b c d e f f g h abcdeffgh abcdeffgh " 包含的最长回文子串就是 " f f ff ff ";
朴素做法
思路:
1.找到字符串 S S S 的所有子串 S ′ [ ] S'[] S′[]
2.然后遍历所有的子串 S ′ [ ] S'[] S′[] ,然后分别判断每个子串 S ′ [ i ] S'[i] S′[i] 是否回文。
3.字符串 S S S 共计 n 2 n^2 n2 个子串,对于每个子串的验证是 O ( n ) O(n) O(n) 所以整体复杂度是 O ( n 3 ) O(n^3) O(n3)
import java.util.Scanner;
public class Main{
static String s;
static int isPal(int start,int end){ //s的[start,end)的左闭右开区间
int len=end-start;
//偶数abba型,对称轴位置没有字母
if(len%2==0){
int mid=start+len/2;
//找到mid的长度,只需要枚举一半即可。
int i=start;
//枚举起点
int pal=end-1;
//对应的回文的位置
for(;i<=mid;i++,pal--){
// i-----> 向中间枚举 <------pal
if(s.charAt(i)!=s.charAt(pal))
return -1;
//有一处不回文,那么整体不回文,返回-1 代表不回文。
}
return len;
//都回文那么整体回文,返回长度
}
//奇数aba型,对称轴位置有字母
else{
int mid=start+((len-1)/2);
//找到mid的长度,只需要枚举一半即可,因为对称轴字母必然和自己对称所以可以不考虑
int i=start;
//枚举起点
int pal=end-1;
//对应的回文的位置
for(;i<=mid;i++,pal--){
// i-----> 向中间枚举 <------pal
if(s.charAt(i)!=s.charAt(pal))
return -1;
//有一处不回文,那么整体不回文,返回-1 代表不回文
}
return len;
}
}
//分解子串,并记录最长回文子串的长度
static int subString(){
int maxn=1;
//最短是1,比如a,b,c,d
int len=s.length();//字符串长度
for(int i=0;i<=len;i++) {//枚举左闭端点
for(int j=i+1;j<=len;j++){//枚举右开端点
maxn=Math.max(maxn, isPal(i,j));
}
}
return maxn;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
s=in.next();
System.out.println(subString());
}
}
中心拓展法
思路:
回文串都是回文的,即所见即所得,既然回文它就有必然有一个对称中心。通过枚举对称中心的位置,进行扩展那么,就可以完成对回文字符串的判定。
那我们就要执行以下操作:
1.枚举中心位置 n 个字符和 n-1 个字符中间位置(偶数回文)
2.以中心位置向两侧延伸,直至不是回文位置
3.在枚举过程中统计最大的回文子串
import java.util.Scanner;
public class Main{
static String s;
public static int expand(int l, int r) {
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
//如果回文处相同且不超过字符串的范围:
l--;
r++;
// 扩展
}
return r - l - 1;
//不能扩展返回最大长度
}
public static int Palindromic() {
if (s == null || s.length() == 0) {
return 0;
}
int maxn=1;
for (int i = 0; i < s.length(); i++) {
int l1 = expand(i, i); // 以字符作为中心点扩展
int l2 = expand(i, i + 1); // 以字符间隙作为中心点扩展
int len = Math.max(l1, l2);
maxn=Math.max(maxn,len);
}
return maxn;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
s=in.next();
System.out.println(Palindromic());
}
}
弊端:
1.由于长度的奇偶性问题,不同的对称轴要分两类情况讨论分析。
2.有多次重复计算。如 c b c a c b c cbcacbc cbcacbc 第一个 c b c cbc cbc 被计算过,第二个 c b c cbc cbc 也被计算过,一整个个 c b c a b c b c cbcabcbc cbcabcbc 又被计算了一次。
M a n a c h a r Manachar Manachar 算法
思路:
1.预处理:在所有字符间隙中和开头结尾插入同样的符号,一般使用 # \# # ,当然只要不影响原字符串本身的符号(即不在原来的字符串中出现)都是可以的。这样可以使的所有的字符串都是奇数串,即消除了奇偶变化所带来的的差异化处理。为了防止越界处理等,一般在开头和结尾在缀上两个不一样的字符。
2. M a x Max Max:最远标记距离,即目前最靠右的回文串的右端点;
P o s Pos Pos: 最远中心点,即目前最靠右的回文串的中心点;
l e n g t h [ i ] length[i] length[i]:回文半径,以 i i i 为中心扩展的回文串的半径,恰好比不扩展的原字符串的直径大 1 1 1;
3.假设前 i − 1 i-1 i−1 个字符已经被处理过,那么将处理第 i i i 个字符,则会有下面的情况。设 j j j 为 i i i 关于 P o s Pos Pos 的对称点, j = 2 × p o s − i j = 2 \times pos - i j=2×pos−i, M a x L MaxL MaxL 为 M a x Max Max 关于 P o s Pos Pos 的对称点。 M a x L = 2 × P o s − M a x MaxL = 2 \times Pos - Max MaxL=2×Pos−Max
4.当 i < M a x i<Max i<Max 时;
当以 j j j 为中心的回文串被 M a x Max Max 串包含:
此时,由于 M a x Max Max 串关于 P o s Pos Pos 回文,所以以 j j j 为对称轴回文的串,也会在 P o s Pos Pos 右侧以 i i i 为对称轴出现。
所以可以得知 l e n g t h [ i ] = l e n g t h [ j ] = l e n g t h [ 2 × P o s − i ] length[i] = length [j] = length[ 2\times Pos -i ] length[i]=length[j]=length[2×Pos−i]。
当以 j j j 为中心的回文串范围超出 M a x Max Max 串:
即 j − l e n g t h [ j ] < M a x L j-length[j] < MaxL j−length[j]<MaxL 时;
不能保证超出 M a x L MaxL MaxL 左侧的字符也会在 M a x Max Max 的右侧出现,所以只能最大限度的考虑 i i i 的回文半径,那就是 M a x − i Max - i Max−i
可以综合考虑二者,因为他们之间肯定存在一个大小关系,如果是第一种情况,那么有 l e n g t h [ 2 × P o s − i ] < = M a x − i length[2\times Pos -i ] <= Max -i length[2×Pos−i]<=Max−i , 如果是第二种情况,那么就有个 l e n g t h [ 2 × P o s − i ] > = M a x − i length[2\times Pos -i ] >= Max -i length[2×Pos−i]>=Max−i 的情况。
因此直接使用最短那个即可。
即当 i < M a x i < Max i<Max 时: l e n g t h [ i ] = m i n ( l e n g t h [ 2 × p o s − i ] , M a x − i ) length[i]=min(length[2\times pos-i],Max-i) length[i]=min(length[2×pos−i],Max−i);
5. i ≥ M a x i\ge Max i≥Max 的情况
此时的 i i i 并不能参考 j j j 的处理情况,也无其他规律可循,只能按照朴素的算法处理。
import java.util.Scanner;
public class Main{
static String s;
public static int manacher(String p){
int le=p.length();
s="$";//前缀
//插入分隔符,奇数化
for(int i=0;i<le;i++){
s=s + '#';
s=s + p.charAt(i);
}
s=s+'#';;
// 前后缀,不用考虑边界处理就能不越界
s=s+'%';//选取不可能出现的字符即可。
int[] length =new int[s.length()];
//预处理字符串成为一个奇数串;
int Max=0,pos=0,ans=0;
le=2*le+1;
for(int i=1;i<=le;i++){
if(Max>i){
length[i]=Math.min(length[2*pos-i],Max-i);
}
else length[i]=1;
//尝试扩展
while(s.charAt(i-length[i])==s.charAt(i+length[i]))
length[i]++;
ans =Math.max(length[i],ans);
if(i+length[i]>Max){
Max=length[i]+i;
pos=i;
}
}
return ans-1;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
String p=in.next();
System.out.println(manacher(p));
}
}
字符串匹配问题
B F BF BF 算法
思路: B F BF BF 算法是一种纯暴力的字符串匹配算法,比较符合人类自然思维方式的方法,即对源字符串和目标字符串逐个字符地进行比较。算法过程,不断的比对,直到在主串中找到目标串或者,遍历完目标串。发现 B F BF BF 算法每次 t t t 匹配失败都往后移动一个位置。
static String T="蓝桥的课程我很喜欢,蓝桥杯的题目很简单";
static String P="蓝桥杯";
static int BF(String tString, String pString){
int i=0; //主串中的位置
int j=0; // 模式串中的位置
for(;i<=tString.length()-pString.length();i++){
boolean isPatt= true;
for(;j<pString.length();j++){
if(tString.charAt(i+j)!=pString.charAt(j)){
isPatt= false;
break;
//一旦不匹配,主串位置后退到i,模式串位置j回退到0;
}
// 当两个字符相同,就比较下一个
}
if(isPatt) return i;
}
return -1;
}
public static void main(String[] args) {
System.out.println(BF(T,P));
}
换一种形式
static String T="ABCDEFG";
static String P="FG";
static int BF(String s,String p) {
int i = 0; //主串中的位置
int j = 0; // 模式串中的位置
int sLen = s.length();
int pLen = p.length();
while (i < sLen && j < pLen) {
if (s.charAt(i)==p.charAt(j)) { // 当两个字符相同,就比较下一个
i++;
j++;
} else {
i = i - j + 1;
j = 0; //一旦不匹配,模式串位置j回退到0;
}
// System.out.println(i+":"+j);
}
if (j == pLen)
return (i - j); //主串中存在该模式返回下标号
else
return -1; //主串中不存在该模式
}
public static void main(String[] args) {
System.out.println(BF(T,P));
}
K M P KMP KMP 算法
**核心思路:**利用已经匹配过的这一部分有效的信息,保持主串位置 i i i 的值不变,去修改模式串的位置 j j j 的值。
**字符串前缀:**符号串左部的任意子串(或者说是字符串的任意首部),在 K M P KMP KMP 算法中使用的是“真”前缀,即不包含自己前缀。简单记忆方式: 前缀要找除了自己,且从头开始的所有子串。
**字符串后缀:**符号串右部的任意子串(或者说是字符串的任意尾部),在 K M P KMP KMP 算法中使用的是“真”后缀,即不包含自己后缀。简单记忆方式: 前缀要找除了自己,且以最后一个字符结尾的所有子串。
N e x t Next Next数组的含义:
对于模式串" a b a a b c a abaabca abaabca"而言
出现这种从第 1 1 1 位就匹配出错的情况,即使通过人工不能找到任何优化方式,于是只能将模式串右移。
恰好模式串的 − 1 -1 −1 的位置正好置于主串的 i i i 位置,这就是 n e x t next next 的第一位补 − 1 -1 −1 的原因。
再看第二种情况,部分匹配成功的情况。
仍对于模式串" a b a a b c a abaabca abaabca"而言:
设主串为" a b a a e f a g e d abaaefaged abaaefaged",那么会有:
在主串的 i = 4 i=4 i=4 位置,模式串 j = 4 j=4 j=4 的位置发生了不匹配。
通过最优人工移动可以得到:
恰好与主串 i i i 位置对应的值是模式串的 1 1 1 号位置。
那么与计算出的 N e x t Next Next 的数组值相同。
原因:
假设某个字符串 s s s 的最长共前后缀为 X = " a b c d . . . " X="abcd..." X="abcd...",那么这个字符串一定是一下结构,开头是 X X X 结尾是 X X X 中间可能会有重叠,匹配到 s s s 的最后一个字符失败后,那知道 X X X 肯定是匹配成功了,因为 X X X 不包含最后一个字符。
既然知道 X X X 匹配成功,那么一定知道,在主串中一定是从 i i i 位置开始且一定有一个 X X X 与模式串中的 X X X 匹配成功。
如下,点为省略号:
i
T=........X.......
S= X.......
j
又已知,字符串 s s s 一定有一个后缀 X X X,那么直接用 s s s 的后缀 X X X 去匹配主串的 X X X,且 X X X 是最长公共前后缀,那么就完成了最优的转移。
当 s s s 是模式串的从头开始的子串时,就可以得到从某一个字符不匹配时的转移情况。
K M P KMP KMP 算法框架和 B F BF BF 大致相似,根据上面的分析,对于字符串的比对我们分为三种。
-
T [ i ] = = P [ j ] T[i]==P[j] T[i]==P[j] 的情况
此时,两字符相同应该继续比对所以:
i = i + 1 i=i+1 i=i+1
j = j + 1 j=j+1 j=j+1
-
T [ i ] ! = P [ j ] T[i]!=P[j] T[i]!=P[j] 的情况
此时,两字符不相同, j j j 应该根据 n e x t next next 数组进行转移,所以:
j = n e x t [ j ] j=next[j] j=next[j]
-
j = − 1 j=-1 j=−1 的情况
因为, j = n e x t [ j ] j=next[j] j=next[j],且 n e x t next next 第一位为 − 1 -1 −1 即出现了第一位就匹配失败的情况,那应该做的是,是的模式串的开头向后移动,即:
j = j + 1 j=j+1 j=j+1
那么 j j j 对应 i i i 的位置也变成了 i + 1 i+1 i+1 所以:
i = i + 1 i=i+1 i=i+1
那么与情况 1 1 1 相同,一同考虑。
public class Main {
static String T = "ABCDEFG";
static String P = "AB";
static int[] next = new int[100005];
static void Getnext(String p) {
int pLen = p.length();
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1) {
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p.charAt(j) == p.charAt(k)) {
++k;
++j;
next[j] = k;
} else {
k = next[k];
}
}
}
static int KMP(int sStart, String s, String p) {
/*
tStart 从主串的哪个位置开始,从头开始为0
s 主串
p 模式串
*/
int i = sStart; //从主串的sStart位置开始
int j = 0;
int sLen = s.length();
int pLen = p.length();
while (i < sLen && j < pLen) {
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s.charAt(i) == p.charAt(j)) {
i++; //继续对下一个字符比较
j++; //模式串向右滑动
} else {
j = next[j];
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = nextt[j]
//nextt[j]即为j所对应的nextt值
}
}
if (j == pLen)
return (i - j); //主串中存在该模式返回下标号
else
return -1; //主串中不存在该模式
}
public static void main(String[] args) {
Getnext(P);
System.out.println(KMP(0, T, P));
}
}