字符串哈希
定义
我们定义一个把字符串映射到整数的函数
我们希望这个函数
Hash 的思想
Hash 的核心思想在于,将输入映射到一个值域较小、可以方便比较的范围。
Warning
这里的「值域较小」在不同情况下意义不同。
在 哈希表 中,值域需要小到能够接受线性的空间与时间复杂度。
在字符串哈希中,值域需要小到能够快速比较(
同时,为了降低哈希冲突率,值域也不能太小。
性质
具体来说,哈希函数最重要的性质可以概括为下面两条:
在 Hash 函数值不一样的时候,两个字符串一定不一样;
在 Hash 函数值一样的时候,两个字符串不一定一样(但有大概率一样,且我们当然希望它们总是一样的)。
我们将 Hash 函数值一样但原字符串不一样的现象称为哈希碰撞。
解释
我们需要关注的是什么?
时间复杂度和 Hash 的准确率。
通常我们采用的是多项式 Hash 的方法,对于一个长度为
特别要说明的是,也有很多人使用的是另一种 Hash 函数的定义,即
显然,上面这两种哈希函数的定义函数都是可行的,但二者在之后会讲到的计算子串哈希值时所用的计算式是不同的,因此千万注意 不要弄混了这两种不同的 Hash 方式。
由于前者的 Hash 定义计算更简便、使用人数更多、且可以类比为一个
还有,有时为了方便和扩大模数,我们在 C++ 中我们会使用 unsigned long long
来定义 Hash 函数的结果。由于 C++ 的特性,我们相当于把模数
准确率会在后面讨论。
Hash 的错误率分析
Hash 冲突
Hash 冲突是指两个不同的字符串映射到相同的 Hash 值。
我们设 Hash 的取值空间(所有可能出现的字符串的数量)为
则 Hash 冲突的概率为:
证明
当 Hash 中每个值生成概率相同时,Hash 不冲突的概率为:
化简得到:
则 Hash 冲突的概率为:
这个公式还是太复杂了,我们进一步化简。
根据泰勒公式:
当
将它带入 Hash 不冲突的原始公式:
化简:
则 Hash 冲突的概率为:
卡大模数 Hash
注意到这个公式:
为了卡掉 Hash,我们要满足一下条件:
要大于模数。 要尽量小。
举个例子:
若字符集为 大小写字母和数字,模数为
所以对于这个范围,我们随机生成
卡自然溢出 Hash
这种 Hash 由于模数太大,用上面的方法卡不了,所以我们需要另一种方法。
首先,这种 Hash 是形如
b 为偶数
此时
容易发现若
所以我们只要构造形如:
aaa...a
baa...a
且长度大于
b 为奇数
定义
例:
即把 a
变成 b
,把 b
变成 a
。
再定义
不断构造
推导
尝试相减:
发现出现了
设:
根据原式得:
因为
所以:
但这样太大了,
即
所以
即当
例题
例题:BZOJ 3097 Hash Killer I
给定一个用 自然溢出 实现的 Hash,要求构造一个字符串来卡掉它。
例题:BZOJ 3097 Hash Killer II
给定一个用 大模数 实现的 Hash,要求构造一个字符串来卡掉它。
例题:洛谷 U461211 字符串 Hash(数据加强)
给定
Hash 的改进
多值 Hash
看了上面这么多的卡法,当然也有解决办法。
多值 Hash,就是有多个 Hash 函数,每个 Hash 函数的模数不一样,这样就能解决 Hash 冲突的问题。
判断时只要有其中一个的 Hash 值不同,就认为两个字符串不同,若 Hash 值都相同,则认为两个字符串相同。
一般来说,双值 Hash 就够用了。
多次询问子串哈希
单次计算一个字符串的哈希值复杂度是
一般采取的方法是对整个字符串先预处理出每个前缀的哈希值,将哈希值看成一个
令
现在,我们想要用类似前缀和的方式快速求出
对比观察上述两个式子,我们发现
实现
模数 Hash:
注:效率较低,实际使用中不推荐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
双值 Hash:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
```
Hash 的应用
字符串匹配
求出模式串的哈希值后,求出文本串每个长度为模式串长度的子串的哈希值,分别与模式串的哈希值比较即可。
允许 次失配的字符串匹配
问题:给定长为
这道题无法使用 KMP 解决,但是可以通过哈希 + 二分来解决。
枚举所有可能匹配的子串,假设现在枚举的子串为
总的时间复杂度为
最长回文子串
二分答案,判断是否可行时枚举回文中心(对称轴),哈希判断两侧是否相等。需要分别预处理正着和倒着的哈希值。时间复杂度
这个问题可以使用 manacher 算法 在
通过哈希同样可以
最长公共子字符串
问题:给定
很显然如果存在长度为 check(k)
的逻辑为我们将所有所有字符串的长度为
时间复杂度为
确定字符串中不同子字符串的数量
问题:给定长为
为了解决这个问题,我们遍历了所有长度为
为了方便起见,我们将使用
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
例题
CF1200E Compress Words
给你若干个字符串,答案串初始为空。第
字符串个数不超过
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
本页面部分内容译自博文 строковый хеш 与其英文翻译版 String Hashing。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。
本页面最近更新:2024/8/6 18:13:10,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:iamtwz, 1292224662, algoriiiiithm, dkz051, Enter-tainer, GldHkkowo, Haohu Shen, HeRaNO, ImpleLee, Ir1d, kenlig, ksyx, Marcythm, Menci, mgt, ouuan, shawlleyw, ShuYuMo2003, sshwy, szdytom, taodaling, Tiphereth-A, wangchong, wendajiang, Xeonacid, xglight, Yanjun-Zhao, zyouxam
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用