manacher算法
一、是什么?
Manacher算法,又叫“马拉车”算法,可以在时间复杂度为O(n)的情况下求解一个字符串的最长回文子串长度的问题。它是对中心扩散法的一个优化。
中心扩散法,简称Z法。
Manacher算法,简称M法。
二、相关知识
- 回文字符串,一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。
- 中心扩散法,以某点为中心向两端扩散,找到以该点为中心的最长回文子串。(代码在最后)
三、为什么?
因为这个方法是对中心扩散法的一个优化。它的时间复杂度为O(n),而中心扩散法是时间复杂度O(n2),空间复杂度O(1)。
它是怎么做到的?Z法是对每个点都从中心向两边查找,而M法部分是从中心,部分是根据对称法则直接得到。
四、怎么做?
回文串分奇偶,所以首先会对字符串加“#”处理。这样可以简化代码。
private fun manacherString(s: String): String {
val sb = StringBuilder()
for (i in 0 until s.length) {
sb.append("#${s[i]}")
}
sb.append("#")
return sb.toString()
}
接着弄清楚几个基本概念和几个结论。
1、回文半径数组radius
记录以每个位置的字符为回文中心求出的最长回文半径长度
比如i位置,radius[i]即为i位置的最长回文半径
2、当前最长回文右边界R
这个位置之前的回文子串所能到达的最右边的地方。
比如当前到达角标i,i之前的角标中radius最大值maxTemp,而R<=i + maxTemp - 1。
3、当前最长回文的对称中心所指的角标c
代码中会取较小的值Math.min(radius[2c-i],R-i+1),正常情况下c>=R/2。
比如当前到达角标i,而radius[i-1]是最大值,则c=i-1,R=radius[i-1]+i-1-1。
2c-i实际上是当前角标i以c为对称中心的对称点,比如现在的角标c+1=i,对称点2*c-i=i-1。
4、如果radius中的最大值为max,则最大回文字符串长度为max-1。
这个结论用文字就可以说清楚,现在能取的最大回文必然是奇数,因为最左和最右必然是“#”,-1后则为偶数,因为这是最大回文半径,所以-1后的值就是最大回文字符串长度。
上面两个图取的是特殊点,是为了简单验证我给出的结论。
五、代码
Z法
/**
* 中心扩展算法
* 从中间往两边,必然是相等的字符
*/
fun longestPalindrome1(s:String):String{
val n = s.length
if (n == 0 || n == 1) {
return s
}
var left = 0
var right =0
for (i in 0 until n) {
//两种对称的情况,中心点在2n或2n+1的位置
val len1 = getLongestLen(s,i,i)
val len2 = getLongestLen(s,i,i+1)
val len = Math.max(len1, len2)
if (len > right-left) {
left = i - (len-1)/2//-1是为了防止出现小于0
right = i+len/2
}
}
return s.subSequence(left,right+1).toString()
}
private fun getLongestLen(s: String, left:Int, right:Int):Int{
var lef = left
var righ = right
while (lef >= 0 && righ < s.length && s[lef] == s[righ]) {
lef--
righ++
}
return righ - lef+1-2//上面的操作会多进行一步,所以要-2
}
M法
/**
* Manacher算法
*“马拉车”算法
* 用“#”字符处理之后的新字符串
* 1、回文半径数组radius 记录以每个位置的字符为回文中心求出的最长回文半径长度
* 2、当前最长回文右边界R 这个位置之前的回文子串所能到达的最右边的地方。
* 3、当前最长回文的对称中心所指的角标c 所以代码中会取较小的值Math.min(radius[2*c-i],R-i+1),正常情况下c>=R/2
* 注:这里的最长回文是指某个点的最长回文,因为从某个点算,可能有多个回文串。
* R <= radius[i] + i-1 取一个特殊的点就可以得到该结论
* radius[i] - 1正好是原字符串中最长回文串的长度
*/
fun longestPalindrome2(s:String):String{
if (s.isEmpty()) {
return s
}
val charArr = manacherString(s)
//回文半径数组 radius[i] >= R-i+1
val radius = IntArray(charArr.length)
var R = 0//当前回文子串右边界
var c = 0//当前最长回文子串中心指向的角标l
var max = -1//最长回文半径
var r = 0//最长回文子串的中心角标
for(i in 0 until radius.size){
//角标小于当前最大回文子串最右侧R时,根据中心对称原则直接计算radius[i]
//否则需要通过下面的while循环计算
radius[i] = if(R>i) Math.min(radius[2*c-i],R-i+1) else 1
//和中心扩散方法一样,寻找该点的最长回文串;同时保证不超过边界
//如果radius[i]为1,则判断i点周围的第一位字符;
//不为1,理论上来说会直接到下一轮循环
while (i + radius[i] < charArr.length && i - radius[i] > -1
&& charArr[i - radius[i]] == charArr[i + radius[i]]) {
radius[i]++
}
//如果回文半径有更新,则更新最右边界,从而提高效率;这就是比Z法快的原因
if (i + radius[i]-1 > R) {
R = i+radius[i]-1
c = i
}
//存储最长回文半径,及其角标
if (radius[i] > max) {
r = i
max = radius[i]
}
}
//最长回文半径为max-1,中心为r,则字符串所在位置[r-(max-1),r+(max-1)]
val sb = charArr.subSequence(r-(max-1),r+(max-1)).replace(Regex("#"),"")
return sb
}