参考资料:
https://www.cnblogs.com/cloudplankroader/p/10988844.html
https://segmentfault.com/a/1190000008484167
感谢大大的优秀博客!🥰
小熊の算法笔记:Manacher算法
从简单的扩散讲起:
首先我们先看最朴素的“扩散”算法。
扩散算法的想法非常的简单。
比如我们有一个序列为:
| 字符 | A | C | D | A | B | A | C | D |
|---|---|---|---|---|---|---|---|---|
| 序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
如果我门以序号4为中心,然后半径为2(直径为3)我们就可以扩散出一个ABA的回文序列。所以我们以每个序号为中心,然后慢慢的扩大回文半径,算法的直观复杂度应该是
O
(
n
2
)
O(n^2)
O(n2)
在我们想再次的考虑优化算法的复杂度的时候,我们先再看看这个算法的小问题。
那就是其实当序列长度为奇数的偶数的时候,我们应该在3和4之间进行扩散。
我们这里有个小小技巧:
#
A
#
C
#
D
#
A
#
B
#
A
#
C
#
D
#
\#A\#C\#D\#A\#B\#A\#C\#D\#
#A#C#D#A#B#A#C#D#
当我们在每个字符之间加入#的时候,序列总会变成奇书的长度(无论原来的序列是偶数还是奇数)。这样我们就可以继续的扩散了。所以最终我们找到的回文子序列可能是#A#B#A#长度为7,7/2=3才能的到我们不加入# 号时候的长度。
考虑开始加速我们的算法:
我们知道,我们总是想着利用之前算过的结果,来加速程序的计算过程。比如我们学过的动态规划就是利用了这个特点哦!
我们假设有这样的序列:
(下图数据借用自thson同学的数据)
| 字符: | $ | # | a | # | b | # | b | # | a | # | h | # | o | # | p | # | x | # | p | # | o | # | ^ |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 序号 : | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 半径: | 1 | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 | 1 |
我们假设我们现在已经扩散过了序号为5的位置,并且我们找到了ta的最大半径是5,也就是序号1-9的位置。

现在我们开始探索8这个位置,而从8这个位置扩散,我们或许没有必要那样的笨因为我们知道1-9这个范围的串串是回文的,我们可以充分的回文的特性(正着读和反着读是一样的),看看序号2的半径是多大。这样就免的再次的慢慢的扩散啦!
这里就是我们的加速的过程了。
从我们的样例来看。答案确实是如此。
但是我们不得不再次考虑一个情况:
玩意序列2的半径大于了10-8=2的长度呢?(就是以8扩散的半径会不会大于2)

