目录
1.KMP算法
题目描述
输入格式
输出格式
样例输入
ABABABC ABA
样例输出
1 3 0 0 1
数据范围
下标从1开始。
KMP算法思想
在一次不匹配时,主串不动,拉着模式串继续向后进行匹配。
(1).取最长的相等的前后缀,可以保证不漏解。
(2).通过模式串的前后缀的自我匹配的长度,计算next函数,给j一张表,失配时跳到next[j]的位置继续匹配。
next函数
next[i]:表示模式串P[1,i]中相等前后缀的最长长度。
P | a a b a a b a a a a |
ne[1]=0 | a |
ne[2]=1 | a a |
ne[3]=0 | a a b |
ne[4]=1 | a a b a |
ne[5]=2 | a a b a a |
ne[6]=3 | a a b a a b |
ne[7]=4 | a a b a a b a |
ne[8]=5 | a a b a a b a a |
ne[9]=2 | a a b a a b a a a |
ne[10]=2 | a a b a a b a a a a |
ne[1]=0;
for(int i=2,j=0;i<=n;i++)
{
while(j&&P[i]!=p[j+1]) j=ne[j];
if(P[i]==P[j+1]) j++;
ne[i]=j;
}
思想
双指针:i扫描模式串,j扫描前缀。
初始化,ne[1]=0,i=2,j=0。
每轮for循环,i向右走一步。
(1)如果P[i]!=P[j+1],让j回跳到能匹配的位置,如果找不到匹配的位置,j回跳到0.
(2)若P[i]==P[j+1],让j+1,指向前缀的末尾。
(3).ne[i]=j.
和j+1位置进行比较,是一个预判,进可攻,退可守的思想。
时间复杂度为O(n),因为j的总次数不超过2n。
匹配过程
for(int i=1,j=0;i<=m;i++)
{
while(j&&S[i]!=P[j+1]) j=ne[j];
if(S[i]==P[j+1]) j++;
if(j==m) printf("%d\n",i-n+1);
}
双指针:i扫描主串,j指针扫描模式串。
初始化,i=1,j=0.
每轮for循环,i向右走一步。
(1)如果S[i]!=P[j+1],让j回跳到能够匹配的位置,如果找不到匹配的位置,回跳到0.
(2)如果S[i]==P[j+1],让j向右走一步。
(3)如果匹配成功,输出匹配位置。
时间复杂度
j指针次数不超过2m,所以时间复杂度为O(m)
总时间复杂度为O(n+m)
#include <iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 100100, M = 1000010;
int n, m;
int ne[N];
char s[M], p[N];
void get_next()
{
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];//没有推到0并且不相等的话,j指针一直回退
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
}
void kmp()
{
for (int i = 1, j = 0; i <= m; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];//直到找到相等的为止
if (s[i] == p[j + 1]) j ++ ;
if (j == n)
{
printf("%d\n", i - n+1);
j = ne[j];
}
}
}
void print()
{
for(int i=1;i<=n;i++) cout<<ne[i]<<" ";
}
int main()
{
cin>>s+1>>p+1;
n=strlen(p+1),m=strlen(s+1);
get_next();
kmp();
print();
return 0;
}
2.扩展KMP(Z函数)
题目描述
输入格式
两行两个字符串a,b.
输出格式
第一行一个整数,表示 z 的权值。
第二行一个整数,表示 p 的权值。
样例输入
aaaabaa aaaaa
样例输出
6 21
解释说明
数据范围
思路
Z函数:对于一个长度为n的字符串S。z[i]表示S中其后缀S[i,n]和前缀S[1,x]的最长匹配长度。
3.manacher算法
题目描述
给出一个只由小写英文字符组成的字符串 S ,求 S 中最长回文串的长度 。
字符串长度为 n。
输入格式
一行小写英文字符组成的字符串 S。
输出格式
一个整数表示答案。
样例输入
aaa
样例输出
3
数据范围
1<=n<=1.1*10^7.
马拉车算法思想
马拉车算法可以在O(n)的时间复杂度内求出一个字符串的最长回文串。
马拉车算法的步骤如下:
(1)改造字符串
在字符串之间和串的两端插入#,改造后,都变成奇回文串,方便统一处理。
奇回文串 aba,改造后 #a#b#a#
偶回文串 abba,改造后 #a#b#b#a#
s[0]='$'是哨兵(边界).
scanf("%s",a+1);
int n=strlen(a+1),k=0;
s[0]='$',s[++k]='#';
for(int i=1;i<=n;i++)
s[++k]=a[i],s[++k]='#';
n=k;
(2)回文半径d[i]
d[i]表示以i为中心的最长回文串的长度的一半(上取整),叫做回文半径。
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 | 4 | 1 | 2 | 1 |
(3)加速盒子[l,r]
维护右端点最靠右的最长回文串,利用盒子,借助之前的状态来计算新的状态,盒内d[i]可以利用对称点的d值转移,盒外暴力。
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 | 4 | 1 | 2 | 1 |
(4)算法流程
计算完前i-1个d函数,维护盒子[l,r]
1.如果i<=r(在盒内),i的对称点为r-i+l,
1.1如果d[r-i+l]<r-i+1,则d[i]=d[r-i+l].
也就是说对称点的回文半径长度小于r-l+1,那么直接可以利用对称点的回文半径进行转移,因为对称点的回文半径没有出r端点,那么此时d[i]=d[r-i+l]一定是回文串,因为没有出r区间,可以保证一定是回文串。
1.2如果d[r-i+l]>=r-i+1,则令d[i]=r-i+1,从r+1往后进行暴力枚举。
2.如果i>r(在盒外),则从i开始进行暴力枚举。
3.求出d[i]后,如果i+d[i]-1>r,则更新盒子l=i-d[i]+1,r=i+d[i]-1。
void get(char *s,int n)
{
d[1]=1;
for(int i=2,l,r=1;i<=n;i++)
{
if(i<=r) d[i]=min(d[r-i+l],r-i+1);//在盒子内,找对称点
while(s[i-d[i]]==s[i+d[i]]) d[i]++;//求出盒子外的回文长度
if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;//更新区间
}
}
(5).时间复杂度
内层while循环的总次数决定了总的执行次数,对于while循环,如果条件成立,d++,即盒子的右端点r就会后移若干位,而r<=n,故r右移的总次数不会超过n,故时间复杂度为O(n).
(6).模拟
i=2 d=2 [1,3]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 |
i=3 d=2 [1,5]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 |
i=4 d=2 [1,5]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 |
i=5 d=1 [1,5]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 |
i=6 d=4 [3,9]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 | 4 |
i=7 d=1 [3,9]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 | 4 | 1 |
i=8 d=2 [3,9]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 | 4 | 1 | 2 |
i=9 d=1 [3,9]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
s | # | a | # | a | # | b | # | a | # |
d[i] | 1 | 2 | 3 | 2 | 1 | 4 | 1 | 2 | 1 |
因为我们又添加了#,相当于添加了一半的字符,那么最长回文子串的长度=新串的最大半径-1.
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e7;
char a[N*2],s[N*2];
int d[N*2];
void get(char *s,int n)
{
d[1]=1;
for(int i=2,l,r=1;i<=n;i++)
{
if(i<=r) d[i]=min(d[r-i+l],r-i+1);//在盒子内,找对称点
while(s[i-d[i]]==s[i+d[i]]) d[i]++;//求出盒子外的回文长度
if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;//更新区间
}
}
int main()
{
scanf("%s",a+1);
int n=strlen(a+1),k=0;
s[0]='$',s[++k]='#';
for(int i=1;i<=n;i++)
s[++k]=a[i],s[++k]='#';
n=k;
get(s,n);
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,d[i]);
cout<<ans-1 <<endl;
return 0;
}