突然发现模板挺重要的,用别人写的模板去写题感觉有点不舒服,还是自己总结一下出个板子好了,适合自己的才是最好的。就先从kmp这个专题开始总结吧。
自己的模板
判断模式串在总串的哪个位置开始出现
#include <iostream>
#include<algorithm>
#include<string.h>
#include<stdio.h>
using namespace std;
int ne[10005];
int s[1000005],p[10005];
int m,n;
void getnext()
{
int i=-1,j=0; //i是头,j是尾,然后往后面跳
ne[0]=-1; //判断是否跳到头
while(j<n-1) //因为在n-2时候就已经判断了n-1所以只用到n-2
{
if(i==-1||p[i]==p[j]) //是否跳到开头或者匹配成功
{
++i;
++j;
ne[j]=i;
}
else
i=ne[i];
}
}
void kmp()
{
int i=0,j=0;
while((i<m)&&(j<n))
{
if(j==-1||s[i]==p[j])
{
++i;
++j;
}
else
{
j=ne[j];
}
}
if(j>=n) //判断是否全部匹配
printf("%d\n",i-j+1);
else
printf("-1\n");
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&m,&n);
for(int a=0;a<m;a++)
scanf("%d",&s[a]);
for(int a=0;a<n;a++)
scanf("%d",&p[a]);
getnext();
kmp();
}
return 0;
}
在这些基础上的话有一些其它的扩展
1.最常见的问题
判断一个串在另一个串中出现了几次这个问题可以分成两种,一种是可以有折叠的,一种是没有重复的,比如问AZA在AZAZAZA中出现了几次,如果可以折叠的话就是3次,不可以折叠的话就是2次,那么只需要在原来的板子上面加一个操作就行
可以折叠的话就用用next数组往前跳,不可以折叠的话就是跳到开头继续判断就行了
可以折叠
if(j==-1||s[i]==p[j])
{
if(j==n-1)
{
++k;
j=ne[j];
continue;
}
++i;
++j;
}
不可以折叠
if(j==-1||s[i]==p[j])
{
if(j==n-1)
{
++k;
j=0;
++i;
continue;
}
++i;
++j;
}
2.循环节
判断循环节的问题,首先这个问题在上面getnext()中得到next数组的时候while里面的是j<n而不是n-1,因为如果n-1的话是从n-2那个环节判断的n-1,但是判断的是n-1前面的,没有n-1本身,而j<n的话判断了n-1,然后把n-1的情况存到next[n]中
然后的话就是 len§-next[n]代表的是循环节的长度,如果len§% ( len§-next[n])==0,就表明是一个完整的循环节,否则就是有残缺的,如果是完整的循环节,那么他的循环次数就是 len§ / (len§-next[n])
3.判断一个字符串的前缀和另一个字符串的后缀的最长公共部分
比如abcd的前缀和dabc的后缀的最长公共部分就是abc
让这两个字符串先结合成一个串,然后对这个串进行next处理,中间不用加限制条件,等到最后判断的时候直接判断,用到strcat函数strcat(a,b)就是把数组b连接到a的末尾
#include <iostream>
#include<algorithm>
#include<string.h>
#include<stdio.h>
using namespace std;
char s[100005],p[50005];
int m,n;
int ne[100005];
void getnext()
{
求next[]数组的略去了
int h=ne[l];
while(h>m||h>n)
//如果s与p连接后ne[l]>m或者n的话就表明p开头匹配next的值连着s结尾的开始匹配了,所以肯定要往前面返回,就是h=ne[h]
{
h=ne[h];
//h就是前缀和后缀的最长长度
}
if(h==0)
{
printf("0\n");
}
else
{
for(int a=0;a<h;a++)
{
printf("%c",s[a]);
}
printf(" %d\n",h);
}
}
int main()
{
while(scanf("%s",s)!=EOF)
{
scanf("%s",p);
m=strlen(s),n=strlen(p);
strcat(s,p);
getnext();
}
}
4.暴力匹配
比如告诉你10个长度为60的字符串,然后问你他们这些串相同的最长公共子串,这时候你需要用到暴力的方法,可以把第一个当成母串,然后每次递增截取,第一次可以从60个字符中先找一个,然后将这一个和其它九个进行匹配,匹配的时候用strstr函数,如果有一个找不到的话就break结束暴力,然后截取60个字符中的另外1个字符,然后和其他9个匹配,如果第一个字符串的60个字符全都没找到的话直接结束最外层循环,因为1个字符都找不到的话那两个就更不用说了,然后直到匹配结束
strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
5.扩展KMP
扩展kmp求出的next[i],extend[i]数组,代表的意思是 next[i]表示的是字符串T对自己进行操作,next[i]表示T串中从i这个位置开始(即T[i]开始到T串的结尾)与T(从0这个位置开始)本身的最长公共长度(即T[i ~n]与T[0 ~n]的最长公共前缀),extend[i]求出的是S串从i开始到结尾与T串的公共前缀长度(即S[i ~m]与T[0 ~n]的最长公共长度),解决的问题有
1.一个字符串所有的前缀子串在这个串中出现的次数之和(也可以用dp,dp[i]=dp[next[i] ]+1),比如abab的话就是a出现的次数+ab出现次数+abc出现次数+abcd出现次数=2+2+1+1=6 ,用next数组扩展自己然后将next数组求和就行了,至于什么原理我也不是太清楚
代码如下
int next[N],extand[N];
void getnext(char *T){// next[i]: 以第i位置开始的子串 与 T的公共前缀
int i,length = strlen(T);
next[0] = length;
for(i = 0;i<length-1 && T[i]==T[i+1]; i++);
next[1] = i;
int a = 1;
for(int k = 2; k < length; k++){
int p = a+next[a]-1, L = next[k-a];
if( (k-1)+L >= p ){
int j = (p-k+1)>0? (p-k+1) : 0;
while(k+j<length && T[k+j]==T[j]) j++;// 枚举(p+1,length) 与(p-k+1,length) 区间比较
next[k] = j, a = k;
}
else next[k] = L;
}
}
void getextand(char *S,char *T){
memset(next,0,sizeof(next));
getnext(T);
int Slen = strlen(S), Tlen = strlen(T), a = 0;
int MinLen = Slen>Tlen?Tlen:Slen;
while(a<MinLen && S[a]==T[a]) a++;
extand[0] = a, a = 0;
for(int k = 1; k < Slen; k++){
int p = a+extand[a]-1, L = next[k-a];
if( (k-1)+L >= p ){
int j = (p-k+1)>0? (p-k+1) : 0;
while(k+j<Slen && j<Tlen && S[k+j]==T[j] ) j++;
extand[k] = j;a = k;
}
else extand[k] = L;
}
}
Manacher算法
这类算法是求回文串而设计出来的算法,回文串就是正着读和倒着读都一样的,最常见的问题就是问你一个字符串中最长的回文串长度
模板如下:
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<string.h>
#define mod 10007
using namespace std;
char s1[110010],s2[220010]; //s1是初始的串,s2是变换过后的串
int p[220010];
int manacher()
{
int k=0;
s2[k++]='$';
s2[k++]='#';
int n=strlen(s1);
for(int a=0;a<n;a++)
{
s2[k++]=s1[a];
s2[k++]='#';
}
s2[k]='\0';
int i=0,mx=0,ans=-1;
p[0]=0;
for(int a=1;a<k;a++)
{
if(a<mx)
{
p[a]=min(p[2*i-a],mx-a);
}
else
p[a]=1;
while(s2[a-p[a]]==s2[a+p[a]]) //因为第一次判断肯定成功,所以最后ans-1才是最长字符串长度
p[a]++;
if(a+p[a]>mx)
{
i=a;
mx=a+p[a];
}
ans=max(ans,p[a]);
}
return ans-1; //ans-1为字符串长度而不是ans
}
int main()
{
while(scanf("%s",s1)!=EOF)
{
printf("%d\n",manacher());
}
return 0;
}
如果题目让你求最长串的起始位置的话,假设l为左位置,r为右位置的话,(l和r指的是在原串s1的位置),然后就可以在原串中输出来
r=(p[f]-1)/2+f/2-1; // p[f]就是上面模板的ans,p[f]-1就是最长回文串的长度,而f就是以f位置的那个字符为中心的回文串往两边扩展得到最长回文串
l=r-p[f]+2;
如果遇到这类问题,给你一串字符串,让你把它分成两串,每一串的每一个字母都有它自己的价值,如果分开后的串是回文串,他的价值就是每一个字母价值之和,否则价值为0,让你求最大的价值(HDU 3613)
//前面的正常操作就行,我只写核心的代码
//n是字符串的长度,ans是最大价值,sum[n]指的是前n个字符串的价值(先不考虑是不是回文串)
int ans=0;
for(int a=0;a<n-1;a++)
{
int num=0;
int k1=a+1,k2=n-a-1; //把字符串分成两段后,k1是字符串的左部分,k2是右部分
int k=p[a+2]-1;
if(k1==k)//判断前串是不是回文串
{
num+=sum[a];
}
k=p[a+n+2]-1;
if(k2==k) //判断后串是不是回文串
{
num+=(sum[n-1]-sum[a]);
}
ans=max(ans,num);
}
最小/(大)表示法
可以通过此算法将字符串按照字典序最小(大)排序,最后求出的min(i,j)就是表示从这个位置开始的串字典序最小(大)
int Get_min()
{
int n = strlen(s);
int i = 0,j = 1,k = 0,t;
//表示从i开始k长度和从j开始k长度的字符串相同
while(i < n && j < n && k < n)
{
t = s[(i+k)%n] - s[(j+k)%n];
//t用来计算相对应位置上那个字典序较大
if(!t) k++;//字符相等的情况
else
{
if(t > 0) i += k+1;//i位置大,最大表示法: j += k+1
else j += k+1;//j位置大,最大表示法: i += k+1
if(i == j) j++;
k = 0;
}
}
return i >j ?j :i;
}