字符串哈希

注意(Warning)

本文章内所有有关字符串下标的,统一都从 \(1\) 开始算起


定义

字符串哈希其实就是把一段字符串转化成一个数字。
在进行字符串匹配时不需要再 \(O(strlen(s))\) 匹配字符串本身,而只需要匹配两个字符串的哈希值就好了。


字符串哈希值计算

对于一个字符串 \(s\) ,定义它的哈希值为:

\[f(s) = \sum ^{l} _{i = 1} s[i] * p ^ {l-i} \]

即:

\[f(s) = s[1] * p ^ {l - 1} + s[2] * p ^ {l -2} + \dots + s[l] * p ^ {0} \]

其中,
\(p\) 是一个质数,
\(l\) 是字符串长度,
\(s[i]\) 转化为它在 \(ASCII\) 码对应的数值
举个例子:字符串 "\(abc\)" 的哈希值为:\(a * p^2 + b * p + c\)


字符串前缀哈希值算

在代码中,我们会用到字符串的前缀的哈希值(至于为什么等会会说),
因此我们用 \(hush[i]\) 表示字符串 \(s[1 \dots i]\) 的哈希值。

例如一个字符串 \(s_1 s_2 s_3 s_4 s_5\)
\(hush[1] = s_1\)
\(hush[2] = s_1 * p + s_2\)
\(hush[3] = s_1 * p ^ 2 + s_2 * p + s_3\)
\(hush[4] = s_1 * p ^ 3 + s_2 * p ^ 2 + s_3 * p + s_4\)
\(hush[5] = s_1 * p ^ 4 + s_2 * p ^ 3 + s_3 * p ^ 2 + s_4 * p + s_5\).
那么就可以得到这样一个公式:

\[hush[i] = hush[i - 1] * p + s[i] \]

当然 \(hush[i]\) 有可能溢出,所以再对一个数 \(mod\) (最好是素数) 取模就好了。


子串哈希值计算

在字符串匹配中,我们实际上用的是模式串的哈希值主串的子串的哈希值进行比对。

因此,在比对过程中我们不能再对子串进行哈希值计算,因为这样合暴力匹配原串复杂度一样。

这时前缀哈希值就派上用场了。

依旧拿上面的例子

\(eg1:\)
要求 \(s_3 s_4\) 的哈希值,它显然是 \(s_3 * p + s_4\)
现在看如何用前缀哈希值来算。

从感觉上讲,这有点像前缀和,所以考虑如何用 \(hush[4]\)\(hush[3 - 1]\) 算出他。
\(hush[3 - 1 = 2] = s_1 * p + s_2\)
\(hush[4] = s_1 * p ^ 3 + s_2 * p ^ 2 + s_3 * p + s_4\)
貌似确实可以,只需要用 \(hush[4] - hush[3 - 1] * p ^ {2}\) 即可得到。

\(eg2:\)
\(s_2 s_3 s_4\) 的哈希值,是 \(s_2 * p ^ 2 + s_3 * p + s_4\)
\(hush[2 - 1 = 1] = s_1\)
\(hush[4] = s_1 * p ^ 3 + s_2 * p ^ 2 + s_3 * p + s_4\)
可以用 \(hush[4] - hush[2 - 1] * p ^ {4 - 2 + 1 = 3}\) 算出来。

整体可以发现,
如果要算 \(s[l \dots r]\) 的哈希值,
那么 \(hush[r]\) 就包含了所有要计算的部分,
但是还多了 \(s[1] * p ^ {r - 1} + s[2] * p ^ {r - 2} + \dots + s[l - 1] * p ^ {r - l + 1}\) 一部分,
\(hush[l - 1] = s[1] * p ^ {l - 2} + s[2] * p ^ {l - 3} + \dots + s[l - 1]\)
发现只需要给 \(hush[l - 1]\) 乘以 \(p ^ {r - l + 1}\),就可以得到多出的那一部分,
再用 \(hush[r]\) 减去就可以了。
但由于取模的存在,以上这个式子还要取模。

综上可得:要计算 \(s[l \dots r]\) 的哈希值就用:
\(((hush[r] - hush[l - 1] * p ^ {r - l + 1}) \% mod + mod) \% mod\) \(O(1)\) 算出。

注:之所以这么写是因为 \(hush[r]\)\(hush[l - 1]\) 都是已经被取模过的,也就是说
\(\space\space\space\space\space\) \(hush[r] - hush[l - 1] * p ^ {r - l + 1}\) 可能小于 \(0\),只有这样才能保证子串哈希值是正的。

如此一来,计算字串的哈希值就轻而易举了。


双值哈希

双值哈希是为了解决哈希冲突而设计的。

简单来说,就是取两个 \(p\),两个 \(mod\),分别对字符串进行哈希值的计算,
当且仅当两个哈希值相同时,两个字符串才相同。

更具体的:
\(hush1[i] = hush1[i - 1] * p1 \% mod1\)
\(hush2[i] = hush2[i - 1] * p2 \% mod2\)
要比较字符串 \(str1\) \(str2\)
当且仅当:
\(\space\space\) \(str1\)\(hush1[strlen(str1)]\) 等于 \(str2\)\(hush1[strlen(str2)]\)
\(\space\space\) \(str1\)\(hush2[strlen(str1)]\) 等于 \(str2\)\(hush2[strlen(str2)]\) 时,
两字符串相等。

这种双值哈希十分可靠,可以直接使用,而且不会被卡掉。


最后一些细节及优化

  1. 在选定 \(p\)\(mod\) 时,两者都最好选一个较大的质数,可以降低哈希的冲突率。
    在这里推荐 \(p\)\(131\)\(1331\)\(1313131\)\(mod\) 一般取 \(23333333\)

  2. 更为方便地,\(mod\) 可以取 \(2^{64} - 1\),即 \(unsigned \ long \ long\) 所能储存的最大
    值,然后将 \(hush\) 数组开为 \(unsigned \ long \ long\),这样就可以用 \(unsigned \ long \ long\)自然溢出代替取模

  3. 如果有多个模式串,一个一个算它们的哈希值和暴力没什么区别。
    不如将他们都拼成一个总模式串,整体算一次,要用哪个模式串的哈希值时,直接
    用:

    \[((hush[pos[i] + strlen(t[i]) - 1] - hush[pos[i] - 1] * p ^ {strlen(t[i])}) \% mod + mod) \% mod\]

    其中,\(t[i]\) 表示第 \(i\) 个模式串,\(pos[i]\) 表示第 \(i\) 个模式串的第一个字符在
    模式串
    中的下标。

  4. 在计算 \(p ^ {r - l + 1}\) 时,可以用快速幂,也可以用一个数组 \(powp_i\) 预处理好 \(p\)
    \(i\) 次方,注意别忘了取模


关于字符串哈希会被卡掉

总而言之,最好用双值哈希。

posted @ 2024-09-17 21:16  syzyc  阅读(70)  评论(0)    收藏  举报