这三点完全可以放这一起学,都是把原来暴力的方法优化达到线性的运算,原理不难,活用的话有点挑战,推荐刷题吧。
一、KMP(关键词:next数组,前缀,循环节)
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。
kmp入门资料
kmp例题
kmp原理不难理解,但其中的next数组是关键。kmp算法的主要作用在于对next数组的运用。
(1)next数组的定义:
1、设模式串T[0,m-1],(长度为m),那么next[i]表示既是是串T[0,i-1]的后缀又是串T[0,i-1]的前缀的串最长长度(不妨叫做前后缀),注意这里的前缀和后缀不包括串T[0,i-1]本身。
2、代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。
3、简而言之,next[i]代表了前缀和后缀的最大匹配的值。
(2)next数组的性质:
性质1:对于每一个长度len的子串,该子串的最小循环节为len-next[len]
性质2:kmp的next不断向前递归的过程可以保证对于每一个当前前缀,都有一段后缀与之对应
性质1拓展一:求字符串的前缀是否为周期串时,len%(len-next[len])==0时,循环节为len,循环次数len/(len-next[len])。(hdu1358)
性质1拓展二:不论S是不完整的循环串还是完整的循环串,len-next[len]一定是串S中去除末尾残缺部分之后,存在的最小循环节长度。
模板(因为next可能在一些编译器中会重复含义,类似max,所以下面我用Next)
const int maxn=1e5+7;
int Next[maxn];
void getnext(string a){
int i=0,j=-1,len=a.size();
Next[0]=-1;
while(i<len-1){
if(j==-1||a[i]==a[j]) Next[++i]=++j;
else j=Next[j];
}
}
int kmp(string s,string p){
int i=0,j=0;
int N=s.size(),M=p.size();
while(i<N){
if(j==-1||s[i]==p[j]) i++,j++;
else j=Next[j];
if(j==M) return i-j;
}
return -1;
}
int main(){
string a,b;
//字符串a中找字符串b
cin>>a>>b;
getnext(b);
int ss=kmp(a,b);
cout<<ss;
}
其中getnext函数还有改进版,也是对这种算法的改进版本。
void getnext(const char *p) {//前缀函数(滑步函数)
int i = 0, j = -1;
nextval[0] = -1;
while(i != len)
{
if(j == -1 || p[i] == p[j]) //(全部不相等从新匹配 || 相等继续下次匹配)
{
++i, ++j;
if(p[i] != p[j]) //abcdabce
nextval[i] = j;
else //abcabca
nextval[i] = nextval[j];
}
else
j = nextval[j]; //子串移动到第nextval[j]个字符和主串相应字符比较
}
}
但大多数情况下,第一种简单的getnext函数就够用了,第二种很少用,有时反而更容易超时。
推荐补题:hdu3336 (kmp+线性dp), hdu3746 (KMP:补齐循环节)
二、拓展kmp
(1)BM算法
KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的 尾部 开始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。
BM算法详解
模板(待更新)
(2)strstr()函数(泪目)
学完kmp才知道,实际上,单单判断字符串str2是否是str1的子串时,KMP算法并不比最简单的c库函数strstr()快多少。
定义:strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
在串中查找指定字符串的第一次出现
用法:
#include
#include
int main(){
char *str1 = "Borland International", *str2 = "nation", *ptr;
ptr = strstr(str1, str2);
printf("%s\n", ptr);
return 0;
}
输出为:national
注意:strstr()函数注意点
strstr(str1,str2)返回值能随str1变化而变化,因为他们内容有共用地址,地址一样,输出的内容也一样。
所以在使用或者处理strstr(str1,str2)返回值之前,切记不要对str1字符串进行更改,若要更改,应该等使用完返回值后再更改。
(3)Sunday算法
上面这两个算法在最坏情况下均具有线性的查找时间。
比BM算法更快的查找算法即Sunday算法。
模板就不更新了,比较难。
三、manacher算法(关键词:最长回文子串,)
Manacher算法是查找一个字符串的最长回文子串的线性算法。相对于前面介绍的两个算法,Manacher算法的应用范围要狭窄得多。
manacher算法题目
模板:
const int maxn=2e5+7;
char a[maxn*2],sz[maxn*2];
int p[maxn*2];
void mnc(char *s){
sz[0]='%'; sz[1]='#';
int j=2,k=0,R=0,ans=0;
for(int i=0;s[i];i++,j+=2){ sz[j]=s[i]; sz[j+1]='#'; }
sz[j]=0;
for(int i=1;sz[i];i++){
if(i<R) p[i]=min(p[k+k-i],R-i);
else p[i]=1;
while(sz[i-p[i]]==sz[i+p[i]]) p[i]++;
if(i+p[i]>R){ k=i; R=i+p[i]; }
ans=max(ans,p[i]-1);
}
printf("%d\n",ans);
}
int main(){
while(~scanf("%s",a)) mnc(a);
}
以i为对称中心时,长度为ans = p[i]-1,左端点为 l = (i-p[i])/2 。右端点 r = l+ans-1 。(hdu3294)
例题:
1、求最长向中心递增回文子串(hdu4513)
const int maxn=2e5+7;
int a[maxn*2],sz[maxn*2],p[maxn*2],n,t,m;
int main(){
scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
sz[0]=0; sz[1]=-1; m=2;
for(int i=0;i<n;i++,m+=2){ sz[m]=a[i]; sz[m+1]=-1; }
sz[m]=-1;
//防止越界
int k=0,R=0,ans=0;
for(int i=1;i<=m;i++){
// 1.计算p[i]初始值
if(i<R) p[i]=min(p[k+k-i],R-i);
else p[i]=1;
// 2.扩张p[i],以适应达到p[i]最大值
while(sz[i-p[i]]==sz[i+p[i]]){
if(sz[i-p[i]]==-1) p[i]++;
else{
if(sz[i-p[i]]<=sz[i-p[i]+2]) p[i]++;
else break;
}
}
// 3.更新中点k,回文半径R
if(i+p[i]>R){ k=i; R=i+p[i]; }
// 4.更新最长回文
ans=max(ans,p[i]-1);
}
printf("%d\n",ans);
}
}
/*
2
3
51 52 51
4
51 52 52 51
3
4
*/