Manacher
马拉车算法用于寻找最长回文串。
其思路如下:
First
首先,对于一个字符串,其子串的长度一定是奇数或者偶数。判断回文串过程中也是如此,这就增加了判断的难度。对此,马拉车算法采用如下方法将要找的子串全部变为奇数长度:在每个字符前添加同一个字符#
。
也就是:对于字符串
"a b a b a"
将其变为:
"#a#b#a#b#a#"
这样,对于任意的原字符串中的字符,其回文串一定是奇数长度:
"#a#" || "#a#b#a#" || "#a#b#a#b#a"
但是在马拉车算法中,我们会在两端添加两个与原字符前的特殊字符不同的字符:
"^#a#b#a#b#a#$"
我们先不看原因,按照这种思路继续走。
Second
接下来我们要在经过预处理之后的字符串中找到最大回文串。
在马拉车算法中要到了一个整型数组:p[size], 以及两个整型变量:c(圆心), r(最大半径)。
string:^#a#b#a#b#a#$
p[] :0020406040200
我们可以发现以中心字符为圆心,两边的p数组的值是对称的。
利用这个性质,在寻找回文串的过程中我们可以少算一部分比如对于a右侧的字符b, 我们可以用一下a左侧的字符b的p数组的值。
那么这里就会由几个问题:
Problem 1
以右侧b
的回文串的半径大于左侧b
的回文串长度。
对于上述例子,很可能有下面的情况:
string:^#a#b#a#b#a#b#$
p[] :00204060_______
这个时候我们看右侧b
的回文串长度,明显大于左侧b
。
那么这个时候我们就应该用中心扩展法继续寻找最大半径。
Problem 2
如果以右侧b
为中心得到的回文串长度不足左侧b
呢?
比如:
string:^#b#a#b#a#b#a#$
p[] :002040406040___
此时对于右侧a
,回文串半径为左侧a
的p数组的值时, 就会导致数组越界。
所以这个时候我们应该多一个判断。
比较对称点的p数组的值和该点到边界的距离的大小,选出其中较小的值。
Third
c
圆心和r
半径的更新:
我们逐步地得出p[i]
的值,当此时的p[i]+i
大于当前r
,则更新r
和c
的值。
比如:
i : 012345678901234
string: ^#c#b#c#b#c#b#$
p[] : 00204060_______
mx = 6;
id = 6;
//此时字符b位置为 8,可以得出p[i]值为6, p[i] + i == 14 > mx;
那么为什么要用p[i]+i
?
p[i]
是当前的最大半径,i
为当前位置,那么p[i]+i
则是当前字符位置的回文串的最大范围。id
作为对称点,自然要跟随更新。
接下来我们来看代码:
Code
class mnc {
public:
static string init(string s) {
int len1 = s.size();
string str;
str += "^#";
for (int i = 0; i < len1; i++) {
str+=s[i];
str+="#";
}
str+="$";
return str;
}
static string MNC(const string& s) {
if (s.empty()||s.size()<1)return "";
string str = init(s);
int size = str.size();
int id = 0, mx = 0;
int p[size];
memset(p, 0, sizeof(p));
for (int i = 0; i < size; i++) {
p[i] = mx < i ? 1 : min(mx-i, p[2*id-1]); //如果此时半径小于当前位置则返回 1,因为此时不能使用对称性。
while (str[i-p[i]]==str[i+p[i]])p[i]++; //这里不需要考虑越界的情况。
//更新圆心和半径的值。
if (i+p[i] > mx) {
mx = i+p[i];
id = i;
}
}
int len = 0;
int center = 0;
//我们可以根据p数组来确定新回文串的最大长度和其中的中心字符位置,那么我们怎么通过这两个值得到原字符串中的回文串呢?我们需要得到原串中的回文串的起始位置和串的长度。
for (int i = 0; i < size; i++) {
if (p[i]>len)len = p[i], center = i;
}
center = (center - len) / 2;
return s.substr(center, len-1);
}
};
寻找原字符串中起始字符的位置
目前我们已知:len
, 即新回文串的最大长度;p[i]
,即新字符串中中心字符对应的回文串的最大半径。
比如,对于子串:
[0020406040200] p
"^#a#b#a#b#a#$" New string
[0123456789012] i
"ababa" Old string
其新回文半径中中心字符的位置为6
,新回文串半径为5
,神奇的是,它的值也就是原字符串的长度,那么该回文串的起始字符在原字符串中的位置即为:(i-p[i])/2
。
同理,下面的字符串中,新半径为3
,新位置为3
, 则起始字符位置为(3-3)/2
。
[0204020] p
"#a#b#a#" Ns
[0123456] i
"aba" Os
那么为什么要在两端加上^
和$
呢?
我们去掉这两个看一看。
[020] p
"#a#" Ns
[012] i
"a" Os
此时,新半径为2
, 新位置为1
,i-p[i]
,得到了负数-1。
为了避免这种情况,我们在字符串两端加一个新的特殊字符,同时防止增加的特殊字符影响p
数组的值,这两个特殊字符需和字符前添加的特殊字符不同。
当然,你会发现如果你将上面所有的p
数组的非零值减一,这样所算出的i-p[i]
就是正的。当时试一下的话你会发现,这样会造成数组越界,在上面的代码中我们用到了中心扩展,但是并没有对中心扩展进行范围限制,靠的就是两端的特殊字符一定不同。
所以当然可以不加边界的字符,而在中心扩展时添加条件,但是边界值的确定会有些麻烦,可以自己尝试一下。