EVERYTHING HAPPENS FOR THE BES|

wnsyou

园龄:2年4个月粉丝:19关注:16

2023-08-28 15:13阅读: 42评论: 0推荐: 0

哈希 hash

字符串哈希

OI-wiki Link

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

令函数 f(s) 为字符串 s 映射后的整数。

主要性质

假设现在有两个字符串 st

  • f(s)f(t) 时,s 一定t 不同。
  • f(s)=f(t) 时,s 不一定t 相同。

f(s)=f(t)st 时,我们称发生了哈希冲突。

大致思想

首先设定进制数 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 个不同的字符串时,不发生哈希冲突的概率是 mod×(mod1)×(mod2)(modn+1)modn

mod=109+7 时,经过测试,我们可以发现:

  • n=102 时,概率大致为 99.999505001212170588950622907021%
  • n=103 时,概率大致为 99.950062456651772569132113899215%
  • n=104 时,概率大致为 95.123402247659835488397678249228%
  • n=5×104 时,概率大致为 28.650599316946955379668306174157%
  • n=6×104 时,概率大致为 16.529789848427975192568691042982%
  • n=7×104 时,概率大致为 8.629167509240295731162218376142%
  • n=105 时,概率大致为 0.673716114763418643374601169592%
  • n=2×105 时,概率大致为 0.000000205861313426543109296943%
  • n=5×105,概率已经小于了 1030

n 来到 2×105 及以上时,不出现哈希冲突的概率已经很小了,也就是说模数设为 109+7 顶多也就是解决 n6×104 的题,当 n 更大时几乎是必然出现哈希冲突,则需要考虑更大的模数。

mod=1018+3 时(这是个质数),经过测试,我们可以惊喜的发现:

  • n=104 时,概率大致为 99.999999994999513598095339239613%
  • n=105 时,概率大致为 99.999999499995059201512201396689%
  • n=106 时,概率大致为 99.999949999962680605332387973050%,趋近于 100%
  • n=107 时(以下为研究所需,正常题目不可能出这么大的数据范围),概率大致为 99.995000124497913132062473784423%,仍然趋近于 100%
  • n=108 时,概率大致为 99.501247914276431954941695701145%,也是几乎不会出现哈希冲突。
  • n=109 时,概率大致为 60.653065930827893892981345080884%

也就是说,当模数设为 1018+3 时,正常题目几乎不可能出现哈希冲突(如果出现了,那么恭喜你,你可以去买彩票了!)。

注意,当模数设为 1018+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] 的哈希值,则使用类似于前缀和的求法,公式:hshrhshl1×Prl+1

可以发现 rl+1n,则可以在预处理前缀哈希值的同时处理 P0n 次方在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;
}

哈希表

详见 OI Wiki

可以用 map。

本文作者:wnsyou の blog

本文链接:https://www.cnblogs.com/wnsyou-blog/p/hash.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   wnsyou  阅读(42)  评论(0编辑  收藏  举报
  1. 1 勝利への道 安藤浩和
  2. 2 Minecraft’s End Eric Fullerton
  3. 3 月光曲完整版 贝多芬 云熙音乐
  4. 4 平凡之路 (Live版) 朴树
  5. 5 Minecraft C418
  6. 6 Paradise NiziU
  7. 7 叫我,灰原哀 龙大人不喷火
  8. 8 心机之蛙,一直摸你肚子 ——《名侦探柯南》原创同人曲 炊饭,叶辞樱,温海,寒砧,南柯柯,小茜玛姬,盛姝,阿崔Ac,贝壳初,千湛,兮茶子DaYu,乔慕,黎鹿北,起千温卿,遮阳伞,曲悠
  9. 9 战 歌 此去经年
平凡之路 (Live版) - 朴树
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 朴树/韩寒

作曲 : 朴树

编曲 : 朴树

徘徊着的 在路上的

你要走吗 via via

易碎的 骄傲着

那也曾是我的模样

沸腾着的 不安着的

你要去哪 via via

谜一样的 沉默着的

故事你真的在听吗

我曾经跨过山和大海

也穿过人山人海

我曾经拥有着的一切

转眼都飘散如烟

我曾经失落失望失掉所有方向

直到看见平凡才是唯一的答案

当你仍然

还在幻想

你的明天 via via

她会好吗 还是更烂

对我而言是另一天

我曾经毁了我的一切

只想永远地离开

我曾经堕入无边黑暗

想挣扎无法自拔

我曾经像你像他像那野草野花

绝望着也渴望着

也哭也笑平凡着

jia do

la so

哦~

向前走 就这么走

就算你被给过什么

向前走 就这么走

就算你被夺走什么

向前走 就这么走

就算你会错过什么

向前走 就这么走

就算你会

我曾经跨过山和大海

也穿过人山人海

我曾经拥有着的一切

转眼都飘散如烟

我曾经失落失望失掉所有方向

直到看见平凡才是唯一的答案

我曾经毁了我的一切

只想永远地离开

我曾经堕入无边黑暗

想挣扎无法自拔

我曾经像你像他像那野草野花

绝望着 渴望着 也哭也笑平凡着

我曾经跨过山和大海

也穿过人山人海

我曾经问遍整个世界

从来没得到答案

我不过像你像他像那野草野花

冥冥中这是我 唯一要走的路啊

时间如烟

如此这般

明天已在 via via

风吹过的 路依然远

你的故事讲到了哪

加载中…

{{tag.name}}

{{tran.text}}{{tran.sub}}
无对应文字
有可能是
{{input}}
尚未录入,我来提交对应文字
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示