字符串哈希
注意(Warning)
本文章内所有有关字符串下标的,统一都从 \(1\) 开始算起。
定义
字符串哈希其实就是把一段字符串转化成一个数字。
在进行字符串匹配时不需要再 \(O(strlen(s))\) 匹配字符串本身,而只需要匹配两个字符串的哈希值就好了。
字符串哈希值计算
对于一个字符串 \(s\) ,定义它的哈希值为:
即:
其中,
\(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]\) 有可能溢出,所以再对一个数 \(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)]\) 时,
两字符串相等。
这种双值哈希十分可靠,可以直接使用,而且不会被卡掉。
最后一些细节及优化
-
在选定 \(p\) 和 \(mod\) 时,两者都最好选一个较大的质数,可以降低哈希的冲突率。
在这里推荐 \(p\) 取 \(131\) 或 \(1331\) 或 \(1313131\),\(mod\) 一般取 \(23333333\)。 -
更为方便地,\(mod\) 可以取 \(2^{64} - 1\),即 \(unsigned \ long \ long\) 所能储存的最大
值,然后将 \(hush\) 数组开为 \(unsigned \ long \ long\),这样就可以用 \(unsigned \ long \ long\) 的自然溢出代替取模。 -
如果有多个模式串,一个一个算它们的哈希值和暴力没什么区别。
不如将他们都拼成一个总模式串,整体算一次,要用哪个模式串的哈希值时,直接
用:\[((hush[pos[i] + strlen(t[i]) - 1] - hush[pos[i] - 1] * p ^ {strlen(t[i])}) \% mod + mod) \% mod\]其中,\(t[i]\) 表示第 \(i\) 个模式串,\(pos[i]\) 表示第 \(i\) 个模式串的第一个字符在总
模式串中的下标。 -
在计算 \(p ^ {r - l + 1}\) 时,可以用快速幂,也可以用一个数组 \(powp_i\) 预处理好 \(p\)
的 \(i\) 次方,注意别忘了取模。

浙公网安备 33010602011771号