Mancher算法
1. 介绍
- 用来计算回文子串,用一个数组记录每个位置上,以当前位置为中心,往两边扩的最长回文半径
2. 求回文子串长度
有两种方法:
方法一:
- 暴力求解,无优化,以每个位置为中心,往两侧匹配。
- 这个方法有一个缺陷,只能匹配奇数的时候,比如
aba
,如果是abba
中间的两个b就匹配不上。 - 解决方法是将每个字符中间都加上一个特殊字符
#
,任意字符都行,就算是字符串中存在的字符也是可以的 - 然后字符串
abba
就会变成#a#b#b#a#
,这时候再往两侧扩就不会出现错误
方法二:
- Mancher算法求长度
- 首先将原字符串加上特殊字符,变成Mancher串
- 由变量C,R 和一个数组arr 进行求最长回文子串长度
- 变量C的含义:最长回文子串的中心位置
- 变量R的含义:最右匹配成功的位置 + 1
- arr数组的含义:当前位置的回文长度
- for循环从
i = 0
遍历到字符串结尾有四种情况:- 了解4个case之前,需要了解一个关键点
- 因为C是中心位置,C所在的位置左右两侧有一个很大的回文区域
left...right
范围,然后i
位置是在C位置之后的,所以i
位置可以跟 C位置作一个对称,得到i'
,也就是这样子i'...C...i
- case1:
i > R
代表i
在R
外,只能暴力求解 - case2:
i < R
表示i
在R
内,此时i
根据C找到对应i'
的位置,直接取arr[i']
的值,因为在C这个大范围内,C的左侧跟C的右侧都是回文,因为i'
是i
的对称,所以arr[i']
的值和arr[i]
的值也一样。 - case3:当
i < R
,此时i
根据C找到对应i'
的位置,如果i'
的范围已经不在C的回文范围内了,这时候就不能直接取arr[i']
的值了,因为i
在C的回文范围内,所以i
的最长回文长度只能是C回文范围的右侧,也就是R,所以arr[i] = R - i
- case4:当
i < R
,此时i
根据C找到对应i'
的位置,如果i'
的左侧压到了C回文范围的左侧,这时候就不能直接取arr[i']
的值了,但是至少可以取一个已经确定的回文长度,也就是C的右侧回文范围,也就是R的位置,所以arr[i]
的回文长度可以从R-i
开始计算,如果后面遇到了回文,长度就继续增加
3. 完整代码
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
string mancherString(string s) {
string str = "#";
for (int i = 0; i <= s.size(); i++) {
str.push_back(s[i]);
str.push_back('#');
}
return str;
}
int main() {
string s = "abc1234321ab";
string str = mancherString(s); // #a#b#c#1#2#3#4#3#2#1#a#b#
vector<int> pArr(str.size(), 0);
int C = -1; // 回文串的中心位置
int R = -1; // 最右的扩成功位置,再下一个位置
int Max = -1;
for (int i = 0; i < str.size(); i++) {
// i'...C...i
// 当前位置是i 可以根据C对称的找到i' , 因为C所在的位置左右两侧有一个很大的回文区域left...right范围
// case1: i'回文串的范围 在C回文串范围left和right内
// 此时i可以直接取i'的值,因为i也在C的范围内,然后又是对称取的i'
// case2: i'回文串的范围 在C回文范围left和right外
// 因为i'的范围已经不在left和right内了,但是i在C的回文范围内,i最大长度也只能到C的right,也就是R
// 此时直接出答案 i的值为R-i
// case3: i'回文串的范围 左侧压到了C回文范围的left
// 也就是说,i的最小长度是可以到C的right,当初C的left-1和right+1不是回文,只是因为这两个值不同
// 但是有可能i的left-1位置和C的right+1位置相同,所以说得从right+1开始判断是否回文
pArr[i] = R > i ? min(pArr[C * 2 - i], R - i) : 1; // C * 2 - i 就是 i'的位置
// i - pArr[i]相当于left , i + pArr[i]相当于right
// i - pArr[i] 左边最开始待匹配的位置 , i + pArr[i] 右边最开始待匹配的位置
while (i - pArr[i] > -1 && i + pArr[i] < str.size()) {
// 暴力扩,如果右侧第一个位置跟左侧第一个位置相等,代表回文长度需要加1,因为又是一个回文
if (str[i - pArr[i]] == str[i + pArr[i]]) {
pArr[i]++;
}
else {
// 如果中了case1和case2会直接退出循环
break;
}
}
// 更新C和R
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
// 记录最大长度
Max = max(Max, pArr[i]);
}
cout << "Ans :" << Max - 1 << endl; // Ans :7
return 0;
}
4. 关于Mancher的相关题目
对于一个字符串,我们想通过添加字符的方式使得新的字符串整体变成回文串,但是只能在原串的结尾添加字符,请返回在结尾添加的最短字符串。
给定原字符串A及它的长度n,请返回添加的字符串。保证原串不是回文串。
测试样例:
“ab”,2
返回:“a”
题目是核心代码模式,我这边写成了ACM模式
思路:
Mancher算法中的arr数组中每个位置的含义:以当前位置为中心,向两侧扩,最长回文子串的长度
所以这题用Mancher来解,题意是在字符串末尾加字符,所以说只需要在求arr数组的时候,遇到了一个位置的右侧可以扩到字符串末尾,把长度保存下来,然后退出循环遍历
在原字符串中求0位置
到原字符串的长度减去刚刚保存的长度加1
的子串,然后将子串逆序,就是答案了
代码
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
string mancherString(string s) {
string str = "#";
for (int i = 0; i < s.size(); i++) {
str.push_back(s[i]);
str.push_back('#');
}
return str;
}
int process(string str) {
int C = -1;
int R = -1;
int ans = 0;
vector<int> pArr(str.size());
for (int i = 0; i != str.size(); i++) {
pArr[i] = R > i ? min(pArr[C * 2 - i], R - i) : 1;
while (i + pArr[i] < str.size() && i - pArr[i] > -1) {
if (str[i + pArr[i]] == str[i - pArr[i]]) {
pArr[i]++;
}
else {
break;
}
}
if (i + pArr[i] > R) {
C = i;
R = i + pArr[i];
}
if (R == str.size()) {
ans = pArr[i];
break;
}
}
return ans;
}
int main() {
string s = "abbbbbaa";
string str = mancherString(s);
int len = process(str);
cout << len;
string ans = s.substr(0, s.size() - len + 1);
reverse(ans.begin(), ans.end());
cout << ans;//bbbbba
return 0;
}
推荐文章