Manacher
Manacher
是找出字符串中回文子串很有效的算法
具体而言,是在暴力的基础上一步步优化
首先,最暴力的是 枚举左右端点,再 判断
但可以直接枚举中间点,优化到
此时出现了个问题:长度为奇数的回文串中点在中间字母,
但长度为偶数的回文串中间点在空隙处!
所以,我们把空隙处插入'#'的特殊字符(保证与其它字符不同),就更方便枚举空隙(在最前面和最后面也加入)
但 的复杂度也不理想
# | a | # | b | # | b | # | a | # | a | # | b | # | b | # |
---|
这时我们要利用回文串本身的对称性
看第3个#号,易知以它为中心,两段字符对称最长可延伸至开头和第5个#(,此时已找出的回文串最大的右边界)处(也就是最长回文子串)
向后移一个看b(记作 ),此时以第3个 # 为对称点,它与第1个b完全对称,记为 , 中存了以 为对称中心的极长回文串长度 的值
那么当 时,
-
若 ( 关于 对称点),即以 为中心点回文子串超出 范围,则 初始为 ,剩下只能暴力枚举了
-
若 ,则以 为中心点回文子串与以 为中心点回文子串一模一样(对称),即
当 时,只能枚举了
分析:
时间复杂度:
循环好理解,但暴力查询为何线性?
因为假如 比 小,根本不会执行 ,不用枚举
但即便 比 大,由于 为已找出的回文串最大的右边界,因此递增,自然此时 要比 大也应递增
应用:
1. 重载比较方式
P3501 [POI2010]ANT-Antisymmetry
:
直接把比较回文串的相等改为异或为 则相等即可
注意此时 # 为通配符
代码:
for(int i = 1; i <= n * 2; i += 2)
{
p[i] = (mx > i) ? min(mx - i + 1, p[id * 2 - i]) : 1;
while(i - p[i] >= 1 && i + p[i] <= n * 2 && (t[i - p[i]] != t[i + p[i]] || (t[i - p[i]] == '#')))
p[i]++;
sum += p[i] >> 1;
if(i + p[i] - 1 > mx) mx = i + p[i] - 1, id = i;
}
2. 找所有回文串(个数等)
:
题目只让我们求串长为奇数的串,且要找前 大的串长之积
比较显然的想法是开数组 对每个长度记录个数,但个数太多直接算不行
注意 求的是极长回文子串,也就是一个长为 的包含长为 的回文串
那除去下标为偶数位的,相当于覆盖了数组的前缀!
因此,从大到小枚举奇数位到位置 , 为 的和,那长为 的回文串个数为
或者相当于极长长度为 的串把 ,用差分维护
代码:
for(int i = 1; i <= n * 2 + 1; i++)
{
p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
while(t[i - p[i]] == t[i + p[i]]) p[i]++;
if(i + p[i] > mx) mx = i + p[i], id = i;
if(i % 2 == 0) tong[1]++, tong[p[i] / 2 + 1]--;
}
for(int i = 1; i <= (n + 1) / 2 + 1; i++)
{
tong[i] += tong[i - 1];
sum += tong[i];
if(tong[i]) maxx = max(maxx, (ll)i);
}
if(sum < k) printf("-1");
else
{
ll noww = k;
for(int i = maxx; i > 0; i--)
{
if(noww - tong[i] < 0 && noww > 0)
{
ans = ans * qmi((ll)(i * 2 - 1), noww) % mod;
break;
}
else if(noww - tong[i] < 0 && noww <= 0) break;
ll lsh = qmi((ll)(i * 2 - 1), tong[i]);
if(tong[i]) ans = ans * lsh % mod;
noww -= tong[i];
}
printf("%lld", ans);
}
3. 进行 dp
:
注意到拼接时两个串不一定都是极长的,因此 后得额外进行 dp
因为中间没有重复字符,所以想枚举中间的端点,则端点一定为 #
设 表示以 为左端点点的极长回文串, 为以 为右端点的极长回文串
在 时对极长的串进行预处理,
然后每向右移两位 ,每向左移两位
代码:
for(int i = 1; i <= n * 2; i++)
{
p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
while(t[i - p[i]] == t[i + p[i]]) p[i]++;
if(i + p[i] > mx) mx = i + p[i], id = i;
l[i - p[i] + 1] = max(l[i - p[i] + 1], p[i] - 1);
r[i + p[i] - 1] = max(r[i + p[i] - 1], p[i] - 1);
}
for(int i = 1; i <= n * 2; i += 2) l[i] = max(l[i], l[i - 2] - 2);
for(int i = n * 2; i > 0; i -= 2) r[i] = max(r[i], r[i + 2] - 2);
for(int i = 1; i <= n * 2; i += 2)
if(r[i] && l[i]) ans = max(ans, l[i] + r[i]);
4. P6216 回文匹配
先求出那些左端点后面可以接上 ,做前缀和
对 跑 manacher
对于一个回文中心有极长回文串 ,那么 中的左端点就能产生贡献
前缀和相减就是
但是还有 ,也需要计算
发现加的前缀和是一个区间,减的前缀和也是一个区间
再次前缀和,注意边界的计算
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m >> (s + 1) >> (t + 1);
lens = strlen(s + 1), lent = strlen(t + 1);
s[0] = '~', s[lens + 1] = '#', pw[0] = 1;
for(ui i = 1; i <= lens; ++i)
hsh[i] = hsh[i - 1] * P + s[i], pw[i] = pw[i - 1] * P;
for(ui i = 1; i <= lent; ++i) hah = hah * P + t[i];
for(ui i = 1; i + lent - 1 <= lens; ++i)
if(gethsh(i, i + lent - 1) == hah) ++num[i];
for(ui i = 1; i <= lens; ++i) tot[i] = tot[i - 1] + num[i];
for(ui i = 1; i <= lens; ++i) sum[i] = sum[i - 1] + tot[i];
for(ui i = 1; i <= lens; ++i)
{
p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
while(s[i - p[i]] == s[i + p[i]]) ++p[i];
if(i + p[i] > mx) mx = i + p[i], id = i;
if(i + p[i] - 1 - (i - p[i] + 1) + 1 < lent || i <= lent / 2) continue;
ui r = i + (lent / 2), l = i - (lent / 2);
if(i - p[i]) ans += sum[i + p[i] - lent] - sum[r - lent] - (sum[l - 1] - sum[i - p[i] - 1]);
else ans += sum[i + p[i] - lent] - sum[r - lent] - (sum[l - 1] - sum[i - p[i]]);
}
cout << ans;
return 0;
}
5. 在扩展时计算答案
首先应枚举中间的断点,因为整个串也是回文串,在这个串中找答案
关键在快速枚举左右的中心
在 被更新时,可以枚举串的右端点,则中间和右端点的中点则是右半部分的中心
关于中间对称的位置应该是左半部分的中心,判断它是否满足条件
Q1:为什么这样不会漏掉答案呢?
因为如果没发生拓展,则在另一边对称的有完全相同的回文串,没必要枚举了
Q2:为什么时间复杂度是 ?
因为枚举端点是 ,而下一轮 即更新为 ,原理同 manacher 复杂度分析
所以,一个串中有多少个本质不同的回文串?
个!
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010, inf = 1e9;
int n, p[N], id, mx, ans, q[N], hh = 1, tt, tree[N << 2];
char t[N], s[N];
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> (t + 1);
s[0] = '~', s[n * 2 + 1] = '#', s[n * 2 + 2] = '$';
for(int i = 1; i <= n; ++i) s[i * 2] = t[i], s[i * 2 - 1] = '#';
for(int i = 1; i <= n * 2; ++i)
{
p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
while(s[i - p[i]] == s[i + p[i]]) ++p[i];
if(i + p[i] > mx)
{
if(i & 1)
for(int j = max(i + 4, mx); j < i + p[i]; ++j)
if((j - i) % 4 == 0 && p[i * 2 - (i + j) / 2] + i * 2 - (i + j) / 2 >= i)
ans = max(ans, j - i);
mx = i + p[i], id = i;
}
}
cout << ans;
return 0;
}
6. P5446 [THUPC2018] 绿绿和串串
发现复制的本质是把串变成以末尾字符为中心的回文串
然后原串一定是 的前缀
对 跑 manacher
如果只复制一次,那么可以通过判断以当前点为中心回文的部分能否覆盖到末尾判断
复制多次看起来有点麻烦
但如果复制一次,得到的串可以再经过一系列复制得到 ,就判断这个结尾可以
倒着处理,类似 DP 的思想
int main()
{
ios::sync_with_stdio(false), cin.tie(0);
cin >> T;
while(T--)
{
cin >> s, n = s.length();
t.clear(); t.pb('~'), t += s;
t.pb('$'), mx = id = las = 0;
for(int i = 1; i <= n; ++i)
{
book[i] = 0, p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
while(t[i - p[i]] == t[i + p[i]]) ++p[i];
if(i + p[i] > mx) mx = i + p[i], id = i;
}
for(int i = n; i > n / 2; --i)
if(i + p[i] - 1 >= n) book[i] = 1, las = i;
for(int i = n / 2; i > 0; --i)
if(i == p[i])
{
if(book[i + p[i] - 1]) las = i, book[i] = 1;
}
for(int i = 1; i <= n; ++i)
if(book[i]) print(i), putchar(' ');
putchar('\n');
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现