字符串(二)Manacher
前言
::::info[AI 使用说明]
本文仅使用 deepseek 进行润色,保证人类贡献远大于 AI 贡献。
::::
::::info[作者的神秘习惯]
本文算法描述中,除特别说明外,数组或字符串的下标都从 那么本篇应该质量会更低。\
本来是想和一些别的串串知识点一起复习的,太懒了所以只复习了 Manacher。
Manacher
我们定义一个字符串
Problem:\ 求在一个满足
|s|\le 10^6 的字符串s ,求出它的最长回文子串长度。
我们考虑中心扩展法。即,我们对于所有的 baddaba,我们从每一个位置出发能够扩展到的回文串是:
- 对于位置
1 ,奇数可以扩展出字符串b,偶数没有。 - 对于位置
2 ,奇数可以扩展出字符串a,偶数没有。 - 对于位置
3 ,奇数可以扩展出字符串d,偶数可以扩展出字符串baddab。 - 对于位置
4 ,奇数可以扩展出字符串d,偶数没有。 - 对于位置
5 ,奇数可以扩展出字符串a,偶数没有。 - 对于位置
6 ,奇数可以扩展出字符串aba,偶数没有。 - 对于位置
7 ,奇数可以扩展出字符串a,偶数没有。
所以最长的回文子串应该是 baddab。从这里我们可以知道,从一个位置出发,奇数至少能扩展出来一个长度为 |
|---|
| 它正确的原因是,一个回文字符串,分别删除开头和结尾的字符依然是回文字符串(因为删去后剩下的字符串依然满足条件)。但是这样的时间复杂度是 $O( |
我们首先考虑统一奇偶字符串,方式就是每两个字符之间都加上分隔符,比较常见的是#。这样所有回文都变成了奇数长度。但是它不会改变原本一个子串是回文串,因为如果是奇数长度的字符串那么对称位置的 # 会互相抵消。而偶数长度,就会变成以 # 为对称中心的串,于是同理了。\ |
| 那么新的串的长度就会变成 |
| 我们考虑维护两个变量 |
| - |
| - |
对于当前位置
- 如果
i<\text{mx} ,那么i 在当前最右回文串的内部,那么找到i 关于\text{id} 的对称点j=2\times \text{id} - i ,因为回文的对称性,以j 为中心的回文在以i 为中心处也会出现,但不能超过\text{mx} 的边界。那么我们先暂时令p_i=\min\{p_j,\text{mx}-i\} 。 - 否则
p_i 初始为1 。
::::info[初始化对吗]{open}
假设当前有一个以
#include<bits/stdc++.h>
using namespace std;
const int N = 2e7 + 2e6 + 5;
int n,m;
string s,t;
int p[N];
int main() {
cin >> s;
n = s.size();
t = "^#";
for(int i = 0;i < n;i ++) {
t += s[i];
t += '#';
}
t += '$';
m = t.size() - 1;
int id = 0,mx = 0;
int ans = 0;
for(int i = 1;i <= m;i ++) {
if(i < mx) p[i] = min(p[2 * id - i],mx - i);
else p[i] = 1;
while(t[i - p[i]] == t[i + p[i]]) p[i] ++;
if(i + p[i] > mx) {
id = i;
mx = i + p[i];
}
ans = max(ans,p[i] - 1);
}
printf("%d",ans);
return 0;
}
:::: ::::success[分讨奇偶]
#include<bits/stdc++.h>
using namespace std;
const int N = 1e7 + 1e6 + 5;
int n;
string s;
int d1[N],d2[N];
int main() {
cin >> s;
n = s.size();
s = " " + s;
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int k;
if(i > r) k = 1;
else k = min(d1[l + r - i],r - i + 1);
while(k <= i - 1 && i + k <= n && s[i - k] == s[i + k]) k ++;
d1[i] = k;
k --;
if(i + k > r) {
l = i - k;
r = i + k;
}
}
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int k;
if(i > r) k = 0;
else k = min(d2[l + r - i + 1],r - i + 1);
while(k <= i - 2 && i + k <= n && s[i - k - 1] == s[i + k]) k ++;
d2[i] = k;
k --;
if(i + k > r) {
l = i - k - 1;
r = i + k;
}
}
int ans = 0;
for(int i = 1;i <= n;i ++) {
ans = max(ans,max(d1[i] * 2 - 1,d2[i] * 2));
}
printf("%d",ans);
return 0;
}
::::
例一:拉拉队排练
题意:\ 求所有的奇数长度的回文子串按照长度降序排序后,前
k 的长度乘积(取模19930726 ),如果总数小于k 输出-1 。
由于插分割符的目的是统一奇偶,而本题只需要考虑奇数所以插不插分割符其实无所谓。那么我们直接跑 Manacher 得到
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 5;
const int Mod = 19930726;
int n,k;
string s;
int d1[N];
int cnt[N];
int ksm(int a,int b) {
int res = 1;
while(b) {
if(b & 1) res = res * a % Mod;
a = a * a % Mod;
b >>= 1;
}
return res;
}
void Manacher() {
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int x;
if(i > r) x = 1;
else x = min(d1[l + r - i],r - i + 1);
while(x <= i - 1 && i + x <= n && s[i - x] == s[i + x]) x ++;
d1[i] = x;
x --;
if(i + x > r) {
l = i - x;
r = i + x;
}
}
}
signed main() {
cin >> n >> k;
cin >> s;
s = " " + s;
Manacher();
for(int i = 1;i <= n;i ++) {
cnt[1] ++;
if(2 * d1[i] + 1 <= n) cnt[2 * d1[i] + 1] --;
}
for(int i = 3;i <= n;i += 2) cnt[i] += cnt[i - 2];
int ans = 1;
int tot = 0;
for(int i = n;i >= 1;i --) {
if(i % 2 == 0) continue;
if(cnt[i] == 0) continue;
int take = min(cnt[i],k - tot);
ans = ans * ksm(i,take) % Mod;
tot += take;
if(tot >= k) break;
}
if(tot < k) puts("-1");
else printf("%lld\n",ans);
return 0;
}
::::
例题二:[POI 2010] ANT-Antisymmetry
题意:\ 我们定义反对称为,0,1 取反之后再将整个串反过来和原串一样的字符串。\ 给定一个 01 字符串,求有多少个子串是反对称的。
Tips:本题为作者很久以前做的题目,字符串下标从
这样我们就可以得到所有位置
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 5;
int n,d[N],ans = 0;
string S;
inline void manacher(){
for(int i = 0,l = 0,r = -1;i < n;i ++) {
int k;
if(i > r) k = 0;
else k = min(d[l + r - i + 1],r - i + 1);
while(k <= i - 1 && k + i < n && S[i - k - 1] != S[i + k]) k ++;
d[i] = k;
k --;
if(i + k > r) {
l = i - k - 1;
r = i + k;
}
}
}
signed main(){
scanf("%lld",&n);
cin >> S;
manacher();
for(int i = 0;i < n;i ++) {
ans += d[i];
}
printf("%lld\n",ans);
return 0;
}
::::
例题三:最长双回文串
题意:\ 求最长的能够分成两个非空回文串的字符串。
我们分别预处理出来以
那么我们枚举所有的分界点
::::success[P4555]
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n;
string s;
int d1[N],d2[N];
int L[N],R[N];
void Manacher() {
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int k;
if(i > r) k = 1;
else k = min(d1[l + r - i],r - i + 1);
while(k <= i - 1 && i + k <= n && s[i - k] == s[i + k]) k ++;
d1[i] = k;
k --;
if(i + k > r) {
l = i - k;
r = i + k;
}
}
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int k;
if(i > r) k = 0;
else k = min(d2[l + r - i + 1],r - i + 1);
while(k <= i - 2 && i + k <= n && s[i - k - 1] == s[i + k]) k ++;
d2[i] = k;
k --;
if(i + k > r) {
l = i - k - 1;
r = i + k;
}
}
}
signed main() {
cin >> s;
n = s.size();
s = " " + s;
Manacher();
for(int i = 1;i <= n;i ++) {
int len = 2 * d1[i] - 1;
int l = i - d1[i] + 1,r = i + d1[i] - 1;
L[l] = max(L[l],len);
R[r] = max(R[r],len);
}
for(int i = 1;i <= n;i ++) {
if(d2[i] > 0) {
int len = 2 * d2[i];
int l = i - d2[i],r = i + d2[i] - 1;
L[l] = max(L[l],len);
R[r] = max(R[r],len);
}
}
for(int i = 2;i <= n;i ++) L[i] = max(L[i],L[i - 1] - 2);
for(int i = n - 1;i >= 1;i --) R[i] = max(R[i],R[i + 1] - 2);
int ans = 0;
for(int i = 1;i < n;i ++) {
if(R[i] > 0 && L[i + 1] > 0)ans = max(ans,R[i] + L[i + 1]);
}
printf("%lld\n",ans);
return 0;
}
::::
例题四:回文匹配
给定两个字符串
s (长度为n )和t (长度为m ),对于s 的每一个长度为奇数的回文子串,统计t 在该回文子串中出现的次数,并求和。
首先我们解决
那么我们使用前缀和优化即可
那么我们对这一坨式子进行简化:
我们设
进一步,我们化简之后还可以拆成两个部分:
那么两个式子都再使用一遍前缀和优化即可。\
注意只需要对 ans 用的是 unsigned int,理论上存在溢出风险,实际实现时建议使用 unsigned long long 或 __int128。
::::success[P6216]
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e6 + 5;
int n,m;
string s,t;
int nxt[N],p[N];
int match[N],sum[N],pre[N];
void gnext() {
nxt[1] = 0;
int j = 0;
for(int i = 2;i <= m;i ++) {
while(j && t[j + 1] != t[i]) j = nxt[j];
if(t[j + 1] == t[i]) j ++;
nxt[i] = j;
}
}
void kmp() {
int j = 0;
for(int i = 1;i <= n;i ++) {
while(j && t[j + 1] != s[i]) j = nxt[j];
if(t[j + 1] == s[i]) j ++;
if(j == m) {
match[i - m + 1] = 1;
j = nxt[j];
}
}
}
void manacher() {
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int x;
if(i > r) x = 1;
else x = min(p[l + r - i],r - i + 1);
while(x <= i - 1 && i + x <= n && s[i - x] == s[i + x]) x ++;
p[i] = x;
x --;
if(i + x > r) {
l = i - x;
r = i + x;
}
}
}
int gp(int idx) {
if(idx < 0) return 0;
if(idx > n) return pre[n];
return pre[idx];
}
signed main() {
cin >> n >> m;
cin >> s >> t;
s = " " + s;
t = " " + t;
s += '$';
t += '$';
gnext();
kmp();
manacher();
for(int i = 1;i <= n;i ++) sum[i] = sum[i - 1] + match[i];
for(int i = 1;i <= n;i ++) pre[i] = pre[i - 1] + sum[i];
int kt = (m + 2) / 2;
unsigned int ans = 0;
for(int i = 1;i <= n;i ++) {
if(p[i] < kt) continue;
ans += gp(i + p[i] - m) - gp(i + kt - m - 1) - (gp(i - kt) - gp(i - p[i] - 1));
}
printf("%u\n", ans);
return 0;
}
::::
例题五:绿绿和串串
题意:\ 给定字符串
S ,判断它的哪些前缀可以通过不断以末尾字符以轴进行翻转最后覆盖整个S 。
它的过程实质上就是以结尾字符为对称中心,把左边的字符复制到右边。容易注意到一次翻转能覆盖到多远,取决于以
设
- 如果以
i 为中心的回文串右端点r 满足r\ge n ,那么一次翻转就能覆盖整个S ,点i 就是答案。 - 否则,如果以
i 为中心的回文串左端点l=1 ,那么翻转一次后可以扩展到r ,只要r 也是答案,i 就是答案。
::::info[P5446]
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6 + 5;
int n,T;
string s;
int d1[N];
bool ok[N];
void Manacher() {
for(int i = 1,l = 1,r = 0;i <= n;i ++) {
int k;
if(i > r) k = 1;
else k = min(d1[l + r - i],r - i + 1);
while(k <= i - 1 && i + k <= n && s[i - k] == s[i + k]) k ++;
d1[i] = k;
k --;
if(i + k > r) {
l = i - k;
r = i + k;
}
}
}
signed main() {
scanf("%d",&T);
while(T --) {
cin >> s;
n = s.size();
s = " " + s;
Manacher();
for(int i = 1;i <= n;i ++) ok[i] = false;
for(int i = n;i >= 1;i --) {
int r = i + d1[i] - 1;
int l = i - d1[i] + 1;
if(r >= n) ok[i] = true;
else if(l == 1 && ok[r]) ok[i] = true;
}
for(int i = 1;i <= n;i ++) {
if(ok[i]) printf("%lld ",i);
}
puts("");
}
return 0;
}
::::
例题六:泪雨 Namid[A]me
题意:\ 给定一个只含小写字母和
? 的长度n 的字符串S 。定义一个子串是泪雨,当且仅当:
- 它是回文串;
- 其中
? 的数量不少于字母的数量。 要求对于所有是泪雨的子串,求出它们中? 的位置下标之和,再对所有泪雨子串的这个和求和。位置下标从1 开始。
直接对于所有的回文串暴力判断是
设回文区间为
由于我们要求下标和,设固定中心和半径分别为
那么就是对所有区间和
因为对称的两个
我们设
::::success[P12977]
#include<bits/stdc++.h>
using namespace std;
const int N = 2e7 + 5;
int n,m,d[N],len[N];
char s[N];
__int128 cnt[N],sum[N];
__int128 ans;
__int128 Manacher() {
__int128 res = 0;
for(int i = 1,r = 0,mid = 0;i <= m;i ++) {
if(r >= i) {
int p = mid * 2 - i;
d[i] = d[p];
cnt[i] = cnt[p];
sum[i] = sum[p];
len[i] = len[p];
while(d[i] > r - i) {
int R = p + d[i];
if(2 * cnt[i] >= len[i] && s[R] != '|') sum[i] -= cnt[i];
if(s[R] == '?') cnt[i] -= 2;
if(s[R] != '|') len[i] -= 2;
d[i] --;
}
} else {
len[i] = (s[i] == '|') ? 0 : 1;
cnt[i] = sum[i] = 0;
if(s[i] == '?') {
cnt[i] = 1;
if(2 * cnt[i] >= len[i]) sum[i] = 1;
}
}
while(s[i - d[i] - 1] == s[i + d[i] + 1]) {
d[i] ++;
if(s[i + d[i]] != '|') {
len[i] += 2;
if(s[i + d[i]] == '?') cnt[i] += 2;
}
if(2 * cnt[i] >= len[i] && s[i + d[i]] != '|') sum[i] += cnt[i];
}
res += (__int128)sum[i] * (__int128)(i + (__int128)1) / (__int128)2;
if(i + d[i] >= r) {
r = i + d[i];
mid = i;
}
}
return res;
}
void print(__int128 x) {
if(x == 0) { putchar('0'); return; }
if(x < 0) { putchar('-'); x = -x; }
char buf[50]; int len = 0;
while(x) { buf[len++] = x % 10 + '0'; x /= 10; }
while(len) putchar(buf[--len]);
}
signed main() {
scanf("%d",&n);
s[0] = '~';
for(int i = 1;i <= n;i ++) {
cin >> s[i * 2 - 1];
s[i * 2] = '|';
}
m = 2 * n;
s[m + 1] = '$';
ans = Manacher();
print(ans);
putchar('\n');
return 0;
}
::::