字符串哈希
字符串哈希
字符串哈希就是将一个字符串映射为P进制的整数.
- 将一个字符串映射成一个P进制整数
对于一个长度为n的字符串s,这样定义一个Hash函数:\(h(s)=\sum_{i=1}^{n}s[i] \times p^{n-i}(mod M)\)
例如,字符串,abc,其哈希值为\(ap^2 + bp^1 + c\) - 如果两个字符串不一样,哈希值却一样,这种现象称为哈希碰撞
- 解决哈希碰撞的方法:
巧妙地设置P和M的值,保证P与M互质.
P通常取质数131或者13331
M通常取大整数\(2^{64}\),把哈希函数值的数据类型定义为UUL(unsigned long long
),超过则自动移除,等价于取模
解题步骤
- 求一个字符串的哈希值相当于求前缀和,求一个字符串的子串哈希值相当于求区间和
- 递推公式\(h[i] = h[i - 1] \times P + s[i]\)
- 求子串的哈希值也就是求区间和:\(h[l,r] = h[r] - h[l - 1] \times p^{r - l + 1}\)
- 计算前缀和的时间复杂度为\(O(n)\), 查询子串哈希值的时间复杂度为\(O(1)\)
注意:本文图文并茂
将提供以下图文链接供大家理解:
图文链接:
飞书图解链接🎉🎉🎉
密码:6&482J63
练习
题目一
Luogu P3370 【模板】字符串哈希
解法一:😤
AC代码,展开查看
#include<bits/stdc++.h>
using namespace std;
int main(){
unordered_set<string> set;
int n;
cin >> n;
while(n -- ){
string s;
cin >> s;
set.insert(s);
}
cout << set.size();
return 0;
}
解法二:
AC代码,展开查看
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1505, P = 131;
ULL h[N];
char s[N];
ULL calc(char* s, int len){
for(int i = 1; i <= len; i ++ ){
h[i] = h[i - 1] * P + s[i];
}
return h[len];
}
int main(){
unordered_set<ULL> set;
int n;
scanf("%d", &n);
while(n -- ){
scanf("%s", s);
set.insert(calc(s, strlen(s)));
}
printf("%d", set.size());
return 0;
}
题目二
AC代码,展开查看
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 2e4 + 10, P = 131;
char s[N];
ULL p[N], h[N];
int n, m; // 第一,二串字符串的长度
// 求子串哈希值
ULL get(int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
// check 函数,求是否是公共子串
bool check(int x){
unordered_set<ULL> hash;
for(int i = 1; i <= n - x + 1; i ++ ){
hash.insert(get(i, i + x - 1));
}
// 将上面的 n - x + 1 中的 n 换为 n + m 即可
for(int i = n + 1; i <= n + m - x + 1; i ++ ){
if(hash.count(get(i, i + x - 1))) return true;
}
// 否则返回false
return false;
}
// 二分查找,查找最大公共子串 0 <= x <= min(n, m)
int find(){
int l = -1, r = min(n, m) + 1; // 可行区在左边
while(l + 1 < r){
int mid = l + r >> 1;
if(check(mid)) l = mid; // 如果是公共子串放大
else r = mid;
}
return l;
}
int main(){
scanf("%s", s + 1);
n = strlen(s + 1);
scanf("%s", s + n + 1);
m = strlen(s + n + 1); // 将第一,二串字符串合并
// 求字符串哈希
p[0] = 1;
for(int i = 1; i <= n + m; i ++ ){
p[i] = p[i - 1] * P;
char c = s[i];
if(c >= '0' && c <= '9'){
if(i <= n) c = '#';
else c = '$';
}
h[i] = h[i - 1] * P + c;
}
// 因为要求最大公共子串,最大化,是否型
printf("%d", find());
return 0;
}
题目三
Acwing 3875. 最长公共子串
使用与上题一致的解法.
AC代码,展开查看
// 这道题不能两两求最长公共子串, 因为两两最长公共子串可能不同,
// 也不能先求两个字符串的最长公共子串,将求得的最长公共子串与剩下的字符串去求最长公共子串
// ,因为两两最长公共子串可能不同
// 使用字符串哈希 + 二分
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 2e3 + 10, P = 131;
ULL p[N], h[N];
int n, len = N;
vector<string> strs;
ULL get(int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
bool check(int x){
unordered_map<ULL, int> m;
for(int i = 0; i < n; i ++ ){
// 预处理字符串的哈希值
for(int j = 1; j <= strs[i].size(); j ++ ){
h[j] = h[j - 1] * P + strs[i][j - 1];
}
unordered_set<ULL> s;
for(int j = 1; j + x - 1 <= strs[i].size(); j ++ ){
s.insert(get(j, j + x - 1));
}
for(auto item : s){
m[item] ++ ;
}
}
for(auto &[k, v] : m){
if(v == n) return true;
}
return false;
}
int main(){
cin >> n;
for(int i = 0; i < n; i ++ ){
string s;
cin >> s;
len = min(len, (int)s.size());
strs.push_back(s);
}
// 预处理p数组
p[0] = 1;
for(int i = 1; i <= N; i ++ ) p[i] = p[i - 1] * P;
// 二分, 0 <= 最长公共子串的长度 <= len, 所以 0 <= x <= len;
int l = -1, r = len + 1;
while(l + 1 < r){
int mid = l + r >> 1;
if(check(mid)) l = mid; // 放大x
else r = mid; // 可行区在左边
}
printf("%d", l);
return 0;
}
本文参考自【董晓算法的个人空间-哔哩哔哩】
海纳百川,有容乃大!如果文章有什么不足之处,还请大神们评论区留言指出,我在此表达感谢🥰!若大家喜欢我的作品,欢迎点赞、收藏、打赏🎉🎉🎉!