回文子串
1.题目描述
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:"abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:"aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
2.题解
2.1 中心扩展
public int countSubstrings(String s) {
int n = s.length(), ans = 0;
for (int i = 0; i < 2 * n - 1; ++i) {
int l = i / 2, r = i / 2 + i % 2;
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
--l;
++r;
++ans;
}
}
return ans;
}
遍历所有可能的回文中心,累加回文子串的个数。
2.2 Manacher算法
public int countSubstrings(String s) {
int n = s.length();
StringBuffer t = new StringBuffer("$#");
for (int i = 0; i < n; ++i) {
t.append(s.charAt(i));
t.append('#');
}
n = t.length();
t.append('!');
int[] f = new int[n];
// rMax为最大回文右端点,iMax为最大回文右端点的回文中心
int iMax = 0, rMax = 0, ans = 0;
for (int i = 1; i < n; ++i) {
// 初始化 f[i]
f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1;
// 中心拓展
while (t.charAt(i + f[i]) == t.charAt(i - f[i])) {
++f[i];
}
// 动态维护 iMax 和 rMax
if (i + f[i] - 1 > rMax) {
iMax = i;
rMax = i + f[i] - 1;
}
// 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
ans += f[i] / 2;
}
return ans;
}
Manacher算法的处理方式是在所有的相邻字符中间插入#
,比如abaa
会被处理成#a#b#a#a#
,这样可以保证所有找到的回文串都是奇数长度的。假设原字符串为S
,经过这个处理之后的字符串为s
。
我们用f(i)
来表示以s
的第i
位为回文中心,可以拓展出的最大回文半径。
以aaa
为例:
注意到f(4)=4
,即以s[4]
为回文中心的最大回文子串为#a#a#a#
,此时,iMax
为4
,rMax
为7
。
由于5<7
,所以f(5)
可能为f(3)
或者为7-5+1=3
。考虑以下代码:
// ...
Math.min(rMax - i + 1, f[2 * iMax - i])
// ...
这里为什么要取较小值呢?以aba
为例:
这里f(5)=f(3)
,因为s[2]=s[6]
,s[2]≠s[4]
,s[4]≠s[6]
。
以cabacdcabae
为例:
注意到由于s[2]≠s[22]
,以s[12]
为回文中心的最大回文子串为s[3:21]
。
这里考虑f(18)
可能为f(6)
或者21-18+1=4
。
由于s[2]=s[10]
,以s[6]
为回文中心的最大回文子串包含s[2]
。
如果f(18)=f(6)
,意味着以s[18]
为回文中心的最大回文子串包含s[22]
,这就要求s[22]=s[14]
,根据对称性可知,s[10]=s[14]
,于是得出s[2]=s[22]
,这显然不对。
因此,当i <= rMax
时,f(i)
只能取rMax - i + 1
和f[2 * iMax - i]
中的较小值。
最后,ans += f[i] / 2
表示累加以第i
位为回文中心的回文子串的个数,比如对于$#c#a#b#a#c#
,以s[6]
为回文中心,其回文子串包含c#a#b#a#c
、a#b#a
和b
,其回文子串个数为f(6)/2=3
。
注意:#a#b#a#
和a#b#a
是一样的。
Manacher算法避免了像中心扩展那样盲目地扩展,这里先初始化f[i]
再中心扩展。
// ...
while (t.charAt(i + f[i]) == t.charAt(i - f[i])) {
++f[i];
}
// ...
参考: