专栏文章
关于使用字符串哈希求解最长回文子串问题
算法·理论参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @minwnzm8
- 此快照首次捕获于
- 2025/12/02 09:34 3 个月前
- 此快照最后确认于
- 2025/12/02 09:34 3 个月前
前言
这个算法相较于 manacher 的空间与时间效率,是并不优秀的。但是它比 manacher 更易懂,学习门槛很低。
小剧场
老师:很多算法都是以人名命名的,manacher 就是一个,再比如说 myt 发明的算法就可以叫 myt 算法。哎呀,这个名字不太好听哈。(笑)把 y 改成 i 就不错了。(笑)
两小时后,“MIT 哈希”诞生了
两小时后,“MIT 哈希”诞生了
叠甲
可能这个算法在现在已经有人发现了,但作者并未看到相关文章,如有雷同,纯属巧合(真的真的)。
字符串哈希
字符串比较
字符串哈希其实就是把一个字符串映射成一个自然数,若想比较是否一样,只需判断两串映射的值是否一样即可。“前缀”哈希函数通常是如下形式:
“后缀”哈希函数通常是如下形式:
那么我们现在可以判断两个处理过哈希函数的字符串是否一样,那能不能通过某个字符串的前缀哈希函数或后缀哈希函数,去求得子串的哈希函数呢?答案是可以的。
子串比较
下面假设我们处理的前缀哈希函数数组为 。
对于 ,根据定义,有 ,观察到 ,也就是说 ,那么我们就通过预处理的前缀哈希函数数组得到了子串的哈希值,再对哈希值进行比较即可。
对于 ,根据定义,有 ,观察到 ,也就是说 ,那么我们就通过预处理的前缀哈希函数数组得到了子串的哈希值,再对哈希值进行比较即可。
关联题目
题目大意
给定一个字符串,求出最长回文子串长度()。
算法介绍
最朴素的暴力
考虑枚举左右端点 ,使用字符串哈希判断 与 是否相等。很明显,它的时间复杂度是 的,非常慢。
二分优化
那么我们发现对于一个左端点,回文串不满足二分的性质,但是对于一个中间点,二分它向外扩展的长度是完全可行的。但是注意到偶数长度的回文串它的中间点不在某个字符上,所以我们可以在原串的每个字符之间插入其它字符,例如 '#'。时间复杂度为 ,对于一部分的字符串题可以通过,但上题的字符串长度在 量级上故仍无法通过。
“MYT 哈希”
首先,还是给字符之间插入 '#',这样避免了偶数长度的情况。注意到,对于每个中间点的长度 其实我们只关心他们的最大值,即 那么我们可以记录一个变量 ,表示截至目前从中心点扩展的最大长度(并非最长回文子串长度)。那么对于目前枚举到的中心点 我们发现如果 扩展到的长度小于等于 是完全没有贡献的(无法更新答案)。那么我们可以再枚举一个 表示当中间点 向外扩展的长度。如果发现进行这次扩展之后就不是回文子串了,就可以直接停止枚举,否则更新 的值。最后的答案应为 (对于不同的写法这里也可能是 )但是由于我们插入了井号,实际答案应该是 (同样的,不同的写法这里也可能是 )。
关于时间复杂度
有人可能看到这是双重循环便认为时间复杂度为 的,实则不然。观察到内层循环的起始点 是单调上升的,也就是说对于任意的 若它的内层循环的执行次数为 ,则下次循环的最大循环次数也会减少 ,那么内层循环最多被执行 次,故时间复杂度为 (类似于双指针的时间复杂度分析),可以通过本题。
正解代码
CPP#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ll MAXN = 22e6 + 5, base = 499, p = 1e9 + 7;
ll n;
string s;
ll h1[MAXN];
ll h2[MAXN];
int bp[MAXN];
ll mod(ll x) {
return (x % p + p) % p;
}
ll gethash1(ll l, ll r) {
return mod(h1[r] - h1[l - 1] * bp[r - l + 1] % p);
}
ll gethash2( ll l, ll r) {
return mod(h2[r] - h2[l - 1] * bp[r - l + 1] % p);
}
bool chk(ll l, ll r) {
return gethash1(l, r) == gethash2(n - r + 1, n - l + 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
string tmp;
cin >> tmp;
for(char it:tmp)
s.push_back('#'), s.push_back(it);
s.push_back('#');
n = s.size();
s = " " + s;
bp[0] = 1;
for(int i = 1; i <= n; i++) {
h1[i] = (h1[i - 1] * base + s[i]) % p;
h2[i] = (h2[i - 1] * base + s[n - i + 1]) % p;
bp[i] = bp[i - 1] * base % p;
}
ll len = 1;
for(int i = 1; i <= n; i++) {
for(int j = len; i + j - 1 <= n && i - j + 1 >= 1; j++) {
if(!chk(i - j + 1, i + j - 1))
break;
len = max(len, j * 1ll);
}
}
cout << max(len - 1, 1ll);
return 0;
}
“MYT 哈希”拓展
其实这种字符串哈希可以拓展到一些其他题,比如“找一个字符串中循环节长度为 k 的最长子串”,但其实它在这道题上并不好用,有一道好的练习题,我并不想公开出来(以后应该会)。
总结
经测试,这种算法因为哈希取模等原因,较于 manacher 算法的时间有 至 的常数。但是较于二分优化也比较快。
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...