Commit 89497d03 authored by 赵彦君's avatar 赵彦君 💬
Browse files

9-2hash

parent fe407970
Loading
Loading
Loading
Loading
+52 −13
Original line number Diff line number Diff line
@@ -5,11 +5,11 @@ Hash 的核心思想在于,将输入映射到一个值域较小、可以方便
!!! warning
    这里的“值域较小”在不同情况下意义不同。
    
[哈希](../ds/hash.md) 中,值域需要小到能够接受线性的空间与时间复杂度。
[ Hash ](../ds/hash.md) 中,值域需要小到能够接受线性的空间与时间复杂度。
    
    在字符串哈希中,值域需要小到能够快速比较( $10^9$ 、 $10^{18}$ 都是可以快速比较的)。
    在字符串 Hash 中,值域需要小到能够快速比较( $10^9$ 、 $10^{18}$ 都是可以快速比较的)。
    
    同时,为了降低哈希冲突率,值域也不能太小。
    同时,为了降低 Hash 冲突率,值域也不能太小。

我们定义一个把字符串映射到整数的函数 $f$ ,这个 $f$ 称为是 Hash 函数。

@@ -27,7 +27,7 @@ Hash 的核心思想在于,将输入映射到一个值域较小、可以方便

这里面的 $b$ 和 $M$ 需要选取得足够合适才行,以使得 Hash 函数的值分布尽量均匀。

如果 $b$ 和 $M$ 互质,在输入随机的情况下,这个 Hash 函数在 $[0,M)$ 上每个值概率相等,此时单次比较的错误率为 $\frac 1 M$ 。所以,哈希的模数一般会选用大质数。
如果 $b$ 和 $M$ 互质,在输入随机的情况下,这个 Hash 函数在 $[0,M)$ 上每个值概率相等,此时单次比较的错误率为 $\dfrac 1 M$ 。所以, Hash 的模数一般会选用大质数。

## Hash 的实现

