哈希 hash

$$\texttt{字符串哈希}$$

OI-wiki Link

字符串哈希,将字符串映射为一个整数,利用这个整数快速地判断两个字符串是否相等。

令函数 \(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;
}

附:生日悖论

百度百科 Link

生日悖论,指的是随机选出 \(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。

posted @ 2023-08-28 15:13  wnsyou  阅读(28)  评论(0编辑  收藏  举报