KMP&扩展KMP&Manacher算法基础与习题(第一更)
KMP&扩展KMP&Manacher算法基础与习题(第三更)
目录
A:HDU-2594 Simpsons’ Hidden Talents
扩展KMP算法讲解(转自扩展KMP算法)
问题定义:给定两个字符串 S 和 T(长度分别为 n 和 m),下标从 0 开始,定义extend[i]
等于S[i]...S[n-1]
与 T 的最长相同前缀的长度,求出所有的extend[i]
。举个例子,看下表:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
S | a | a | a | a | a | b | b | b |
T | a | a | a | a | a | c | ||
extend[i] | 5 | 4 | 3 | 2 | 1 | 0 | 0 | 0 |
为什么说这是 KMP 算法的扩展呢?显然,如果在 S 的某个位置 i 有extend[i]
等于 m,则可知在 S 中找到了匹配串 T,并且匹配的首位置是 i。而且,扩展 KMP 算法可以找到 S 中所有 T 的匹配。接下来具体介绍下这个算法。
算法流程:
(1)
如上图,假设当前遍历到 S 串位置 i,即extend[0]...extend[i - 1]
这 i 个位置的值已经计算得到。设置两个变量,a 和 p。p 代表以 a 为起始位置的字符匹配成功的最右边界,也就是 "p = 最后一个匹配成功位置 + 1"。相较于字符串 T 得出,S[a...p) 等于 T[0...p-a)。
再定义一个辅助数组int next[]
,其中next[i]
含义为:T[i]...T[m - 1]
与 T 的最长相同前缀长度,m 为串 T 的长度。举个例子:
i | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
T | a | a | a | a | a | c |
next[i] | 6 | 4 | 3 | 2 | 1 | 0 |
(2)
S[i]
对应T[i - a]
,如果i + next[i - a] < p
,如上图,三个椭圆长度相同,根据 next 数组的定义,此时extend[i] = next[i - a]
。
(3)
如果i + next[i - a] == p
呢?如上图,三个椭圆都是完全相同的,S[p] != T[p - a]
且T[p - i] != T[p - a]
,但S[p]
有可能等于T[p - i]
,所以我们可以直接从S[p]
与T[p - i]
开始往后匹配,加快了速度。
(4)
如果i + next[i - a] > p
呢?那说明S[i...p)
与T[i-a...p-a)
相同,注意到S[p] != T[p - a]
且T[p - i] == T[p - a]
,也就是说S[p] != T[p - i]
,所以就没有继续往下判断的必要了,我们可以直接将extend[i]
赋值为p - i
。
(5)最后,就是求解 next 数组。我们再来看下next[i]
与extend[i]
的定义:
- next[i]:
T[i]...T[m - 1]
与 T 的最长相同前缀长度; - extend[i]:
S[i]...S[n - 1]
与 T 的最长相同前缀长度。
恍然大悟,求解next[i]
的过程不就是 T 自己和自己的一个匹配过程嘛,下面直接看代码。
模板
void Getnext(char *str)
{
int i=0,j,po,len=strlen(str);
nex[0]=len;
while(str[i]==str[i+1]&&i+1<len)
i++;
nex[1]=i;
po=1;
for(i=2;i<len;i++)
{
if(nex[i-po]+i<nex[po]+po)
nex[i]=nex[i-po];
else
{
j=nex[po]+po-i;
if(j<0)j=0;
while(i+j<len&&str[i+j]==str[j])
j++;
nex[i]=j;
po=i;
}
}
}
void EXKMP(char *s1,char *s2,int ex[])
{
int i=0,j,po,len=strlen(s1),l2=strlen(s2);
Getnext(s2);
while(s1[i]==s2[i]&&i<len&&i<l2)
i++;
ex[0]=i;
po=0;
for(i=1;i<len;i++)
{
if(nex[i-po]+i<ex[po]+po)
ex[i]=nex[i-po];
else
{
j=ex[po]+po-i;
if(j<0)j=0;
while(i+j<len&&j<l2&&s1[i+j]==s2[j])
j++;
ex[i]=j;
po=i;
}
}
}
例题
A:HDU-2594 Simpsons’ Hidden Talents:题目主要意思 就是让你找出前面一串字符的前缀和后面字符串的相同的后缀 ,并且打印这个字符串的长度 我的做法就是把两个字符串拼接起来,用KMp算法的NEXT数组可以求相同的前缀后缀,(next[len]即为后缀与前缀相等的长度)但是要注意,求出的长度不应该大于原来的最短字符串的长度,AC代码:
#include <cstring>
#include <iostream>
#include <cstdio>
#include <fstream>
#include <algorithm>
using namespace std;
const int maxn=1e6+7;
const int INF=0x3f3f3f3f;
#define M 50015
int next1[maxn];
char str[maxn],mo1[maxn],mo2[maxn];
int ans;
void getnext()
{
int i=0,j=-1,m=strlen(str);
while(i<m){
if(j==-1||str[i]==str[j])
next1[++i]=++j;
else
j=next1[j];
}
}
/*int kmp()
{
int i=0,j=0,n=strlen(str),m=strlen(mo);
while(i<n){
if(j==-1||str[i]==mo[j])
i++,j++;
else
j=next1[j];
if(j==m)
ans++;
}
return ans;
}*/
int main()
{
while(cin>>mo1>>mo2){
strcpy(str,mo1);
strcat(str,mo2);
next1[0]=-1;
getnext();
int len=strlen(str);
int len1=strlen(mo1);
int len2=strlen(mo2);
int n=next1[len];
if(n>=len1||n>=len2){
if(n==len1&&n==len2)
cout<<mo1<<' '<<n<<endl;
else if(len1<len2)
cout<<mo1<<' '<<len1<<endl;
else
cout<<mo2<<' '<<len2<<endl;
}
else{
bool flag=false;
for(int i=0;i<n;i++){
cout<<str[i];
flag=1;
}
if(flag)
cout<<' ';
printf("%d
",n);
}
}
}
B:HDU-3336 Count the string:这道题目是KMP算法,对Next数组的活用。题意就是输入 一个字符串,判断它的子串从0到i(i<=长度) 在主串出现的次数之和。题中也给出了 abab子串有a,ab,aba,abab 分别在主串出现了2,2,1,1 共6次。题目解法,跟Next数组创建有关系,Next数组查询的时候会用到回溯,这就证明了,你所要找的串,之前出现过,这样就可以根据回溯的次数来计算出现次数了。
比如题目中的
序号 0 1 2 3 4
字符串 a b a b
next -1 0 0 1 2
从j=1开始,1回溯一次 sum+=1,j=2的时候也是一次,sum+=1,j=3与j=4时分别回溯两次,sum+=2,sum+=2。所以总共六次。
AC代码:
#include <cstring>
#include <iostream>
#include <cstdio>
#include <fstream>
#include <algorithm>
using namespace std;
const int maxn=2e6+7;
const int INF=0x3f3f3f3f;
#define M 50015
int next1[maxn];
char str[maxn],mo[maxn];
int ans;
void getnext()
{
int i=0,j=-1,m=strlen(mo);
while(i<m){
if(j==-1||mo[i]==mo[j])
next1[++i]=++j;
else
j=next1[j];
}
}
/*int kmp()
{
ans=0;
int i=0,j=0,n=strlen(str),m=strlen(mo);
while(i<n){
if(j==-1||str[i]==mo[j])
i++,j++;
else
j=next1[j];
if(j==m)
ans++;
}
return ans;
}*/
int main()
{
int n,t;
scanf("%d",&t);
while(t--){
memset(next1,0,sizeof next1);
int sum=0;
scanf("%d %s",&n,mo);
next1[0]=-1;
getnext();
for(int i=1;i<=n;i++){
int tem=i;
while(tem){
sum++;
tem=next1[tem];
}
}
printf("%d
",sum%10007);
}
}
C:HDU-4300 Clairewd’s message:一道思维题,给你一张含有26个字母的密码表,对应相应位置的字母a~z。再给你一组不完整的密文+明文,要求输出完整的密文+明文。思路是将密文+明文翻译过来,以原来的密文+明文为主串,翻译后的字符串为模式串,进行扩展KMP匹配。(具体为什么可以好好想想,毕竟思维题),AC代码:
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
int T,n,len;
char s[100010],t[100010],a[28];
int nexts[100010],extand[100010];
void getnexts(char *t)
{
int len =strlen(t);
int a=0;
nexts[0]=len;
while(a<len-1&&t[a]==t[a+1]) a++;
nexts[1]=a;
a=1;
for(int k=2;k<len;k++)
{
int p=a+nexts[a]-1,L=nexts[k-a];
if(k-1+L>=p)
{
int j=max((p-k+1),0);
while(k+j<len&&t[k+j]==t[j]) ++j;
nexts[k]=j;
a=k;
}
else nexts[k]=L;
}
}
void ekmp(char *s,char *t)
{
int lens=len,lent=strlen(t),a=0;
int minlen=min(lens,lent);
while(a<minlen&&s[a]==t[a]) ++a;
extand[0]=a;
a=0;
for(int k=1;k<lens;k++)
{
int p=a+extand[a]-1,L=nexts[k-a];
if(k-1+L>=p)
{
int j=max(p-k+1,0);
while(k+j<lens&&j<lent&&s[k+j]==t[j]) ++j;
extand[k]=j;
a=k;
}
else extand[k]=L;
}
}
int main()
{
cin>>T;
while(T--)
{
char hash[150];
scanf("%s",a);
for(int i=0;i<26;i++)
hash[ a[i] ]='a'+i;
scanf("%s",s);
len=strlen(s);
for(int i=0;i<len;i++)
t[i]=hash[s[i]];
getnexts(t);
ekmp(s,t);
int pos=len;
for(int i=0;i<len;i++)
{
if(i+extand[i]>=len&&i>=extand[i])
{
pos=i;
break;
}
}
for(int i=0;i<pos;i++)
printf("%c",s[i]);
for(int i=0;i<pos;i++)
printf("%c",t[i]);
printf("
");
}
}
D:HDU-1238 Substrings:给您一些区分大小写的字母字符串,找出最大的字符串X的长度,这样就可以找到X或它的逆字符串作为任意给定字符串的子字符串,可以直接暴力枚举,具体看代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
int Next[105];
void getNext(string w,int len){
int i = -1,j = 0;
memset(Next,0,sizeof(Next));
Next[0] = -1;
while(j < len){
if(i == -1 || w[i] == w[j]){
i++,j++;
Next[j] = i;
}
else
i = Next[i];
}
}
bool kmp(string w,int m,string s,int n){
int i = 0,j = 0;
getNext(w,m);
while(j < n){
if(i == -1 || w[i] == s[j])
i++,j++;
else
i = Next[i];
if(i >= m){
return true;
}
}
return false;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
string str[102];
int ans = 0;
int n,i,j,k;
scanf("%d",&n);
for(i = 0; i < n; i++){
cin >> str[i];
}
for(i = 0; i < str[0].length(); i++){
for(j = i; j < str[0].length(); j++){//j从i开始,长度为1的串也要算
string w = str[0].substr(i,j-i+1);
for(k = 1; k < n; k++){
string rw = w;
reverse(w.begin(),w.end());
if(!kmp(w,w.length(),str[k],str[k].length())&&!kmp(rw,rw.length(),str[k],str[k].length()))
break;
}
if(k >= n){
if(w.length()>ans)
ans = w.length();
}
}
}
printf("%d
",ans);
}
return 0;
}
E:HDU-2328 Corporate Identity:给出若干个串,求最长公共子串,又是一道暴力枚举题。直接暴力枚举公共字串的开始位置和长度,AC代码:
#include <cstdio>
#include <cstdlib>
#include<cstring>
#include <algorithm>
using namespace std;
char str [20000][20000];
int next1[20000];
char temp[20000];
char sum[20000];
void getnext(char *s1){
int j=0,k=-1;
int len=strlen(s1);
next1[0]=-1;
while(j<len){
if(k==-1||s1[j]==s1[k]){
++j;
++k;
if(s1[j]!=s1[k]) next1[j]=k;
else next1[j]=next1 [k];
}
else k=next1[k];
}
return ;
}
bool kmp(char *s1,char *s2){
int len1=strlen(s2);
int len2=strlen(s1);
getnext(s1);
int i=0,j=0;
while(i<len1){
if(j==-1||s1[j]==s2[i]){
++i;
++j;
}
else j=next1[j];
if(j==len2)
return true;
}
return false;
}
int main()
{
int n;
int i,j,k;
while(scanf("%d",&n)==1&&n){
for( i=0;i<n;i++)
scanf("%s",str[i]);
int len=strlen(str[0]);
memset(sum,' ',sizeof(sum));
for(i=0;i<len;i++){
int ans=0;
for(j=i;j<len;j++){
temp[ans]=str[0][j];
ans++;
temp[ans]=' ';//注意这里一定要加上,否则出现越界等情况
int flag=1;
for(k=1;k<n;k++){
if(!kmp(temp,str[k])){
flag=0;
break;
}
}
if(flag){
if(strlen(temp)>strlen(sum)){
strcpy(sum,temp);
}
else if(strlen(temp)==strlen(sum)&&strcmp(temp,sum)<0)
strcpy(sum,temp);
}
}
}
if(strlen(sum)>0) printf("%s
",sum);
else printf("IDENTITY LOST
");
}
return 0;
}
F:HDU-3374 String Problem:题意:最小最大表示的模板题,可以记住模板,没必要知道原因,想知道也可以一去网上查查。给出多组数据,每组数据给出一个字符串,要求输出这个字符串的最小最大表示的起始位置,然后分别求出在同构串中起始位置的字符出现的次数。思路:最小最大的起始位置直接套用模版即可,然后使用 KMP 的 next 数组求循环节,则次数=长度/循环节长度(循环n次代表可以有n个位置使移动后和最小最大表示相等)。
循环字符串的最小表示法的问题可以这样描述:对于一个字符串S,求S的循环的同构字符串S’中字典序最小的一个。由于语言能力有限,还是用实际例子来解释比较容易:设S=bcad,且S’是S的循环同构的串。S’可以是bcad或者cadb,adbc,dbca。而且最小表示的S’是adbc。最大表示则为dbca
AC代码:
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
#define INF 0x3f3f3f3f
const int N=1000000+5;
using namespace std;
int Next[N];
char str[N];
void getNext(char p[]){
Next[0]=-1;
int len=strlen(p);
int j=0;
int k=-1;
while(j<len){
if(k==-1||p[j]==p[k]){
k++;
j++;
Next[j]=k;
}
else{
k=Next[k];
}
}
}
int minmumRepresentation(char *str){//最小表示法
int len=strlen(str);
int i=0;
int j=1;
int k=0;
while(i<len&&j<len&&k<len){
int temp=str[(i+k)%len]-str[(j+k)%len];
if(temp==0)
k++;
else{
if(temp>0)
i=i+k+1;
else
j=j+k+1;
if(i==j)
j++;
k=0;
}
}
return i<j?i:j;
}
int maxmumRepresentation(char *str){//最大表示法
int len=strlen(str);
int i=0;
int j=1;
int k=0;
while(i<len&&j<len&&k<len){
int temp=str[(i+k)%len]-str[(j+k)%len];
if(temp==0)
k++;
else{
if(temp>0)
j=j+k+1;
else
i=i+k+1;
if(i==j)
j++;
k=0;
}
}
return i<j?i:j;
}
int main(){
while(scanf("%s",str)!=EOF){
getNext(str);
int n=strlen(str);
int len=n-Next[n];
int num=1;//数量
if(n%len==0)
num=n/len;
int minn=minmumRepresentation(str);//最小表示
int maxx=maxmumRepresentation(str);//最大表示
printf("%d %d %d %d
",minn+1,num,maxx+1,num);
}
return 0;
}
G:HDU-2609 How many:有n个有01组成的字符串,每个字符串都代表一个项链,那么该字符串就是一个环状的结构,求可以经过循环旋转,最后不同的串有多少个。最小表示法的应用,将每个串用最小表示法表示出来,其中有多少个不一样的即为结果。AC代码:
#include <bits/stdc++.h>
#define PI acos(-1.0)
#define INF 0x3f3f3f3f
#define MOD 1000000007
#define EPS 1e-6
#define N 1123456
using namespace std;
int n,m,sum,res,flag;
char s[101],ss[101];
int minString(char *s)
{
int i=0,j=1,k=0;
int len=strlen(s);
while(i<len&&j<len&&k<len)
{
if(s[(i+k)%len]==s[(j+k)%len])k++;
else
{
if(s[(i+k)%len]>s[(j+k)%len])i=i+k+1;
else j=j+k+1;
if(i==j)j++;
k=0;
}
}
return i<j?i:j;
}
int main()
{
int i,j,k,kk,cas,T,t,x,y,z;
vector<string>sn;
while(scanf("%d",&n)!=EOF)
{
sn.clear();
for(i=0;i<n;i++)
{
scanf("%s",s);
m=strlen(s);
t=minString(s);
for(j=0;j<m;j++)
ss[j]=s[(j+t)%m];
string st(ss);
sn.push_back(st);
}
sort(sn.begin(),sn.end());
res=1;
for(i=1;i<n;i++)
if(sn[i]!=sn[i-1])
res++;
printf("%d
",res);
}
return 0;
}
H:FZU-1901 Period II:题意:给出一个字符串,问可以看作由长度多少的子串循环得到,最后一周期可以不全。思路:这题可以KMP,next数组求公共的子串,取next[len],之前求循环节的知道,len-next[len]就是最短的循环节,依次递归地求next用子串长度减就是子串的最短循环节,加上原来的子串外的那部分就是新的循环节(子串外部分也是子串的循环节)这个在纸上画一画基本都出来了。两者加在一起就是len-nexts[buf],其中buf为依次递归求nexts的值。AC代码:
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
char s[2000100];
int nexts[2000100],sum[2000100];
int len,n,num;
void getnexts(char *s)
{
int i=0,j=-1;
nexts[0]=-1;
while(i<len)
{
if(s[i]==s[j]||j==-1)
{
nexts[++i]=++j;
}
else j=nexts[j];
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%s",s);
len=strlen(s);
getnexts(s);
num=0;
int buf=len;
while(buf)
{
buf=nexts[buf];
sum[num++]=len-buf;
}
printf("Case #%d: %d
",i,num);
for(int j=0;j<num-1;j++)
printf("%d ",sum[j]);
printf("%d
",sum[num-1]);
}
}