Manacher 马拉车算法
5. 最长回文子串 - 力扣(LeetCode)
马拉车算法是目前解决寻找字符串中最长的回文子串时间复杂度最低的算法(线性O(n)).
中心扩散法
初始化一个长度与字符串 s
相等的 臂长数组 arr
和 最长臂长 max
与 最长臂长回文串中心下标 maxIndex
, 对字符串 s
进行迭代 , 初始化两个指针 left
和 right
分别指向当前位置 i
的两边 , 以判断当前位置字符的左右字符是否相等 和 左右下标是否超过数组边界 , 若相等且不超过边界则指针向外扩散并再次判断 . 退出判断后检测更新 max
和 maxIndex
.
原始的中心扩散法只能处理奇数回文串中心的情况 . 因此需要对原始字符串进行映射扩充 : 在开头 , 结尾以及字符之间插入任意一个相同的 , 非字符串可能存在的字段的特殊字符 ( 一般为 #
) , 这样字符串的长度由 n
映射到 2*n+1
, 如果出现偶数回文串中心的情况 , 映射到新字符串则是落在特殊字符上 , 最后还原即可 , 这样保证了对字符串臂长的计算均建立在奇数中心的情况 .
观察最坏情况 : 字符串每个字符均相同 , 长度为 n
, 则对于每个字符串均需要左右指针扩散计算 , 从 0
次 到 n/2
次 再到 0
次 , 得到计算次数大致为 n^2/4
次 , 时间复杂度为 O(n^2)
.
import java.util.StringJoiner;
class Solution {public String longestPalindrome(String s) {int n = s.length();StringJoiner sj = new StringJoiner("#", "#", "#");for(int i = 0; i < n; ++i) {sj.add(s.substring(i, i + 1));}int number = sj.toString().length();char[] charArr = sj.toString().toCharArray();int[] arr = new int[number];int max = 0;int maxIndex = 0;int left, right;for(int i = 0; i < number; ++i) {left = i - 1;right = i + 1;while(left >= 0 && right < number) {if(charArr[left] == charArr[right]){++arr[i];--left;++right;}else break;}if(arr[i] > max) {max = arr[i];maxIndex = i;}}max = (max - 1) / 2;if((maxIndex - 1) % 2 == 1) { // 偶数中心maxIndex = (maxIndex - 1) / 2;return s.substring(maxIndex - max, maxIndex + max + 2);}maxIndex = (maxIndex - 1) / 2;return s.substring(maxIndex - max, maxIndex + max + 1);}
}
Manacher 对中心扩散法的优化
中心扩散法对每个字符进行计算明显具有冗余 , Manacher
马拉车算法的目的即是尽可能的减少计算 , 降低时间复杂度 : 注意到回文串的臂长具有 对称性 : 回文串以中心点为对称轴 , 其左右两侧的字符是相等的 , 回文串最远距离内任意非中心字符的臂长可以继承对应字符的臂长 . 为了实现这一便利 , 我们需要在每次迭代时额外维护两个量 最右边界r
和 最右边界对应的大回文串中心centerIndex
.
arr[i] = Math.min(r - i, arr[2 * centerIndex - i]);
是算法的核心语句 , 可以认为当回文子字符的臂长没有超过大回文的边界时 , 我们对该该字符是 完全知悉的 , 臂长在大回文内即被截断 . 若镜像点的回文半径较小以至于其完全包含在以 centerIndex
为中心的大回文串中 , 那么当前字符的回文半径可以直接继承完全知悉的镜像点 arr[2 * centerIndex - i]
, 不需要再进行计算 ; 若镜像点的回文半径较大以至于超出了大回文串的边界 , 则当前字符的回文半径至少可以继承到大回文的最右边界right - i
, 而我们对大回文外的字符分布并不知悉 , 因此后续需要计算尝试扩展 .
同样观察最坏情况 : 字符串的每个字符均相同 , 对于前半字符串的每个位置 n
需要计算 n-(n-1)
即 1
次 , 后半字符串因为镜像点始终完全知悉 , 直接继承即可 , 计算次数也为 1
次 , 总计算次数为 n
次 , 时间复杂度为 O(n)
.
class Solution {public String longestPalindrome(String s) {int n = s.length();StringBuilder sb = new StringBuilder();sb.append("#");for(int i = 0; i < n; ++i) {sb.append(s.substring(i, i + 1)).append("#");}int number = sb.toString().length();char[] charArr = sb.toString().toCharArray();int[] arr = new int[number];int max = 0;int maxIndex = 0;int r = maxIndex + max;int centerIndex = 0;int left, right;for(int i = 0; i < number; ++i) {left = i - 1;right = i + 1;if(i <= r) {arr[i] = Math.min(r - i, arr[2 * centerIndex - i]); // Manacher核心语句left -= arr[i];right += arr[i];}while(left >= 0 && right < number && charArr[left] == charArr[right]) {++arr[i];--left;++right;}if(arr[i] > max) {max = arr[i];maxIndex = i;}if(r < arr[i] + i) {r = arr[i] + i;centerIndex = i;}}int start = (maxIndex - max) / 2;int end = start + max;return s.substring(start, end);}
}