哈希 hash
$$\texttt{字符串哈希}$$
字符串哈希,将字符串映射为一个整数,利用这个整数快速地判断两个字符串是否相等。
令函数 \(f(s)\) 为字符串 \(s\) 映射后的整数。
主要性质
假设现在有两个字符串 \(s\) 和 \(t\)。
- 当 \(f(s) \ne f(t)\) 时,\(s\) 一定与 \(t\) 不同。
- 当 \(f(s) = f(t)\) 时,\(s\) 不一定与 \(t\) 相同。
当 \(f(s) = f(t)\) 且 \(s \ne t\) 时,我们称发生了哈希冲突。
大致思想
首先设定进制数 \(P\) 和模数 \(mod\) (模数必须为质数)以及单个字符的映射规则,要求每个单个字符映射后的数不会冲突,通常可以使用其 ASCII 码或者整体偏移一定值,但得确保每个字符映射后的数都要严格小于 \(P\)。
然后将字符串看作一个 \(P\) 进制数,每个位上的数都是其对应位置上的字符经过映射后得到的数,高位在左。
将其转为十进制,记得这个十进制数要模 \(mod\)。
const int P = 131, mod = 1e9 + 7; // p 为进制数,mod 为模数
const int N = 1e5 + 10;
string s;
int n, hsh[N], p[N];
int id (char c) { // 单个字符映射规则
return c - 'a';
}
int get_Hash (int l, int r) { // 返回子串 [l, r] 的哈希值
int sum = 0;
for (int i = l; i <= r; i++) {
sum = (1ll * sum * P + id(s[i])) % mod; // 转十进制
}
return sum;
}
哈希冲突
当有 \(n\) 个不同的字符串时,不发生哈希冲突的概率是 \(\frac{mod \times (mod - 1) \times (mod - 2) \cdots (mod - n + 1)}{mod^n}\)。
当 \(mod = 10^9+7\) 时,经过测试,我们可以发现:
- 当 \(n = 10^2\) 时,概率大致为 \(99.999505001212170588950622907021\%\)。
- 当 \(n = 10^3\) 时,概率大致为 \(99.950062456651772569132113899215\%\)。
- 当 \(n = 10^4\) 时,概率大致为 \(95.123402247659835488397678249228\%\)。
- 当 \(n = 5\times 10^4\) 时,概率大致为 \(28.650599316946955379668306174157\%\)。
- 当 \(n = 6\times 10^4\) 时,概率大致为 \(16.529789848427975192568691042982\%\)。
- 当 \(n = 7\times 10^4\) 时,概率大致为 \(8.629167509240295731162218376142\%\)。
- 当 \(n = 10^5\) 时,概率大致为 \(0.673716114763418643374601169592\%\)。
- 当 \(n = 2 \times 10^5\) 时,概率大致为 \(0.000000205861313426543109296943\%\)。
- 当 \(n = 5 \times 10^5\),概率已经小于了 \(10^{-30}\)!
当 \(n\) 来到 \(2 \times 10^5\) 及以上时,不出现哈希冲突的概率已经很小了,也就是说模数设为 \(10^9+7\) 顶多也就是解决 \(n \leqslant 6 \times 10^4\) 的题,当 \(n\) 更大时几乎是必然出现哈希冲突,则需要考虑更大的模数。
当 \(mod = 10^{18}+3\) 时(这是个质数),经过测试,我们可以惊喜的发现:
- 当 \(n = 10^4\) 时,概率大致为 \(99.999999994999513598095339239613\%\)。
- 当 \(n = 10^5\) 时,概率大致为 \(99.999999499995059201512201396689\%\)。
- 当 \(n = 10^6\) 时,概率大致为 \(99.999949999962680605332387973050\%\),趋近于 \(100\%\)!
- 当 \(n = 10^7\) 时(以下为研究所需,正常题目不可能出这么大的数据范围),概率大致为 \(99.995000124497913132062473784423\%\),仍然趋近于 \(100\%\)!
- 当 \(n = 10^8\) 时,概率大致为 \(99.501247914276431954941695701145\%\),也是几乎不会出现哈希冲突。
- 当 \(n = 10^9\) 时,概率大致为 \(60.653065930827893892981345080884\%\)。
也就是说,当模数设为 \(10^{18}+3\) 时,正常题目几乎不可能出现哈希冲突(如果出现了,那么恭喜你,你可以去买彩票了!)。
注意,当模数设为 \(10^{18}+3\) 时,使用 long long
计算可能会导致溢出,需要用 __int128
来计算,直接强制类型转换((__int128)1
)即可。
using ll = long long;
// p 为进制数,mod 为模数
const int P = 131;
const ll mod = 1e18 + 3;
const int N = 1e5 + 10;
string s;
int n;
ll hsh[N], p[N];
int id (char c) { // 单个字符映射规则
return c - 'a';
}
ll get_Hash (int l, int r) { // 返回子串 [l, r] 的哈希值
ll sum = 0;
for (int i = l; i <= r; i++) {
sum = ((__int128)1 * sum * P + id(s[i])) % mod; // 转十进制
}
return sum;
}
附:测试代码如下:
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
// const long long mod = 1e18 + 3;
long double x = 1;
int n;
int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
x = x * (mod + 1 - i) / mod;
}
cout << fixed << setprecision(32) << x;
return 0;
}
快速求子串哈希值
可我们发现,上面的代码中求子串的哈希值仍然是 \(O(n)\) 的,这和暴力判断没有区别,怎么优化呢?
再观察一下,当字符串不会改变时,可以考虑预处理。
using ll = long long;
// p 为进制数,mod 为模数
const int P = 131;
const ll mod = 1e18 + 3;
const int N = 1e5 + 10;
string s;
int n;
ll hsh[N]; // 存储前缀哈希值
int id (char c) { // 单个字符映射规则
return c - 'a';
}
int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> s, n = s.size(), s = " " + s, p[0] = 1;
for (int i = 1; i <= n; i++) { // 预处理前缀哈希值
hsh[i] = ((__int128)1 * hsh[i - 1] * P + id(s[i])) % mod; // 转为十进制数,hsh[i] 表示前 i 位哈希后的结果
}
return 0;
}
若要求子串 \([l,r]\) 的哈希值,则使用类似于前缀和的求法,公式:\(hsh_r - hsh_{l-1} \times P^{r-l+1}\)。
可以发现 \(r-l+1\leqslant n\),则可以在预处理前缀哈希值的同时处理 \(P\) 的 \(0 \sim n\) 次方在模 \(mod\) 意义下的值。
for (int i = 1; i <= n; i++) { // 预处理前缀哈希值
p[i] = (__int128)1 * p[i - 1] * P % mod; // p[i] 表示 P 的 i 次方 % mod 的值
hsh[i] = ((__int128)1 * hsh[i - 1] * P + id(s[i])) % mod; // 转为十进制数,hsh[i] 表示前 i 位哈希后的结果
}
记得减法取模的细节。
ll Hash (int l, int r) { // 返回子串 [l, r] 哈希值
return (hsh[r] + mod - (__int128)1 * hsh[l - 1] * p[r - l + 1] % mod) % mod;
}
附:生日悖论
生日悖论,指的是随机选出 \(23\) 个或更多人,出现至少两个人生日相同的概率高于 \(50\%\)。
由于这点反人类直觉,所以被称为“生日悖论”。
计算式子很类似哈希冲突,所以提到哈希往往都会提到生日悖论。
验证代码:
#include <bits/stdc++.h>
using namespace std;
long double x = 1;
int n;
int main () {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
x = x * (366 - i) / 365;
}
cout << fixed << setprecision(32) << x; // 随机选择 n 个人,两两生日不同的概率
return 0;
}
$$\texttt{哈希表}$$
详见 OI Wiki。
可以用 map。