【数据结构】Hash 学习笔记
Hash 表
Hash 表又称散列表,哈希表,其核心思想为映射。通常用一个整数来表示某种复杂信息。
字符串 Hash
下面介绍的方法可以将一个任意长度的字符串映射为一个非负整数:
取两个固定值 \(P\) 和 \(M\),把字符串看作 \(P\) 进制数(每一位的值为 char 类型自动转换值即可),将其转化为十进制后对 \(M\) 取模,就可以得到一个非负整数。当 \(M\) 足够大而且 \(P\) 选择得当时,哈希冲突概率基本为零。
下面是字符串哈希的代码实现:
洛谷 P3370 【模板】字符串哈希
参考代码:
#include <bits/stdc++.h>
using namespace std;
unsigned get_hash(string& s) {
unsigned ret = 0;
for (char& c : s) {
ret = ((ret * 0x66ccff) + c);
// 取 P = 0x66ccff, M = unsigned int 最大值
}
return ret;
}
unsigned a[10005], n, ans;
signed main() {
ios::sync_with_stdio(0);
#ifndef ONLINE_JUDGE
clock_t t0 = clock();
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.
cin >> n;
for (int i = 1;i <= n;i++) {
string s;
cin >> s;
a[i] = get_hash(s);
}
sort(a + 1, a + 1 + n);
for (int i = 1;i <= n;i++) {
if (i == 1 || a[i] != a[i - 1]) ans++;
} // 判断有多少不同的 hash 值
cout << ans << endl;
#ifndef ONLINE_JUDGE
cerr << "Time used:" << clock() - t0 << "ms" << endl;
#endif
return 0;
}
前缀 hash 与区间 hash
记字符串 \(S\) 的 hash 值为 \(H(S)\),在其末尾添加一个字符 \(c\),则 hash 值会变为 \(H(S + c) = ((H(S) << 1) + c) \mod M\),其中 \(\mod M\) 是对 \(M\) 取余运算,\(<<\) 是在 \(P\) 进制下的左移运算,\(x << i\) 对应数学运算中的 \(x \times P^i\)。
在 \(S\) 末尾添加一个字符串 \(T\),hash 值则会变为 \(H(S+T)=(H(S)<<(len(T))+H(T))\)
因此,我们算出 \(S\) 的前缀哈希值 \(S[1,i]=F(i)=(F(i-1)<<1)<<+S[i]\),然后就可以在 \(O(1)\) 时间内求出某区间内的哈希值 \(H(S[l,r])=F(r)-(F(l-1)<<(r-l+1))\),
另外,分享一个技巧,这样读入字符串可以让下标从 1 开始。
string s=" ", a;
cin >> a;
s+=a;
参考代码实现:AcWing 138. 兔子与兔子
字符串匹配
计算出模式串的 hash 值,再计算主串中每个长度等于模式串的子串的 hash 值,若相等则匹配。
参考代码:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N = 1e6 + 5;
string s = " ";
ull get_hash(string& s) {
ull ret = 0;
for (char& c : s) {
ret = ((ret * 131) + c); // 取 P = 131, M = unsigned long long 最大值
}
return ret;
}
ull f[N], p[N], n, m, ans;
ull get_hash(int l, int r) {
return f[r] - f[l - 1] * p[r - l + 1];
}
signed main() {
ios::sync_with_stdio(0);
#ifndef ONLINE_JUDGE
clock_t t0 = clock();
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.
string a, t; // 主串和模式串
cin >> a >> t;
n = a.length();
s += a;
p[0] = 1;
for (int i = 1;i <= n;i++) {
f[i] = f[i - 1] * 131 + s[i]; // f[i] = get_hash(s[1,i])
p[i] = p[i - 1] * 131; // p[i] = pow(131,i)
}
ull hasht = get_hash(t); // 模式串的 hash 值
int len_t = t.length();
// cout << get_hash(t) << endl;
for (int i = 1;i <= n - len_t + 1;i++) {
if (get_hash(i, i + len_t - 1) == hasht) {
// cout << i << ' ';
ans++;
}
}
cout << ans << endl;
#ifndef ONLINE_JUDGE
cerr << "Time used:" << clock() - t0 << "ms" << endl;
#endif
return 0;
}
例题
[ABC284F] ABCBAC
题目可以转化为:将字符串划分为三部分 \(S1+S2+S3=S\),而且 \(len(S1)+len(S3)=len(S2)=N\),且 \(S1+S3=\text{reserve}(S2)\),输出翻转后的 \(S2\) 和 \(i=len(S1)\)。
直接枚举 i,就可以得到 \(S1\) 和 \(S3\),:\(H(S1+S3)=(H(S1)<<(n-i)+H(S3))\),用前缀哈希来计算 \(H(S1)\) 和 \(H(S3)\)。
计算翻转后的 \(S2\) 的 hash 值,直接用翻转后的 \(S\) 的前缀哈希就可以求出来。
注意:本题卡自然溢出(\(M=2^{64}\)),可以用别的模数。(数据太强了,20081009,0x66ccff,0xccfccf250都被卡掉了。)
参考代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6 + 5, MOD = 998244353;
int n;
string s = " ";
int hash1[N], hash2[N], p[N];
int get_hash1(int l, int r) {
return (hash1[r] - (hash1[l - 1] * p[r - l + 1]) % MOD + MOD) % MOD;
}
int get_hash2(int l, int r) {
return (hash2[l] - (hash2[r + 1] * p[r - l + 1]) % MOD + MOD) % MOD;
}
signed main() {
ios::sync_with_stdio(0);
#ifndef ONLINE_JUDGE
clock_t t0 = clock();
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.
cin >> n;
string a;
cin >> a;
s += a;
p[0] = 1;
for (int i = 1;i <= n * 2;i++) {
hash1[i] = (hash1[i - 1] * 131 + s[i]) % MOD; // 自定义模数
p[i] = (p[i - 1] * 131) % MOD;
}
for (int i = 2 * n;i--;) hash2[i] = (hash2[i + 1] * 131 + s[i]) % MOD; // 逆序前缀哈希
// cout << p[4] << endl;
for (int i = 0;i <= n;i++) {
int hashs1 = get_hash1(1, i), hashs2 = get_hash2(i + 1, i + n), hashs3 = get_hash1(i + n + 1, n + n);
if ((hashs1 * p[n - i] % MOD + hashs3 % MOD) % MOD == hashs2) {
for (int j = i + n;j > i;j--) cout << s[j];
cout << endl << i << endl;
return 0;
}
}
cout << -1 << endl;
#ifndef ONLINE_JUDGE
cerr << "Time used:" << clock() - t0 << "ms" << endl;
#endif
return 0;
}
AcWing 139. 回文子串的最大长度
和上一题类似,本题也需要求一遍字符串翻转后的 hash 值。
枚举每个点作为回文串的中点,二分求出构成回文串的最大半径,判断方法为左边的逆序哈希值是否和右边的哈希值相等。
枚举的复杂度为 \(O(n)\),二分的复杂度为 \(O(\log n)\),总的时间复杂度为 \(O(n \log n)\),可以通过这题。
参考代码:去 AcWing 查看