最近看到一个回文子串的算法问题,马拉车算法。找了很多资料,文字的,B站的,甚至抖音的,依然一知半解。真的是一口老血喷出来。不知道是不是减肥不咋吃饭的过,觉得脑子也是傻的。现在把我理解的整理一下。
先放上代码,抄的php版本
function longestPalindrome($s)
{
$T = malache($s);
//$T = $s;
$n = strlen($T);
$C = $R = 0;
$p = [];//每个位置的回文子串的数
for ($i = 1; $i < $n - 1; $i++) {
$i_mirror = $C * 2 - $i;//当前中心点对称轴
if ($R > $i) {
$p[$i] = min($R - $i, $p[$i_mirror]);
} else {
$p[$i] = 0;
}
while (($T[$i - 1 - $p[$i]]) == ($T[$i + 1 + $p[$i]])) {
$p[$i]++;
}
if ($i + $p[$i] > $R) {
$C = $i;//中心位置
$R = $i + $p[$i];//最右位置
}
}
$maxLen = 0;
$centerIndex = 0;
//求最大的半径中心和半径值
for ($i = 1; $i < $n - 1; $i++) {
if ($p[$i] > $maxLen) {
$maxLen = $p[$i];
$centerIndex = $i;
}
}
$start = ($centerIndex - $maxLen) / 2;
echo substr($s, $start, $maxLen);
}
function malache($str)
{
$n = strlen($str);
if (!$str) {
return "";
}
$ret = '';
for ($i = 0; $i < $n; $i++) {
$ret .= '#' . $str[$i];
}
$ret .= "#";
return $ret;
}
longestPalindrome("aabccbccbab8");
1.先说一下回文子串,这个我也是昨天刚知道回文串这个东西, 就是,正反都一样的句子。
比如:西湖垂柳丝柳垂湖西,静泉山上山泉静。这里不做中文的比较,就是举个例子
比如:aba,aabcacbaa。
2.解法
从第一个字符开始查找,两边的数据,如果相同,则给相应的中心点的长度值$p[$i]加1。依次类推。p为每个中心点左右两边相等的长度的值,如aba,a位置左右没有相同的值p=0;b位置左右两边a==a,所以b的半径长度值为1;a左右没有相同值所以p=0
a | b | a | |
i | 0 | 1 | 2 |
p | 0 | 1 | 0 |
因为是中心点向两边查找,所以会有个问题,偶数的时候,比如:abba,也是回文子串,但是用这个方式,a的p[0]=0,b的p[1]=0,b的p[2]=0,a的p[3]=0,半径都为0。所以为了解决这个问题,需要把所有的字符串改为奇数。也就是,在每一个字符前跟一个特定字符,#¥%&都可以,结尾再加个特定字符。2*len+1,永远为奇数。出来的结果是:#a#b#b#a#。半径结果为:
# | a | # | b | # | b | # | a | # | |
p | 0 | 1 | 0 | 1 | 4 | 1 | 0 | 1 | 0 |
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
然后把所有的p的值取最大,也就是p[4] = 4为最大的回文子串的中心点。
取起始位置,因为加了#,所以起使位置为:中心点4-长度4再除以2 = 4-4/2。再substr取就可以了。
但是,上述只是暴力解法,还不是马拉车。
马拉车,加了一个最右,每一个位置,都有它最右的回文子串的位置,如下图,比如:#a#为一个回文子串,所以1a的最右位置为2。
# | a | # | b | # | b | # | a | # | |
p | 0 | 1 | 0 | 1 | 4 | 1 | 0 | 1 | 0 |
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
R | 0 | 2 | 2 | 4 | 8 | 8 | 8 | 8 | 8 |
当循环到4#时,最右位置为8。下一位置5b在8里面,也就是说,5b肯定是有,相对于4#的一个对称的3b的。5b对应的3b位置为:中心位置4 * 2 - 5b位置 = 4 * 2 - 5 = 3,这个为对称轴的公式。如下图,对于4#,左右两边颜色相同,回文串的特性。
# | a | # | b | # | b | # | a | # |
如下图,起始半径取对称轴的p半径,标注了相同颜色
# | a | # | b | # | b | # | a | # | |
p | 0 | 1 | 0 | 1 | 4 | 1 | 0 | 1 | 0 |
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
R位置 | 0 | 2 | 2 | 4 | 8 | 8 | 8 | 8 | 8 |
起始半径 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
所以5b起始比较的是5-1-1,和5+1+1:3b==7a
3b的半径i为1,5b+1<最右半径8,所以右则5b的半径肯定>=1(因为是回文子串的特性),所以5b半径的递增是从5-1,5+1开始的。
//如果查找的数,在半径内,则取对称轴和最右位置-当前位置,取最小的作为回文子串数。这块,考虑的是最右位置是否是在对称的3b的半径内。
//如果3b的半径为4,那5b的半径要从当前最右半径8-当前位置5和4之间取一个最小值作为5b的起始半径。结果是不能超过最右半径。
if ($R > $i) {
$p[$i] = min($R - $i, $p[$i_mirror]);
} else {
$p[$i] = 0;
}
//左右两边是否相等,相等,则回文子串数+1
while (($T[$i - 1 - $p[$i]]) == ($T[$i + 1 + $p[$i]])) {
$p[$i]++;
}
如果超过了最右位置,则要重新开始计算中心点和最右位置,比如:abacbcde指针到a后则中心位置变为c的位置,按上述,继续类推
if ($i + $p[$i] > $R) {
$C = $i;//中心位置
$R = $i + $p[$i];//最右位置
}
最后输出计算
$start = ($centerIndex - $maxLen) / 2;
echo substr($s, $start, $maxLen);
总结一下,感觉讲的还不是很清楚,先记录一下。