比如上图,这个时候1-9确实还是回文序列,0和4和6也确实有可能为a,然后10为c。这个时候,10为c,因为如果10为a的话,那么以5为半径的回文序列半径应该可以更大。
所以这个时候,我们在8的这个位置不能取序列2的半径,只能取10-8=2的半径。
所以:
我们设定半径数组为p。那么:
id为上图5的位置,mx-i相当于10-8=2。计算i相当于id的位置计算公式是id-(i-id)=id+id-i=2*id-i。i是一定大于id的。
p[i] = min(p[2 * id - i], mx - i);
我们再次的考虑一种情况。
在这里序列0为a然后4,6,10为c是完全可能的,如果我们照搬前面的那条p[i]公式,我们发现8的位置最大半径只能到达2。
其实,在8这个位置还是可以继续扩大它的最大半径,所以我们在加速完了,也不要忘记看看能不能继续扩大半径圈:
(代码摘抄自:thson同学)
while (s_new[i - p[i]] == s_new[i + p[i]]) // 不需边界判断,因为左有 $,右有 ^
p[i]++;
然后计算完了p[i]我们也要看看能不能继续的更正我们的id中心点,和最大半径后面一位的mx。
// 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
// 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
if (mx < i + p[i]) {
id = i;
mx = i + p[i];
}
至此,我们所有的核心代码已经完全讲解结束啦!
是不是很简单呢!
很感谢thson同学的代码!thson同学赛高赛高!
算法复杂度证明:
马拉车算法的平均复杂度是:
O
(
n
)
O(n)
O(n)
很容易推出 Manacher 算法的最坏情况,即为字符串内全是相同字符的时候。在这里我们重点研究 Manacher() 中的 for 语句,推算发现 for 语句内平均访问每个字符 5 次,即时间复杂度为:
T
w
o
r
s
t
(
n
)
=
O
(
n
)
T_{worst}(n)=O(n)
Tworst(n)=O(n)。同理,我们也很容易知道最佳情况下的时间复杂度,即字符串内字符各不相同的时候。推算得平均访问每个字符 4 次,即时间复杂度为:
T
b
e
s
t
(
n
)
=
O
(
n
)
T_{best}(n)=O(n)
Tbest(n)=O(n)。
综上,Manacher 算法的时间复杂度为
O
(
n
)
O(n)
O(n)。
(内容来自:https://www.cnblogs.com/cloudplankroader/p/10988844.html)
完整代码详解:
//参考链接:https://segmentfault.com/a/1190000008484167
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
char s[1000];
char s_new[2000];
int p[2000];
int Init() {
int len = strlen(s);
s_new[0] = '$';
s_new[1] = '#';
int j = 2;
for (int i = 0; i < len; i++) {
s_new[j++] = s[i];
s_new[j++] = '#';
}
s_new[j++] = '^'; // 别忘了哦
s_new[j] = '\0'; // 这是一个好习惯
return j; // 返回 s_new 的长度
}
int Manacher() {
int len = Init(); // 取得新字符串长度并完成向 s_new 的转换
int max_len = -1; // 最长回文长度
int id;
int mx = 0;
for (int i = 1; i < len; i++) {
if (i < mx)
p[i] = min(p[2 * id - i], mx - i); // 需搞清楚上面那张图含义,mx 和 2*id-i 的含义
else
p[i] = 1;
while (s_new[i - p[i]] == s_new[i + p[i]]) // 不需边界判断,因为左有 $,右有 ^
p[i]++;
// 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
// 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
if (mx < i + p[i]) {
id = i;
mx = i + p[i];
}
max_len = max(max_len, p[i] - 1);
}
return max_len;
}
int main() {
while (printf("请输入字符串:")) {
scanf("%s", s);
printf("最长回文长度为 %d\n\n", Manacher());
}
return 0;
}
小熊的代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string.h>
using namespace std;
#define MaxFileLength 10000 + 5
//最长子串长度 最长子串的个数
int iStr_MaxLength = 0, iStr_MaxNumber = 0;
char Str_Ori[MaxFileLength], Str_New[2 * MaxFileLength];
int iStr_P[2 * MaxFileLength] = {0};
void Manacher(const char *, int &, int &);
void MakeNewStr(const char *, char *, int &);
int main() {
freopen("T01.in", "r", stdin);
gets(Str_Ori);
Manacher(Str_Ori, iStr_MaxLength, iStr_MaxNumber);
printf("%d\n%d", iStr_MaxLength, iStr_MaxNumber);
return 0;
}
void Manacher(const char *Str_Ori, int &i_MaxLength, int &i_MaxNumber) {
int iStr_Length = 0; //新创建的字符串长度
MakeNewStr(Str_Ori, Str_New, iStr_Length);
int id = 0, mx = 0;
iStr_P[0] = 1;
for (int i = 1; i < iStr_Length; i++) {
if (i < mx) {
iStr_P[i] = min(mx - i, iStr_P[2 * id - i]);
} else {
iStr_P[i] = 1;
}
while (Str_New[i - iStr_P[i]] == Str_New[i + iStr_P[i]]) {
iStr_P[i]++;
}
if (i + iStr_P[i] > mx) {
mx = i + iStr_P[i];
id = i;
}
//答案更新
if (iStr_P[i] - 1 > i_MaxLength) {
i_MaxLength = iStr_P[i] - 1;
i_MaxNumber = 1;
} else if (iStr_P[i] - 1 == i_MaxLength) {
i_MaxNumber++;
}
}
/*
for (int i = 0; i < iStr_Length; i++)
cout << iStr_P[i];
*/
}
//生成全新的字符串
void MakeNewStr(const char *Str_Ori, char *Str_New, int &i_Len) {
Str_New[0] = '^';
Str_New[1] = '#';
int j = 2;
for (int i = 0; i < strlen(Str_Ori); i++) {
Str_New[j++] = Str_Ori[i];
Str_New[j++] = '#';
}
Str_New[j++] = '*';
i_Len = j;
Str_New[j] = '\0';
}
Java版本:
(摘抄自: https://www.cnblogs.com/cloudplankroader/p/10988844.html)
public class Manacher {
public static char[] manacherString(String str) {
char[] charArr = str.toCharArray();
char[] res = new char[str.length() * 2 + 1];
int index = 0;
for (int i = 0; i != res.length; i++) {
res[i] = (i & 1) == 0 ? '#' : charArr[index++];
}
return res;
}
public static int maxLcpsLength(String str) {
if (str == null || str.length() == 0) {
return 0;
}
char[] charArr = manacherString(str);
int[] pArr = new int[charArr.length];
int C = -1;
int R = -1;
int max = Integer.MIN_VALUE;
for (int i = 0; i != charArr.length; i++) {
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
pArr[i]++;
else {
break;
}
}
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
max = Math.max(max, pArr[i]);
}
return max - 1;
}
public static void main(String[] args) {
String str1 = "abc123321cba";
System.out.println(maxLcpsLength(str1));
}
}

85

被折叠的 条评论
为什么被折叠?



