AT_abc398_f【题解】

F - ABCBA

原题链接

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 ilen+1 i + l e n − 1 i + len - 1 i+len1 且回文串两端一定是占位符。

再用草稿纸模拟发现,这段回文串在原串中对应的起点为 ( i − l e n + 1 ) / 2 (i -len + 1) / 2 (ilen+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,满足:

  1. S S S a n s ans ans 的前缀。
  2. 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

算法思路
  1. 寻找最长后缀回文子串:利用 Manacher 算法高效找到以每个位置为中心的最长回文子串,并记录能覆盖到 S S S 末尾的最长回文子串。
  2. 构造答案:截取该回文子串的前缀部分,反转后追加到末尾,形成最短回文。
算法步骤
  1. 预处理字符串:在 S S S 的字符间插入占位符(如 #),处理奇偶长度回文。
  2. Manacher 算法:计算每个中心的最长回文半径。
  3. 确定最长后缀回文:遍历所有中心,找到第一个覆盖到预处理串末尾的回文子串。
  4. 构造答案:将原字符串分为前缀和非回文部分,前缀反转后追加到回文部分后。
复杂度分析
  • 时间复杂度 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;
}
代码解释
  1. 预处理:插入 # 构造新字符串 t
  2. Manacher 算法:计算每个中心的最长回文半径 p[i]
  3. 确定最长后缀回文:当回文覆盖到末尾时,记录其中心和长度。
  4. 构造答案:截取原字符串的回文部分,反转前缀后拼接。

此实现简洁高效,直接利用 Manacher 特性找到最优解,保证线性时间复杂度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值