原文链接:英文版链接
首先,我们将字符串S中插入符号“#”转化成另一个字符串T。
比如:S = "abaaba",T = “#a#b#a#a#b#a#”。
为了找到最长回文字串,我们需要围绕Ti进行扩展,Ti-d...Ti+d是一个回文,很明显d是围绕Ti形成的回文的长度。
将每个回文的长度用数组P存起来,这样,P[i]就代表围绕Ti的回文长度,最长回文字串将会是P中的最大元素。
用上面的例子,我们得到的P的结果是(从左至右):
T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0
看P的结果,我们可以发现,最长回文子串就是"abaaba",从而可以得出就是P6 = 6。
插入“#”后,字符串的长度就都转化成奇数长度了(请注意:这只是为了更好的示范该算法。译者注:我发现这是为了避免类似"aa"这样的回文无法正确用P表示的情况)。
现在,想象一下给"abaaba"划一根竖线,有没有发现P的数值根据这根线中心对称?不仅仅是这个字符串,像回文"aba"也是如此,P中的这些数字也反应出了相似的对称性。这是巧合吗?有时候是,有时候不是。这个现象只在一种条件下是符合的,但不管怎么样这是一个不小的进步,这样我们就可以排除掉一些P[i]的值。
我们从一个更加包含更多回文,更能说明问题的一个例子出发,假设S = “babcbabcbaccba”。
上面的图片显示T从字符串S转化过来。假设这个状态下P已经部分完成了,竖实线表示回文“abcbabcba”的中心(C),虚线表示各自的左边缘(L)和右边缘(R),现在我们正在索引i的位置,并且围绕C它的映射是i',那么我们如何马上计算出P[i]?
假设我们已经到达索引i = 13,我们需要计算出p[13](上面的?标记的位置),我们先看看它围绕C的映射i'=9。
绿色的横线分别代表由i和i'为中心的回文所占的区域,我们发现i的镜像P[i'] = p[9] = 1,很明显,由于回文环绕其中心的对称性,我们得出P[i]也必须是1。
由于对称性,从上面很明显可以得出P[i] = p[i'] = 1,实际上,从C开始的后三个元素都可以从对称性得出它们的值(P[ 12 ] = P[ 10 ] = 0, P[ 13 ] = P[ 9 ] = 1, P[ 14 ] = P[ 8 ] = 0)。
现在我们在索引i = 15,以C为中心它的镜像是i' = 7,那么是否P[15] = P[7] = 7?
现在我们在索引i = 15,P[i]的值是多少?如果我们依据对称性,P[i]的结果应该跟P[i']同为7,但这是错误的。如果我们以T15为中心扩展,我们会得到回文“a#b#c#b#a”,它实际上比从对称性得出的结果7要短,为什么呢?
有色线条表示了以i和i'为中心的区域,实绿线表示由于对称性两者必须相同的区域,红实线表示两边可能不相同的区域,虚绿线表示超过了中心的区域
很明显两根实绿线表示的区域必须相同,超过了中心的区域也必须对称(虚绿线表示),这里必须注意P[i']是7,它超出了以C为中心的回文的左边缘L(实红线部分),这样的话它不再遵循回文的对称性了,我们只知道P[ i ] ≥ 5,为了找到P[i]的值我们必须越过右边缘(R)进行特征对比。在这里,既然P[21] != P[1],得出结论P[i] = 5。
这样我们就得出这个算法的核心部分了:
if P[ i' ] ≤ R – i, then P[ i ] ← P[ i' ] else P[ i ] ≥ P[ i' ]. (这里我们必须越过右边界R寻找P[i]的值)
很优雅吧?如果这句话理解了,你就理解了这个算法的核心部分了,这也是最难的部分。
最后的部分是我们该什么时候把C和R的位置往右移动,这个很简单:
如果以i为中心的回文越过了R,我们将C更新为i(回文中心),然后将R扩展为新回文的右边缘。
每移动一步,有两种可能。如果P[ i ] ≤ R – i,我们设置P[i] = p[i'],这个只需要一步。否则我们尝试将回文的中心移到i,并且从右边缘R开始扩展之。扩展R(内部循环)最多需要N步,然后定位和测试中心点也需要N步。最终,这个算法可以保证在2*N步之内完成,得到了一个线性时间解。
现在我们可以通过P获取最长回文的长度maxLen及其中间位置的索引i,那么我们应该截取哪一段字符串才是我们需要的回文子串呢。
对比一下S和T,我们会发现,如果字母A在T中的位置为n的话,那么它在S中的位置就是(n - 1)/2,那是不是截取字符串的起始位置就是(i - maxLen - 1)/2?其实不是的,注意到在T中回文的第一个字母肯定是“#”,所以我们需要先把位置往后移一位,到“#”后面的第一个字母,也就是我们需要的回文的起始字母(比如"#a#a#",我们需要的是"a#a"的起始位置而不是"#a#a#"的起始位置),那最终的结果就是(i - maxLen + 1 - 1)/2,也就是(i - maxLen)/2。
下面奉上javascript的算法。
function maxPalin(t) { var c = 0, R = 0, p = [0], s = '#' + t.split('').join('#') + '#', maxLen = 0, center; for (var i = 1, len = s.length; i < len; ++i) { var iMirror = 2*c - i; p[i] = R > i ? Math.min(R - i, p[iMirror]) : 0; while(s[i - 1 - p[i]] && (s[i - 1 - p[i]] === s[i + 1 + p[i]])) { ++p[i]; } if (p[i] > R - i) { c = i; R = i + p[i]; } } for (i = 1; i < len; ++i) { if (p[i] > maxLen) { maxLen = p[i]; center = i; } } return t.substr((center - maxLen) / 2, maxLen); }