原题链接https://acm.tju.edu.cn/problem/P2040
学校oj好像挂了,题解发不出去,又没有草稿功能,所以先存在这里了。
前言
华为杯时候对字符串不太熟,加上看错题了导致没做出这题,很可惜,苦练几个月,现在已经成为串串大师,回过头来秒一下这题发个题解泄恨。
题意
给定一个长为 \(n\) 的字符串, 求有多少对回文子串满足它们在原串中的位置不重叠。数据范围 \(n \leq 10^6\)
分析
既然是回文串对,那么肯定有一个在左,一个在右。那么先考虑一下对于左边的回文串,能跟他配对的回文串满足左端点大于该串右端点的回文串就都可以匹配上。发现我们只在乎每个回文串的左右端点位置,所以很显然的思路是,如果能够预处理出来这个字符串里,以每个位置作为结尾的回文串个数,和以每个位置作为开头的回文串个数,假设分别是数组 \(wei[N]\) 和数组 \(tou[N]\)。
那么答案显然就是 \(\sum_{i=1}^{n-1} wei[i] \times \sum_{j=i+1}^n tou[i]\)。也就是枚举每个位置作为左边串的结尾,能跟他匹配上的是所有开头大于该位置的所有回文串。问题就转化为求这两个数组,下面有两种方法。
马拉车(Manacher)
处理回文串的经典算法,无脑调用后,可以得到以每个点作为回文中心的最长回文半径。我们发现回文串是有单调性的,也就是求出每个点为中心的最大回文串后,两边同时去掉一个字母,依旧是回文串。所以用马拉车后直接配合差分前缀和,就可以得到这两个数组,过程中要注意一下马拉车添加字符后跟原先串的对应关系,代码如下:
#include <bits/stdc++.h>
// #define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define tii tuple<int, int, int>
#define db double
#define all(a) a.begin(), a.end()
using namespace std;
const int maxn = 2e6 + 10;
const int mod = 998244353;
int d[maxn];
char s[maxn];
int maxpos;
void init(string& str) {
maxpos = str.length() * 2;
s[0] = '#';
for (int i = 0; str[i]; i++) {
s[2 * i + 1] = str[i];
s[2 * i + 2] = '#';
}
}
void manacher(string str) {
init(str);
for (int i = 0, l = 0, r = -1; i <= maxpos; i++) {
int k = (i > r) ? 1 : min(d[l + r - i], r - i);
while (i - k >= 0 && i + k <= maxpos && s[i - k] == s[i + k])
k++; // 调用朴素算法
k--;
d[i] = k;
if (i + k > r) {
r = i + k;
l = i - k;
}
}
}
int wei[maxn], tou[maxn];
void solve() {
int n; cin >> n;
string str; cin >> str;
manacher(str);
for (int i = 0; i <= maxpos; i++) {
if (d[i]) {
if (i & 1) { //奇回文
int pos = i / 2 + 1;
wei[pos]++;
wei[pos + d[i] / 2 + 1]--;
tou[pos + 1]--;
tou[pos - d[i] / 2]++;
} else {
int posl = i / 2; //偶回文串中心偏左
wei[posl + 1]++;
wei[posl + 1 + d[i] / 2]--;
tou[posl + 1]--;
tou[posl - d[i] / 2 + 1]++;
}
}
}
for (int i = 1; i <= n; i++)
tou[i] += tou[i - 1], wei[i] += wei[i - 1];
ll ans = 0, sum = 0;
for (int i = n; i; i--)
sum += tou[i];
for (int i = 1; i < n; i++) {
sum -= tou[i];
ans += wei[i] * sum;
}
cout << ans << "\n";
}
signed main() {
// freopen("1.in", "r", stdin);
// freopen("1.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
// cin >> t;
while (t--) solve();
return 0;
}
回文自动机(PAM)
其实主要是想写这个的,好不容易学了字符串科技怎么能不用呢?使用PAM可以很容易得到以每个点作为结尾的字符串个数,也就是在构建回文自动机过程extend时,lst指针在fail树上的深度,这个在构建的过程中很容易得到。那么想要以每个点作为开头,自然就是反着再建一个回文自动机,就得到所要的这两个数组了。基本上就是PAM的板子题。代码如下:
#include <bits/stdc++.h>
// #define int long long
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int, int>
#define tii tuple<int, int, int>
#define db double
#define all(a) a.begin(), a.end()
using namespace std;
const int maxn = 1e6 + 10;
const int mod = 998244353;
struct PAM {
int tot, last, fail[maxn], c[maxn][27], len[maxn], cnt[maxn];
char s[maxn]; //下标从1开始的字符串
PAM() { tot = last = fail[0] = 1, len[1] = -1;}
int getfail(int x, int i) {
while (s[i - len[x] - 1] != s[i]) x = fail[x];
return x;
}
void extend(char ch, int i) { //插入位置 i 的字符 ch
s[i] = ch;
int x = getfail(last, i), v = ch - 'a';
if (!c[x][v]) {
len[++tot] = len[x] + 2;
int tem = getfail(fail[x], i);
fail[tot] = c[tem][v];
c[x][v] = tot; //必须放最后
cnt[tot] = cnt[fail[tot]] + 1;
}
last = c[x][v];
}
} pam1, pam2;
int wei[maxn];
ll tou[maxn];
void solve() {
int n; cin >> n;
string str; cin >> str;
for (int i = 1; i <= n; i++) {
pam1.extend(str[i - 1], i);
wei[i] = pam1.cnt[pam1.last];
}
for (int i = n; i; i--) {
pam2.extend(str[i - 1], n - i + 1);
tou[i] = pam2.cnt[pam2.last];
tou[i] += tou[i + 1];
}
ll ans = 0;
for (int i = 1; i < n; i++)
ans += 1ll * wei[i] * tou[i + 1];
cout << ans << "\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t = 1;
// cin >> t;
while (t--)
solve();
return 0;
}
标签:int,题解,long,2024,maxn,天津大学,str,回文,define
From: https://www.cnblogs.com/TJUHuangTao/p/18521337