KMP
理论
KMP算法的核心是构建一个部分匹配表,也称为前缀表。这个表记录了模式串中每个位置之前的最长公共前缀和后缀的长度。例如,对于模式串"ababaca",其部分匹配表如下:
位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
字符 | a | b | a | b | a | c | a |
最长公共前后缀长度 | 0 | 0 | 1 | 2 | 3 | 0 | 1 |
模式串 | 前缀 | 后缀 | 最长公共前缀和后缀 |
---|---|---|---|
a | 无 | 无 | 0 |
ab | a | b | 0 |
aba | a, ab | ba, a | 1 |
abab | a, ab, aba | bab, ab, b | 2 |
ababa | a, ab, aba, abab | baba, aba, ba, a | 3 |
ababac | a, ab, aba, abab, ababa | babac, abac, bac, ac, c | 0 |
ababaca | a, ab, aba, abab, ababa, ababac | babaca, abaca, baca, aca, ca, a | 1 |
这里的前缀是指从字符串开头开始,不包含末尾字符的所有子串;后缀则是从字符串末尾开始,不包含首位字符的所有子串。最长公共前缀和后缀指的是前缀和后缀中相同且长度最长的子串的长度。
例题
幸运字符串
问题描述
给定一个长度为 n 的字符串 S ,幸运字符串的定义如下:
- 该字符串为 S 的一个前缀字符串 。
- 该字符串在 S 中至少出现过 2 次 。
现在要你求出长度最大的幸运字符串 。
输入格式
输入第一行,包含一个整数 n,表示字符串的长度 。
输入第二行,长度为 n且由小写字母组成的字符串 。
输出格式
输出仅一行,包含一个整数,表示长度最大的幸运字符串的长度 。
输入案例
7
ababaca
样例输出
3
说明
前缀 aba 在 S中出现了两次,由此答案是 3。
评测数据规模
对于 50% 的评测数据,1≤n≤2×1e3
对于 100% 的评测数据,1≤n≤2×1e5。
//本题考察对next数组含义的理解
//next[i]表示以第i个字符结尾的字符串的最长相等前后缀的长度
//本题所求的最长幸运字符串,其长度实际上就是最长相等前后缀的长度
//故本题只需要对输入的字符串求next数组,比较其中的最大值即可
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 100;
char T[N];
int nex[N];
void get_next(int n, char* S)//求S的next数组
{
nex[0] = nex[1] = 0;
for (int i = 2, j = 0; i <= n; i++)
{
while (j && S[j + 1] != S[i]) {
j = nex[j];
}
if (S[i] == S[j + 1]) {
j++;
}
nex[i] = j;
}
}
int main()
{
int n;
cin >> n;
cin >> T + 1;
get_next(n, T);//对输入的字符串计算next数组
int ans = 0;
for (int i = 1; i <= n; i++)//比较得出next数组中的最大值
{
ans = max(ans, nex[i]);
}
cout << ans << endl;
return 0;
}
Manacher
理论
01 回文串的性质
- 回文串:类似ABA,ABCBA,AABBAA 等的对于每个i具有s[i]=s[n+1-i]的字符串。
- 回文半径:对于一个回文中心i,如果它的半径为r,如果它为奇数长度的回文串的中心,则说明[i - r+1,i+r - 1]为一个回文串。如果i是偶数长度的回文中心,则回文半径没有意义(Manacher算法会解决这个问题)。
- 回文的递归性质:某个点的回文半径相比关于某个回文中心的点的回文半径一定相等或更大。
举个例子,对于回文串ABACABAC,第二个B的回文半径至少是第一个B的回文半径。
02 Manacher算法
前面说过,回文半径对于偶数长度的回文的中心没有意义,例如ABBA中的B就不存在回文半径。
为了解决这个问题,Manacher发明了一种算法,他将所有的回文串都转化为了奇数长度的回文串。
方法就是在字符串中间和头尾插入特殊字符,例如原字符串为ABBA,转换后变为^#A#B#B#A#$ 。
Manacher算法是一种O(n)复杂度计算字符串中每个位置作为回文中心的回文半径的算法。
位置i的回文半径以p[i]表示,意思是在转换后的字符串中[i - p[i]+1,i + p[i]-1]是回文的。
步骤
- 字符串预处理
- 初始化变量
- 遍历字符串计算回文半径
- 还原结果
例题
题目描述
给出一个只由小写英文字符 a,b,c,…y,z 组成的字符串 S ,求 S 中最长回文串的长度 。
字符串长度为 n。
输入格式
一行小写英文字符 a,b,c,⋯,y,z 组成的字符串 S。
输出格式
一个整数表示答案。
输入输出样例
输入
aaa
输出
3
说明/提示
1≤n≤1.1×1e7。
#include<bits/stdc++.h>
using namespace std;
string Get_new(string& str) {
string temp = "#";
for (int i = 0; str[i]; ++i) {
(temp += str[i]) += "#";
}
return temp;
}
int main() {
string str;
cin >> str;
// 1.字符串预处理
str = Get_new(str);
// 动态分配一个数组 r,用于存储每个位置的回文半径
// 数组长度为处理后字符串的长度
int* r = (int*)calloc(sizeof(int), str.size());
//2.初始化变量
// c 表示当前最大回文串的中心位置
int c = 0;
// ans 用于记录最大回文半径
int ans = 0;
// 从第二个字符开始遍历处理后的字符串
// 3.遍历字符串计算回文半径
for (int i = 1; str[i]; ++i) {
// 如果当前位置 i 在以 c 为中心的回文串范围内
if (c + r[c] > i) {
// 利用回文串的对称性,计算 r[i] 的初始值
// r[2 * c - i] 是 i 关于 c 的对称位置的回文半径
// c + r[c] - i 是 i 到当前回文串右边界的距离
r[i] = min(r[2 * c - i], c + r[c] - i);
}
// 尝试扩展以 i 为中心的回文串
// 检查左右字符是否相等
while (i - r[i] >= 0 && str[i - r[i]] == str[i + r[i]]) {
++r[i];
}
// 因为之前扩展时多算了一次,所以这里要减 1
--r[i];
// 如果以 i 为中心的回文串右边界超过了当前最大回文串的右边界
//4.还原结果
if (i + r[i] > c + r[c]) {
// 更新最大回文串的中心位置为 i
c = i;
}
// 更新最大回文半径
ans = max(ans, r[i]);
}
// 输出最大回文半径
cout << ans << endl;
// 释放动态分配的内存
free(r);
return 0;
}