oi-wiki
Version:
wiki for OI / ACM-ICPC
102 lines (61 loc) • 3.26 kB
Markdown
在介绍 Hash 算法之前,首先你需要了解关于 [字符串匹配](/string/match) 的事情。
## Hash 的思想
Hash 的核心思想在于,暴力算法中,单次比较的时间太长了,应当如何才能缩短一些呢?
如果要求每次只能比较 $O(1)$ 个字符,应该怎样操作呢?
是比较第一个?最后一个?随便选一个?求所有字符的和?
我们定义一个把 string 映射成 int 的函数 $f$,这个 $f$ 称为是 Hash 函数。
我们希望这个函数 $f$ 可以方便地帮我们判断两个字符串是否相等。
具体来说,我们希望在 Hash 函数值不一样的时候,两个字符串一定不一样。
另外,反过来不需要成立。我们把这种条件称为是单侧错误。
我们需要关注的是什么?
时间复杂度和 Hash 的准确率。
通常我们采用的是多项式 Hash 的方法,即 $\operatorname{f}(s) = \sum s[i] \times b^i \pmod M$
这里面的 $b$ 和 $M$ 需要选取得足够合适才行,以使得 Hash 的冲突尽量均匀。
如果 $b$ 和 $M$ 互质,在输入随机的情况下,这个 Hash 函数在 $[0,M)$ 上每个值概率相等
此时错误率为 $\frac1M$ (单次比较)
在输入不是随机的情况下,效果也很好。
## Hash 的实现
伪代码:
```text
match_pre(int n) {
exp[0] = 1;
for (i = 1; i < n; i++) {
exp[i] = exp[i - 1] * b % M;
}
}
match(char *a, char *b, int n, int m) {
// match 函数返回:长度为 m 的串 b 在长度为 n 的串 a 中的匹配位置
// hash(a, m) 函数用来获得某个字符串前 m 个字符的部分的 hash 值
ans = new vector();
int ha = hash(a, m);
int hb = hash(b, m);
for (i = 0; i < n - m + 1; i++) {
if ((ha - hb * exp[i]) % M == 0) {
ans.push_back(i);
}
ha = (ha - a[i] * exp[i] + a[i + m] * exp[i + m]) % M;
}
return ans;
}
```
通过上面这段代码,可以发现,每次直接计算 Hash 是 $O(串长)$ 的
## Hash 的分析与改进
改进时间复杂度或者错误率
在上述例子中,时间复杂度已经是 $O(n+m)$
我们来分析错误率
由于 $n >> m$,要进行约 $n$ 次比较,每次错误率 $\frac1{M}$ ,那么总错误率是?
先补充一些随机数学的知识(非严格地)
现在考虑这 $n$ 次比较,如果看成独立的,总错误率 $1-(1-1/M)^n$
当 $M >> n$ 时,总错误率接近于$\frac{n}{M}$
当 $M = n$ 时,接近于 $1-\frac{1}{e} (≈0.63)$
如果不是独立的,最坏情况也就是全部加起来,等于 $\frac{n}{M}$
要改进错误率,可以增加 $M$
选取一个大的 $M$,或者两个互质的小的 $M$
时间复杂度不变,单次错误率平方
## Hash 的应用
不只是字符串中,在其他情况也可以用。
假设有个程序要对 $10^{18}$ 大小的数组进行操作,保证只有 $10^6$ 个元素被访问到
由于存不下,我们在每次操作时,对下标取 Hash 值(比如,直接 $\bmod M$),然后在 $M$ 大小的数组内操作
如果冲突了怎么办?
方案 1:尝试找下一个位置,下下个位置(优点:速度快;缺点:删除麻烦)
方案 2:在数组的每个位置直接挂一个链表