原理
计算每个子串是是否回文,看起来是个 O ( n 2 ) O(n^2) O(n2)的问题,比较朴素的做法是区间dp或者中心拓展法。
但是如果换一个表达形式,计算以每个位置为回文中心的最大回文半径 p i p_i pi,就有希望将复杂度降低到 O ( n ) O(n) O(n),只要计算出这个 O ( n ) O(n) O(n)个元素的 p p p数组即可,而这可以借助马拉车做到
马拉车类似 z z z函数,建议先和 z z z函数放一起学习,对于每个位置 i i i,回文半径 p i p_i pi,则表示 [ i − p i , i + p i ] [i-p_i,i+p_i] [i−pi,i+pi]这区间是 i i i为中心的最大回文串,我们把这个区间当成类似 z z z函数里的 z b o x zbox zbox,维护右端点最大的区间。
如果当前 i i i在区间内,则类似 z f u n c zfunc zfunc的计算,我们可以复用前面的计算结果。具体来说就是由于 i i i所在的这个区间是个回文,把 i i i关于区间中心轴对称过去 2 c − i 2c-i 2c−i,那边的字符和这边是一个镜像关系。
2 c − i 2c-i 2c−i这个位置的回文半径,在不超出 b o x box box的范围内,和 i i i是相同的。超出部分,再暴力匹配,可以看到这个思路和 z f u n c zfunc zfunc是完全一样的,只是 z z z函数利用的是后缀和整个串的 l c p lcp lcp是一段相同串,这里利用的是回文串对称位置,是镜像关系。
复杂度分析也类似,只有超出 r r r才会暴力,并把 r r r更新到暴力的结尾,所以 r r r的移动次数等于暴力更新的次数,最多 O ( n ) O(n) O(n)
实现
实现上的重点是,如果想一次求出所有奇数长度和偶数长度的回文串,可以在原串的每个字符两侧都插入一个填充字符,这样以原始字符为回文中心的结果,对应奇回文,以填充为中心的结果,对应偶回文,只是这样最后需要做一些下标转换,才能得到原始串上的回文串。
具体转换就是最后 r e t u r n return return那里,最长回文串的回文中心 m x i mx_i mxi,最大回文半径 m x mx mx,可以计算出这个回文串在原始串中的起始位置 ( m x i − m x ) / 2 (mx_i-mx)/2 (mxi−mx)/2,并且对应地回文串长度就是 m x mx mx
c
string manacher(const string& s) {
string t = "^#";
for (char c : s) {
t += c;
t += "#";
}
t += '$';
int n = t.size();
vector<int> p(n);
int r = 0, c = 0;
int mx = 0, mx_i = 0;
for (int i = 1; i < n - 1; i++) {
if (i < r) {
p[i] = min(r - i, p[2 * c - i]);
}
while (t[i + p[i] + 1] == t[i - p[i] - 1]) {
p[i]++;
}
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
if (p[i] > mx) {
mx = p[i];
mx_i = i;
}
}
return s.substr((mx_i - mx) / 2, mx);
}
例题
回文串个数,这就体现马拉车的优势了,对每个回文中心,只要知道回文半径 p i p_i pi,就意味着有 p i p_i pi个回文串,注意我这个板子的 p i p_i pi实际上是处理后的串的回文半径,原始串的回文半径需要除二,并且这里的半径不包含 [ i , i ] [i,i] [i,i]这个串,所以如果最大回文串长度是,回文串个数还需要加一,实现上就 ( p i + 1 ) / 2 (p_i+1)/2 (pi+1)/2
在开头添加最少的字符,使得回文。
这题前面做过,kmp做的。既然回文,马拉车是更简单的做法。实际上就是找最长回文前缀,枚举每个回文中心,检查他的最大回文串是不是一个前缀,也就检查最大回文串的左端点是不是 0 0 0,这里的转换就用前面提到的公式, l i = ( i − p i ) / 2 , l_i=(i-p_i)/2, li=(i−pi)/2,如果是,更新答案。
dfs序+马拉车
询问树上每个子树,按后序遍历构造的字符串,是不是回文。
由于后续也是一种dfs序,后序遍历生成的字符串,一个子树对应的串是在一个连续区间内的,只不过由于后续,子树的根是最后访问的,所以对应区间不再是先序的 [ d f n i , d f n i + s z i − 1 ] [dfn_i,dfn_i+sz_i-1] [dfni,dfni+szi−1],而是 [ d f n i − s z i + 1 , d f n i ] [dfn_i-sz_i+1,dfn_i] [dfni−szi+1,dfni]。
然后我们再后序遍历生成的串上跑马拉车,即可 O ( 1 ) O(1) O(1)检查每个区间是否回文。检查时由于我们只知道左右端点,还需要转化成回文中心,并且计算在处理后串(有填充字符)中的下标。
c
class Solution {
public:
vector<bool> findAnswer(vector<int>& parent, string s) {
int n = s.size();
vector<bool> ans(n);
vector<vector<int>> g(n + 1);
vector<int> dfn(n + 1), sz(n + 1, 1);
int cnt = 0;
for (int i = 1; i < n; i++) {
g[parent[i]].push_back(i);
}
for (int i = 0; i < n; i++) {
ranges::sort(g[i]);
}
string ss;
ss.reserve(n);
auto&& dfs = [&](auto&& dfs, int u) -> void {
for (int v : g[u]) {
dfs(dfs, v);
sz[u] += sz[v];
}
ss.push_back(s[u]);
dfn[u] = ++cnt;
};
dfs(dfs, 0);
// cout << ss << '\n';
string t = "^#";
for (char c : ss) {
t += c;
t += "#";
}
t += '$';
int m = t.size();
vector<int> p(m);
int r = 0, c = 0;
for (int i = 1; i < m - 1; i++) {
if (i < r) {
p[i] = min(r - i, p[2 * c - i]);
}
while (t[i + p[i] + 1] == t[i - p[i] - 1]) {
p[i]++;
}
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
}
for (int i = 0; i < n; i++) {
int r = dfn[i];
int l = r - sz[i] + 1;
int len = (r - l + 1);
int mid;
if (len % 2) {
mid = (l + len / 2) * 2;
} else {
mid = (l + len / 2) * 2 - 1;
}
// cout << l << ' ' << r << ' ' << mid << '\n';
// cout << p[mid] << ' ' << len << '\n';
ans[i] = (p[mid] >= len);
}
return ans;
}
};
回文+划分型dp
问能否划分成三个区间,每个都是回文,划分型dp即可
f ( i ) ∣ = f ( j − 1 ) & & g ( j , i ) f(i)\mid=f(j-1)\&\& g(j,i) f(i)∣=f(j−1)&&g(j,i)
马拉车预处理,然后查询一个区间是否回文 g ( j , i ) g(j,i) g(j,i),可以给马拉车增加一个这个接口,还挺常用的
c
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Manacher {
std::vector<int> p;
std::string t;
Manacher(const std::string& s) {
// 1. 预处理字符串
t = "^#";
for (char c : s) {
t += c;
t += "#";
}
t += '$';
int n = t.size();
p.resize(n, 0);
int r = 0, c = 0;
for (int i = 1; i < n - 1; i++) {
if (i < r)
p[i] = std::min(r - i, p[2 * c - i]);
while (t[i + p[i] + 1] == t[i - p[i] - 1])
p[i]++;
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
}
}
// 新增:询问 s[L...R] (闭区间) 是否为回文
bool isPalindrome(int L, int R) const {
if (L < 0 || R < 0 || L > R)
return false;
int center = L + R + 2; // 关键映射公式
int len = R - L + 1;
return p[center] >= len;
}
// 原有的功能:获取最长回文子串
std::string getLongest(const std::string& s) const {
auto it = std::max_element(p.begin(), p.end());
int mx = *it;
int mx_i = std::distance(p.begin(), it);
return s.substr((mx_i - mx) / 2, mx);
}
};
class Solution {
public:
bool checkPartitioning(string s) {
int n = s.size();
s = ' ' + s;
bool dp[n + 1][4];
memset(dp, 0, sizeof dp);
Manacher m(s);
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 3; j++) {
for (int k = 1; k <= i; k++) {
if (m.isPalindrome(k, i)) {
dp[i][j] |= dp[k - 1][j - 1];
}
}
// cout<<dp[i][j]<<' ';
}
// cout<<'\n';
}
return dp[n][3];
}
};
前后缀分解 回文
问两个不重叠的奇回文串,长度乘积的最大值?
就俩串,还不重叠,肯定可以找到一个分界点,一个串在左边,一个在右边,那么可以枚举这个分界点,也就是前后缀分解。这需要预处理前缀,后缀的最长的奇回文串。
难点在这个预处理,因为对于一个回文中心,他不是只有 a [ i + p i ] = 2 p i + 1 a[i+p_i]=2p_i+1 a[i+pi]=2pi+1这一个值的,实际上在半径内的每个位置,都有一个赋值,还都不同。这种情况下,想做前缀和,直接 a [ i + p i ] = 2 p i + 1 a[i+p_i]=2p_i+1 a[i+pi]=2pi+1然后扫一遍前缀,是不对的。
可以利用一个性质:如果 i i i是一个长度 l e n len len的回文串右端点,那么 i − 1 i-1 i−1一定是一个长度 l e n − 2 len-2 len−2的回文串的右端点,所以先做一次正常的扫描前缀,再扫描后缀,执行这里提到的这个更新,计算出的就是最优的前缀。
后缀同理,反过来就行
c
class Solution {
public:
long long maxProduct(string s) {
int n = s.size();
vector<int> p(n);
int r = 0, c = 0;
for (int i = 0; i < n; i++) {
if (i < r) {
p[i] = min(r - i, p[2 * c - i]);
}
while (i + p[i] + 1 < n && i - p[i] - 1 >= 0 && s[i + p[i] + 1] == s[i - p[i] - 1]) {
p[i]++;
}
if (i + p[i] > r) {
r = i + p[i];
c = i;
}
}
vector<int> pre(n), suf(n);
for (int i = 0; i < n; i++) {
int l = i - p[i] , r = i + p[i] ;
pre[r] = max(pre[r], 2 * p[i] +1);
suf[l] = max(suf[l], 2 * p[i] +1);
}
for (int i = 1; i < n; i++) {
pre[i] = max(pre[i], pre[i - 1]);
}
for (int i = n - 2; i >= 0; i--) {
pre[i] = max(pre[i], pre[i + 1] - 2);
}
for (int i = n - 2; i >= 0; i--) {
suf[i] = max(suf[i], suf[i + 1]);
}
for (int i = 1; i < n; i++) {
suf[i] = max(suf[i], suf[i - 1] - 2);
}
long long ans = 0;
for (int i = 0; i < n - 1; i++) {
ans = max(ans, 1ll * pre[i] * suf[i + 1]);
}
return ans;
}
};