回文如何才能不难
直觉上:回文≈难题,LC上的回文难题, 但是总不能指望面试/周赛出中等题吧?
-
- 回文对
-
- 最短回文串
-
- 分割回文串
-
- 统计不同的回文子序列
-
- 超级回文数
-
- 段式回文
-
…
1. 线性时间内解决回文串问题——Manacher算法 {竞赛难度}
Manacher 算法是在线性时间!内求解最长回文子串的算法。在本题中,我们要求解回文串的个数,为什么也能使用 Manacher 算法呢?这里我们就需要理解一下 Manacher 的基本原理。
Manacher 算法的处理方式是在所有的相邻字符中间插入 # \# #,比如 a b a a a b a a abaaabaa abaaabaa 会被处理成 # a # b # a # a # a # b # a # a # \#a\#b\#a\#a\#a\#b\#a\#a\# #a#b#a#a#a#b#a#a#,这样可以保证所有找到的回文串都是奇数长度的(因为 n + n + 1 = 2 n + 1 n+n+1=2n+1 n+n+1=2n+1),以任意一个字符为回文中心,既可以包含原来的奇数长度的情况,也可以包含原来偶数长度的情况。假设原字符串为 S S S,经过这个处理之后的字符串为 s s s。
我们用 f ( i ) f(i) f(i) 来表示以 s s s 的第 i i i 位为回文中心,可以拓展出的最大回文半径,那么 f ( i ) − 1 f(i) - 1 f(i)−1 就是以 i i i为中心的最大回文串长度 (想一想为什么)。
Manacher 算法依旧需要枚举 s s s 的每一个位置并先假设它是回文中心,但是它会利用已经计算出来的状态来更新 f ( i ) f(i) f(i),而不是向「中心拓展」一样盲目地拓展。具体地说,假设我们已经计算好了 $[1, i - 1] $区间内所有点的 f f f(即我们知道 $[1, i - 1] $这些点作为回文中心时候的最大半径), 那么我们也就知道了 [ 1 , i − 1 ] [1, i - 1] [1,i−1] 拓展出的回文达到最大半径时的回文右端点。例如 i = 4 i = 4 i=4的时候 f ( i ) = 5 f(i) = 5 f(i)=5,说明以第 4 个元素为回文中心,最大能拓展到的回文半径是 5,此时右端点为 4 + 5 − 1 = 8 4 + 5 - 1 = 8 4+5−1=8。所以当我们知道一个 i i i 对应的 f ( i ) f(i) f(i) 的时候,我们就可以很容易得到它的右端点为 i + f ( i ) − 1 i + f(i) - 1 i+f(i)−1。
Manacher 算法如何通过已经计算出的状态来更新 f ( i ) f(i) f(i) 呢?Manacher 算法要求我们维护「当前最大的回文的右端点 r m r_m rm」以及这个回文右端点对应的回文中心 i m i_m im 。我们需要顺序遍历 s s s,假设当前遍历的下标为 i i i。我们知道在求解 f ( i ) f(i) f(i) 之前我们应当已经得到了从 [ 1 , i − 1 ] [1, i - 1] [1,i−1] 所有的 f f f,并且当前已经有了一个最大回文右端点 r m r_m rm 以及它对应的回文中心 i m i_m im。
初始化 f ( i ) f(i) f(i)
- 如果 i ≤ r m i \leq r_m i≤rm,说明 i i i 被包含在当前最大回文子串内,假设 j j j 是 $i $关于这个最大回文的回文中心 i m i_m im的对称位置(即 j + i = 2 × i m j + i = 2 \times i_m j+i=2×im),我们可以得到 f ( i ) f(i) f(i) 至少等于 min { f ( j ) , r m − i + 1 } \min\{f(j), r_m - i + 1\} min{f(j),rm−i+1}。这里将 f ( j ) f(j) f(j) 和 r m − i + 1 r_m - i + 1 rm−i+1取小,是先要保证这个回文串在当前最大回文串内。(思考:为什么 f ( j ) f(j) f(j) 有可能大于 r m − i + 1 r_m - i + 1 rm−i+1?)如果 i > r m i > r_m i>rm,那就先初始化 f ( i ) = 1 f(i) = 1 f(i)=1。
中心拓展
- 做完初始化之后,我们可以保证此时的
s
[
i
+
f
(
i
)
−
1
]
=
s
[
i
−
f
(
i
)
+
1
]
s[i + f(i) - 1] = s[i - f(i) + 1]
s[i+f(i)−1]=s[i−f(i)+1],要继续拓展这个区间,我们就要继续判断
s
[
i
+
f
(
i
)
]
s[i + f(i)]
s[i+f(i)]和
s
[
i
−
f
(
i
)
]
s[i - f(i)]
s[i−f(i)]是否相等,如果相等将
f
(
i
)
f(i)
f(i)自增;这样循环直到
s
[
i
+
f
(
i
)
]
≠
s
[
i
−
f
(
i
)
]
s[i + f(i)] \neq s[i - f(i)]
s[i+f(i)]=s[i−f(i)],以此类推。我们可以看出循环每次结束时都能保证$ s[i + f(i) - 1] = s[i - f(i) + 1]$,而循环继续(即可拓展的条件)一定是
s
[
i
+
f
(
i
)
]
=
s
[
i
−
f
(
i
)
]
s[i + f(i)] = s[i - f(i)]
s[i+f(i)]=s[i−f(i)]。 这个时候我们需要注意的是不能让下标越界,有一个很简单的办法,就是在开头加一个 $$$,并在结尾加一个
!
!
!,这样开头和结尾的两个字符一定不相等,循环就可以在这里终止。
这样我们可以得到 s s s 所有点为中心的最大回文半径,也就能够得到 S S S 中所有可能的回文中心的的最大回文半径,把它们累加就可以得到答案。
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
string t = "$#";
for (const char &c: s) {
t += c;
t += '#';
}
n = t.size();
t += '!';
auto f = vector <int> (n);
int iMax = 0, rMax = 0, ans = 0;
for (int i = 1; i < n; ++i) {
// 初始化 f[i]
f[i] = (i <= rMax) ? min(rMax - i + 1, f[2 * iMax - i]) : 1;
// 中心拓展
while (t[i + f[i]] == t[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;
}
};
2. Rabin-Karp编码
214. 最短回文串
给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
示例 1:
输入: "aacecaaa"
输出: "aaacecaaa"
示例 2:
输入: "abcd"
输出: "dcbabcd"
我们可以用Rabin-Karp编码判断一个子集是否为回文串, 设 A S C I I ( i ) ASCII(i) ASCII(i)表示字符的ascii码.
b
a
s
e
base
base可以取比整个字符集大的素数,
m
o
d
mod
mod可以取一个很大的幂加上一个素数,比如
1
0
5
+
7
10^5+7
105+7.
f
(
i
)
和
f
(
i
)
^
f(i)和\hat{f(i)}
f(i)和f(i)^分别表示字符串编码和对应的回文串编码.
f
(
i
)
=
f
(
i
−
1
)
∗
b
a
s
e
%
m
o
d
+
s
[
i
]
f
(
i
)
^
=
(
f
(
i
−
1
)
^
+
s
[
i
]
∗
b
a
s
e
i
)
%
m
o
d
f(i) = f(i-1)*base\% mod + s[i]\\ \hat{f(i)} = (\hat{f(i-1)} +s[i]*base^i)\%mod
f(i)=f(i−1)∗base%mod+s[i]f(i)^=(f(i−1)^+s[i]∗basei)%mod
由于题目只要求在字符串前加字符,因此可以判断
s
s
s中最长回文前缀,
代码如下:
class Solution {
public:
string shortestPalindrome(string s) {
if(!s.size()) return "";
//思路,回文数判断:Manacher算法
//反过来思考:去掉最少几个字符它将称为回文数
int left = 0, right = 0, mul = 1;
int base = 157, mod = 1E5+7;
int loc = 1;
int n = s.size();
for(int i = 0;i < n;i++)
{
left = (long long)left*base%mod + (int)s[i];
right = (right+ (int)s[i]*mul)%mod;
if(left == right)
{
loc = i;
}
mul = (mul*base)%mod;
}
string add = (loc==n-1)?"":s.substr(loc,n-loc);
reverse(add.begin(),add.end());
return (add+s);
}
};
复杂度分析
- 时间复杂度: O ( ∣ s ∣ ) O(|s|) O(∣s∣)。
- 空间复杂度: O ( 1 ) O(1) O(1)。