这要从一道题目说起
这道题最好想的做法就是中心扩展,设字符串为
S
S
S,dp(i, j)
记录 从 i
到 j
是否是回文串,那么就有:
{
d
p
(
i
,
j
)
=
d
p
(
i
+
1
,
j
−
1
)
∧
(
S
i
=
=
S
j
)
d
p
(
i
,
i
)
=
1
\begin{cases} dp(i,~j)=dp(i+1,~j-1)\land (S_i==S_j)\\dp(i,~i)=1\end{cases}
{dp(i, j)=dp(i+1, j−1)∧(Si==Sj)dp(i, i)=1
但是这个办法的时间复杂度仍是
O
(
n
2
)
O(n^2)
O(n2),而 Manacher 算法的时间复杂度能达到
O
(
n
)
O(n)
O(n)
为了表述方便,我们定义一个新概念 臂长 arm_len,表示中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * arm_len+ 1
,其臂长为 arm_len
。
下面的讨论只涉及长度为奇数的回文字符串。长度为偶数的回文字符串我们将会在最后与长度为奇数的情况统一起来。
假设我们现在要计算的扩展中心在
S
i
S_i
Si,上一个已计算出的扩展中心为
S
j
S_j
Sj(
j
j
j 并不是
i
−
1
i-1
i−1,而是已计算出臂长的点中
j
+
a
r
m
_
l
e
n
j+arm\_len
j+arm_len 最大的点)那么有以下两种情况:
其中 length
是
j
j
j 的臂长 arm_len[j]
。 2j-1
是
i
i
i 关于
j
j
j 的对称点,我们设它为 i_sym
,
n
n
n 是 arm_len[i_sym]
。
再设 j+arm_len
为 right
,那么
i
i
i 的最小臂长 min_arm_len
就是
min
(
a
r
m
_
l
e
n
[
i
_
s
y
m
]
,
r
i
g
h
t
−
i
)
\min(arm\_len[i\_sym],~right-i)
min(arm_len[i_sym], right−i)。之后只需要在 min_arm_len
的基础上进行中心扩展就行了,这样大大降低了时间复杂度。
时间复杂度:对于每个位置,扩展要么从当前的最右侧臂长 right
开始,要么只会进行一步,而 right
最多向前走
O
(
n
)
O(n)
O(n) 步,因此算法的复杂度为
O
(
n
)
O(n)
O(n)
class Solution {
public String longestPalindrome(String s) {
int start = 0, end = -1;
String str = "#";
for (int i = 0; i < s.length(); ++i) {
str += s.substring(i, i + 1) + "#";
}
s = str;
int [] arm_len = new int [s.length()];
int right = -1, j = -1;
for (int i = 0; i < s.length(); ++i) {
int cur_arm_len;
if (right >= i) {
int i_sym = j * 2 - i;
int min_arm_len = Math.min(arm_len[i_sym], right - i);
cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len);
} else {
cur_arm_len = expand(s, i, i);
}
arm_len[i] = cur_arm_len;
if (i + cur_arm_len > right) {
j = i;
right = i + cur_arm_len;
}
if (cur_arm_len * 2 + 1 > end - start) {
start = i - cur_arm_len;
end = i + cur_arm_len;
}
}
String ans = s.substring(start, end + 1);
ans = ans.replace("#", "");
return ans;
}
public int expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return (right - left - 2) / 2;
}
}