P10170 [DTCPC 2024] 小方和小立方 题解
littlesnake · · 题解
前言
看别人枚举字串状态,我表示无语,赛时还以为是一道很难的题呢,写了马拉车,调了好久才调出来。我保证我的做法绝对新颖,时间复杂度和空间复杂度都是
思路
看到题目,不难想到马拉车。但是又多了一个条件:
每个字符的出现次数不超过
2 。
但这样也很简单,在向外延伸时多判断一个条件即可。
算法详解
算法由来
在求解最长回文子串的问题时,一般的思路是以当前字符为中心,向其左右两边扩展寻找回文,但是这种解法的时间复杂度是
说人话就是把时间复杂度降低为
预处理
回文字符串以其长度来分,可以分为奇回文(其长度为奇数)、偶回文(其长度为偶数),一般情况下需要分两种情况来寻找回文,马拉车算法为了简化这一步,对原始字符串进行了处理,在每一个字符的左右两边都加上特殊字符(肯定不存在于原字符串中的字符),让字符串变成一个奇回文。例如:
-
原字符串:
abba,长度为4 。 -
预处理后:
XaXbXbXaX,长度为9 。 -
原字符串:
aba,长度为3 。 -
预处理后:
XaXbXaX,长度为7 。
注:X 为特殊字符,以后使用美元符号。
做法
在预处理后,先看是否超出边界
如果扩展最长半径已经超出边界,就更新边界的值。
最后打擂即可。
注意最后输出的是
模板
下面给出求最长回文串长度的例题和代码。
题目:P3805 【模板】manacher。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 11000002;
// n 代表一开始字符串的长度
// m 代表添加特殊符号后的字符串的长度
// p[i] 代表往一边能够扩展的字符数即为从 i 开始的最大回文半径
int n, m, p[N * 2 + 2];
// s[] 代表一开始的字符串
// t[] 代表添加特殊符号后的字符串
char s[N + 2], t[N * 2 + 3];
// 马拉车
inline void manacher() {
n = strlen(s + 1);
m = 0;
// 添加特殊符号
t[++m] = '$';
for(int i = 1; i <= n; i++) {
t[++m] = s[i]; t[++m] = '$';
}
int M = 0, R = 0;
for(int i = 1; i <= m; i++) {
// k 是 i 对于 M 的对称点
int k = M * 2 - i;
if(i > R) p[i] = 1;
else p[i] = min(p[k], R - i + 1);
// 暴力向外扩展
while(i - p[i] > 0 && i + p[i] <= m && t[i - p[i]] == t[i + p[i]])
++p[i];
if(i + p[i] - 1 > R) M = i, R = i + p[i] - 1;
}
}
int main() {
// 从 1 开始读入
scanf("%s", s + 1);
manacher();
int ans = 0;
for(int i = 1; i <= m; i++) ans = max(ans, p[i]);
printf("%d", ans - 1);
return 0;
}
AC 记录:link
小结
小结
马拉车算法将求解最长回文子串的时间复杂度降低到了
这道题我感觉还可以枚举形态做,但是用马拉车进行求解复杂度是惊人的
个人认为这种算法应该叫条件马拉车,在朴素马拉车的基础上增加了一个条件。
本题代码
先放 AC 记录啦~
AC 记录:link
#include <bits/stdc++.h>
#define N 50010
using namespace std;
// n 代表一开始字符串的长度
// m 代表添加特殊符号后的字符串的长度
// p[i] 代表往一边能够扩展的字符数即为从 i 开始的最大回文半径
// cnt[] 代表每个字母出现的次数
int n, m, p[N * 2 + 2], cnt[30];
// s[] 代表一开始的字符串
// t[] 代表添加特殊符号后的字符串
char s[N + 2], t[N * 2 + 3];
// 马拉车
inline void manacher() {
n = strlen(s + 1);
m = 0;
// 添加特殊符号
t[++m] = '$';
for(int i = 1; i <= n; i++) {
t[++m] = s[i];
t[++m] = '$';
}
int M = 0, R = 0;
for(int i = 1; i <= m; i++) {
// 每次需要清空数组
memset(cnt, 0, sizeof(cnt));
// k 是 i 对于 M 的对称点
int k = M * 2 - i;
if(i > R) {
p[i] = 1;
// 本身记一次数
if(t[i] != '$') {
cnt[ t[i] - 'a' ] = 1;
}
}
else {
p[i] = min(p[k], R - i + 1);
// 将之前的数据搬运过来
for(int j = i - p[i] + 1; j <= i + p[i] - 1; j++) {
if(t[j] != '$') cnt[ t[j] - 'a' ]++;
}
}
// 暴力向外扩展,记得多加了一个条件
while(i - p[i] > 0 && i + p[i] <= m && (t[i - p[i]] == t[i + p[i]])) {
if(t[i - p[i]] == '$') {
++p[i];
}else {
// 注意要先判断,再让 p[i] 自增,否则 p[i] 的值会改变
cnt[ t[i - p[i]] - 'a' ] += 2;
if(cnt[ t[i - p[i]] - 'a' ] <= 2){
++p[i];
}else break; // 否则就不可以继续延伸了,跳出循环
}
}
// 更新边界
if(i + p[i] - 1 > R) M = i, R = i + p[i] - 1;
}
}
int main() {
// 从 1 开始读入
scanf("%s", s + 1);
manacher();
int ans = 0;
for(int i = 1; i <= m; i++) {
ans += p[i] / 2;
// 直接除以二下取整得到半径
}
printf("%d", ans);
return 0;
}