@@ -58,32 +58,69 @@ bool cmp(const string& s, const string& t) {

### 错误率

若进行 $n$ 次比较,每次错误率 $\frac 1 M$ ,那么总错误率是 $1-\left(1-\frac 1 M\right)^n$ 。在随机数据下,若 $M=10^9 + 7$ , $n=10^6$ ,错误率约为 $\frac 1{1000}$ ,并不是能够完全忽略不计的。
若进行 $n$ 次比较,每次错误率 $\dfrac 1 M$ ,那么总错误率是 $1-\left(1-\dfrac 1 M\right)^n$ 。在随机数据下,若 $M=10^9 + 7$ , $n=10^6$ ,错误率约为 $\dfrac 1{1000}$ ,并不是能够完全忽略不计的。

所以,进行字符串哈希时,经常会对两个大质数分别取模,这样的话哈希函数的值域就能扩大到两者之积,错误率就非常小了。
所以,进行字符串 Hash 时,经常会对两个大质数分别取模,这样的话 Hash 函数的值域就能扩大到两者之积,错误率就非常小了。

### 多次询问子串哈希
### 多次询问子串 Hash 

单次计算一个字符串的哈希值复杂度是 $O(n)$ ,其中 $n$ 为串长,与暴力匹配没有区别,如果需要多次询问一个字符串的子串的哈希值,每次重新计算效率非常低下。
单次计算一个字符串的 Hash 值复杂度是 $O(n)$ ,其中 $n$ 为串长,与暴力匹配没有区别,如果需要多次询问一个字符串的子串的 Hash 值,每次重新计算效率非常低下。

一般采取的方法是对整个字符串先预处理出每个前缀的哈希值,将哈希值看成一个 $b$ 进制的数对 $M$ 取模的结果,这样的话每次就能快速求出子串的哈希了:
一般采取的方法是对整个字符串先预处理出每个前缀的 Hash 值,将 Hash 值看成一个 $b$ 进制的数对 $M$ 取模的结果,这样的话每次就能快速求出子串的 Hash 了:

令 $f_i(s)$ 表示 $f(s[1..i])$ ,那么 $f(s[l..r])=f_r(s)-f_{l-1}(s) \times b^{r-l+1}$ ,其中 $b^{r-l+1}$ 可以预处理出来。

这样的话,就可以在 $O(n)$ 的预处理后每次 $O(1)$ 地计算子串的哈希值了。
这样的话,就可以在 $O(n)$ 的预处理后每次 $O(1)$ 地计算子串的 Hash 值了。

## Hash 的应用

### 字符串匹配

求出模式串的哈希值后,求出文本串每个长度为模式串长度的子串的哈希值,分别与模式串的哈希值比较即可。
求出模式串的 Hash 值后,求出文本串每个长度为模式串长度的子串的 Hash 值,分别与模式串的 Hash 值比较即可。

### 最长回文子串

二分答案,判断是否可行时枚举回文中心(对称轴),哈希判断两侧是否相等。需要分别预处理正着和倒着的哈希值。时间复杂度 $O(n\log n)$ 。
二分答案,判断是否可行时枚举回文中心(对称轴), Hash 判断两侧是否相等。需要分别预处理正着和倒着的 Hash 值。时间复杂度 $O(n\log n)$ 。

这个问题可以使用 [manacher 算法](./manacher.md) 在 $O(n)$ 的时间内解决。

### 确定字符串中不同子字符串的数量

问题:给定长为 $n$ 的字符串 ,仅由小写英文字母组成,查找该字符串中不同子串的数量。

为了解决这个问题,我们遍历了所有长度为 $l=1,\cdots ,n$ 的子串。对于每个长度为 $l$ ,我们将其 Hash 值乘以相同的 $b$ 的幂次方,并存入一个数组中。数组中不同元素的数量等于字符串中长度不同的子串的数量,并此数字将添加到最终答案中。

为了方便起见,我们将使用 $h[i]$ 作为 Hash 的前缀字符,并定义 $h[0]=0$ 。

```cpp
int count_unique_substrings(string const& s) {
    int n = s.size();
    
    const int b = 31;
    const int m = 1e9 + 9;
    vector<long long> b_pow(n);
    b_pow[0] = 1;
    for (int i = 1; i < n; i++)
        b_pow[i] = (b_pow[i-1] * b) % m;
    
    vector<long long> h(n + 1, 0);
    for (int i = 0; i < n; i++)
        h[i+1] = (h[i] + (s[i] - 'a' + 1) * b_pow[i]) % m;
    
    int cnt = 0;
    for (int l = 1; l <= n; l++) {
        set<long long> hs;
        for (int i = 0; i <= n - l; i++) {
            long long cur_h = (h[i + l] + m - h[i]) % m;
            cur_h = (cur_h * b_pow[n-i-1]) % m;
            hs.insert(cur_h);
        }
        cnt += hs.size();
    }
    return cnt;
}
```

### 例题

???+note "[CF1200E Compress Words](http://codeforces.com/contest/1200/problem/E)"
@@ -92,7 +129,7 @@ bool cmp(const string& s, const string& t) {
    字符串个数不超过 $10^5$ ,总长不超过 $10^6$ 。
    
    ??? mdui-shadow-6 "题解"
        每次需要求最长的、是原答案串的后缀、也是第 $i$ 个串的前缀的字符串。枚举这个串的长度,哈希比较即可。
        每次需要求最长的、是原答案串的后缀、也是第 $i$ 个串的前缀的字符串。枚举这个串的长度, Hash 比较即可。
        
        当然,这道题也可以使用 [KMP 算法](./kmp.md) 解决。
    
@@ -166,3 +203,5 @@ bool cmp(const string& s, const string& t) {
          cout << cur;
        }
        ```

**本页面部分内容译自博文 [строковый хеш](https://github.com/e-maxx-eng/e-maxx-eng/blob/61aff51f658644424c5e1b717f14fb7bf054ae80/src/string/string-hashing.md)与其英文翻译版[String Hashing](https://cp-algorithms.com/string/string-hashing.html)。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。**
 No newline at end of file