哈希是一个玄学的方法……最适骗分法
哈希,指将信息通过某种方式的缩减,映射到某一个值域上,从而表示这个信息。
如果有两个信息同时映射到了同一个位置,那么就会产生哈希冲突。
哈希冲突在哈希表中有两种处理方式:
链表
质数后移(向后移动质数位,知道找到一个空的地方为止)
但是,在OI中,哈希冲突恰恰是毒瘤出题人卡你正确性的方法……关键是我们不一定能够保证没有哈希冲突……QwQ
所以,哈希很玄学,慎用!
一般来说,字符串哈希在OI中常用的公式是 \(hash(str) = \sum_{i=1}^n base^{n-i} \times str_i\)。
在生产环境中,可以有 time33, md5, sha256
等经典算法。但是这里用不到……
那么用这个公式有什么好处呢?
可以通过 \(O(n)\) 的预处理,在 \(O(1)\) 得出任意一个子串的哈希值
可以随意拼接两个字符串,在 \(O(1)\) 内得出合并后的哈希,并在 \(O(m)\) 时间内再处理
可以扩展到二维甚至更高
……
不扯了……讲实现了
由于我们需要映射到某一个值域上,所以,一般来说,我们取为 unsigned long long
可以表示的值域即可。(这样也有一些其他的好处,自然溢出避免了超慢的取模运算)
那么,我们以字符串 jeefy
,base
取 13
为例:
Text | Hash | Calc |
---|---|---|
j | 106 | \(0 \times 13\) + 'j' |
je | 1479 | \(106 \times 13 +\) ’e' |
jee | 19328 | \(1479 \times 13\) + 'e' |
jeef | 251366 | \(19328 \times 13\) + 'f' |
jeefy | 3267897 | \(251366 \times 13\) + 'y' |
生成代码如下
我们考虑换一种表示法:
Text | Calc |
---|---|
j | \(13^0 + ord(j)\) |
je | \(13^1 \times ord(j) + 13^0 \times ord(e)\) |
jee | \(13^2 \times ord(j) + 13^1 \times ord(e) + 13^0 \times ord(e)\) |
jeef | \(13^3 \times ord(j) + 13^2 \times ord(e) + 13^1 \times ord(e) + 13^0 \times ord(f)\) |
jeefy | \(13^4 \times ord(j) + 13^3 \times ord(e) + 13^2 \times ord(e) + 13^1 \times ord(f) + 13^0 \times ord(y)\) |
想必,有了这个表之后,哈希拼接应该就很简单了吧。
\(|B|\) 指的是字符串的长度
我们可以预处理出 \(base^i\)
hashInt bofs[N] = {1}; // base_offset
for (int i = 1; i < N; ++i) bofs[i] = bofs[i - 1] * base;
那么两个计算如下
struct String {
string s;
hashInt ha;
}
String merge(String A, String B) {
String s = {A.s + B.s, A.ha * bofs[B.s.size()] + B.ha};
return s;
}
没有实际运行过程序,请自行斟酌
同理,如果我们需要字符串片段的哈希,我们可以通过合并的逆操作……
hashInt ha[N];
hashInt slice(int l, int r) { // [l, r]
return ha[r] - ha[l - 1] * bofs[r - l + 1];
}
那么,恭喜你,掌握了哈希的大部分用法了。
总所周知,哈希是出题人特别喜欢卡的一种做法,所以,我们需要较少减少哈希冲突的可能。那么比较好的解决方法是双哈希。而双哈希指的是双模数和双底数,也就是说值域和 \(base\) 都不一样。这种双保险的方法是很难被卡的。
没想到吧,同构也可以通过哈希来!
这道题可以用二维哈希
其实C++也是有内置的哈希函数的,参考std::hash - cppreference.com