F - ABCBA
原题链接
题意简述
给定一个字符串 S S S,用它构造一个回文串 a n s ans ans,满足 S S S 是 a n s ans ans 的前缀且 a n s ans ans 回文,在此基础上还要使得 a n s ans ans 最短。
S . s i z e ( ) ∈ [ 1 , 500000 ] S.size() \in [1,500000] S.size()∈[1,500000]
思路
这个数据范围基本上只能用线性算法了,又是回文串,不难想到前段时间刚学的 M a n a c h e r Manacher Manacher
先考虑最坏情况下答案是什么?
例如, S = A B C B S = ABCB S=ABCB
很容易想到 a n s = A B C B B C B A ans = ABCBBCBA ans=ABCBBCBA,即不用思考,直接将 S S S 倒序后接在后面,这样必然满足 a n s ans ans 回文且 S S S 是它的前缀
但它显然不最短。
如何使得 a n s ans ans 更短呢?
可以发现,按上述方法构造的 a n s ans ans 的中间一部分没有存在的意义。
什么意思?
类似开心消消乐的思想,中间一部分回文串我们可以把它消掉
即 B C B B C B BCBBCB BCBBCB 合并成 B C B BCB BCB,那么新的 a n s ans ans 就是 A B C B A ABCBA ABCBA
哎?同样满足 S S S 是 a n s ans ans 的前缀、 a n s ans ans 回文,并且 a n s ans ans 更短,这就是我们想要的答案啊
进一步思考,容易得出一个结论:
如果在 S S S 中,存在一个子串 t t t , t t t 的最右端延伸到 S S S 的最右端,且 t t t 回文,那么在构造的 a n s ans ans 中, t t t 这部分由于自己已经回文,不再需要复制,直接在 t t t 后面续上不在 t t t 内的那部分子串即可。
比如, S = A B C B S = ABCB S=ABCB , B C B BCB BCB 本身已经回文,那我没必要把它再复制一遍,直接在它后面续上前面的 A A A 即可。
再比如, S = T R E E S = TREE S=TREE , E E EE EE 本身已经回文,那我也没必要把它复制一遍,在它后面续上前面的 T R TR TR 的倒序就可以了,答案为 T R E E R T TREERT TREERT 。(为什么是倒序?很简单, a n s ans ans 要回文)
那么具体如何实现呢?
再想想 M a n a c h e r Manacher Manacher 算法,可以计算出以每个 i i i 为对称中心的最长回文串长度,这不正好符合我们的要求吗?
对于当前考虑到的 i i i , l e n len len 是 i i i 为对称中心向两周延伸的最大回文串长度,注意,这里的串并非原串,而是在每两个字符间加入占位符的串。
如果这个回文串的边界可以到达最右端,意味着在构造的字符串中,这段以 i i i 为中心的回文串可以被利用,而 i i i 前面一段的串不回文,就将其倒序,接在 i i i 后面,就是答案。
草稿纸模拟发现,在新串中,该回文串的起终点是 i − l e n + 1 i - len + 1 i−len+1 , i + l e n − 1 i + len - 1 i+len−1 且回文串两端一定是占位符。
再用草稿纸模拟发现,这段回文串在原串中对应的起点为 ( i − l e n + 1 ) / 2 (i -len + 1) / 2 (i−len+1)/2,终点就是原串最右端。
把这一段没有包含进来的前缀截出来,倒序,续在这段后面,就是答案。
其实这个代码细节很多,昨晚赛时没写出来,今天也调了一个小时才搞出来。
话不多说,看代码
代码实现
#include <bits/stdc++.h>
using namespace std;
const int N = 500100;
int p[N << 2];
string s;
string ans;
string init(string str){
string rs;
int n = str.size() * 2 + 1;
for(int i = 0,j = 0;i < n;i++)
rs += (!(i & 1) ? '#' : str[j++]);
return rs;
}
void manacher(string str){
int len,c,r,n = str.size();
c = r = 0;//c为对称中心,r为回文(对称)半径
for(int i = 0;i < n;i++){
len = r > i ? min(p[2 * c - i],r - i) : 1;
/*
r > i即为覆盖到i
p[2*c-i]即为对称点处的回文半径
r-i则对应第二种情况,即对称点的回文半径超出c的回文半径,此时i取到右边界r即可
*/
//朴素算法扩展
while(i + len < n && i - len >= 0 && str[i + len] == str[i - len]) len++;
if(i + len > r){
//回文半径向右延伸
r = i + len;
c = i;
}
// int t_len = len >> 1;
// if(!(i % 2)) t_len++;
// printf("以%d为对称中心的回文串长度为%d,下标为[%d,%d]\n",i,len,i - len + 1,i + len - 1);
// cout << str.substr(i - len + 1,len * 2 - 1) << endl;
// printf("其在原串中的对应串为");
// cout << s.substr((i - len + 1) >> 1,t_len) << endl;
if(i + len == n){
// printf("坐标%d处回文串覆盖到边界",i);
// printf("\n 不回文部分是");
string t = s.substr(0,(i - len + 1) >> 1);
// cout << t;
string tmp = t;
tmp += s.substr((i - len + 1) >> 1);
// printf("\n 回文部分是");
// cout << s.substr((i - len + 1) >> 1,t_len);
reverse(t.begin(),t.end());
tmp += t;
// printf("\n 最终串是");
// cout << tmp << endl;
// if(ans.empty() || tmp.size() < ans.size())
ans = tmp;
return ;
}
p[i] = len;
}
}
int main(){
cin >> s;
manacher(init(s));
cout << ans << endl;
return 0;
}
注意
几个注意点:
1.在原串中,回文串要截到末尾,不需要 l e n ÷ 2 len \div 2 len÷2
2.我一开始把每个 i + l e n = = n i + len == n i+len==n 的答案都算了一遍,可以看到代码中还有注释的部分
if(ans.empty() || tmp.size() < ans.size())
实际上,很好想的一点,当第一次满足 i + l e n = = n i + len == n i+len==n 的时候已经是最优解,因为 i + l e n i + len i+len 单调不减,越往后,回文子串长度必然越短,必然不是最优解,这样的话就不会超时。
3.一定要多输出调试,一些复杂下标处理不好的时候就多输出看看结果,才能更快发现问题,空想只会浪费时间。
注释删了,整理一下,代码还是挺简洁的
#include <bits/stdc++.h>
using namespace std;
const int N = 500100;
int p[N << 2];
string s;
string ans;
string init(string str){
string rs;
int n = str.size() * 2 + 1;
for(int i = 0,j = 0;i < n;i++)
rs += (!(i & 1) ? '#' : str[j++]);
return rs;
}
void manacher(string str){
int len,c,r,n = str.size();
c = r = 0;//c为对称中心,r为回文(对称)半径
for(int i = 0;i < n;i++){
len = r > i ? min(p[2 * c - i],r - i) : 1;
while(i + len < n && i - len >= 0 && str[i + len] == str[i - len]) len++;
if(i + len > r){
r = i + len;
c = i;
}
if(i + len == n){
string t = s.substr(0,(i - len + 1) >> 1);
ans = t;
ans += s.substr((i - len + 1) >> 1);
reverse(t.begin(),t.end());
ans += t;
return ;
}
p[i] = len;
}
}
int main(){
cin >> s;
manacher(init(s));
cout << ans << endl;
return 0;
}
AI题解
附上AI给出的形式化题解,供参考
题解:F - ABCBA
问题分析
给定字符串 S S S,构造最短的回文串 a n s ans ans,满足:
- S S S 是 a n s ans ans 的前缀。
- a n s ans ans 是回文串。
关键观察:若 S S S 中存在一个后缀回文子串,则只需在该子串后添加其前缀的反转即可构造最短 a n s ans ans。例如, S = A B C B S=ABCB S=ABCB 的最长后缀回文为 B C B BCB BCB,构造 a n s = A B C B A ans=ABCBA ans=ABCBA。
算法思路
- 寻找最长后缀回文子串:利用 Manacher 算法高效找到以每个位置为中心的最长回文子串,并记录能覆盖到 S S S 末尾的最长回文子串。
- 构造答案:截取该回文子串的前缀部分,反转后追加到末尾,形成最短回文。
算法步骤
- 预处理字符串:在
S
S
S 的字符间插入占位符(如
#),处理奇偶长度回文。 - Manacher 算法:计算每个中心的最长回文半径。
- 确定最长后缀回文:遍历所有中心,找到第一个覆盖到预处理串末尾的回文子串。
- 构造答案:将原字符串分为前缀和非回文部分,前缀反转后追加到回文部分后。
复杂度分析
- 时间复杂度: O ( ∣ S ∣ ) O(|S|) O(∣S∣)。Manacher 算法为线性复杂度。
- 空间复杂度: O ( ∣ S ∣ ) O(|S|) O(∣S∣)。预处理字符串和辅助数组。
正确性证明
Manacher 算法确保找到所有可能的回文子串。第一个覆盖末尾的回文子串对应原字符串的最长后缀回文,此时添加最短的反转前缀,保证 a n s ans ans 最短且回文。
代码实现
#include <bits/stdc++.h>
using namespace std;
string solve(string s) {
string t = "#";
for (char c : s) t += c, t += '#';
int n = t.size(), c = 0, r = 0, max_len = 0, best_center = 0;
vector<int> p(n, 0);
for (int i = 0; i < n; ++i) {
int len = (i < r) ? min(p[2*c - i], r - i) : 1;
while (i - len >= 0 && i + len < n && t[i - len] == t[i + len]) ++len;
p[i] = len;
if (i + len > r) c = i, r = i + len;
if (i + len == n && len > max_len) { // 覆盖末尾的最长回文
max_len = len;
best_center = i;
}
}
int start = (best_center - max_len + 1) / 2;
string prefix = s.substr(0, start);
string ans = s.substr(start);
reverse(prefix.begin(), prefix.end());
return s + prefix;
}
int main() {
string s; cin >> s;
cout << solve(s) << endl;
}
代码解释
- 预处理:插入
#构造新字符串t。 - Manacher 算法:计算每个中心的最长回文半径
p[i]。 - 确定最长后缀回文:当回文覆盖到末尾时,记录其中心和长度。
- 构造答案:截取原字符串的回文部分,反转前缀后拼接。
此实现简洁高效,直接利用 Manacher 特性找到最优解,保证线性时间复杂度。
461

被折叠的 条评论
为什么被折叠?



