注意(Warning)
本文章内所有有关字符串下标的,统一都从 1 1 1 开始算起。
定义
字符串哈希其实就是把一段字符串转化成一个数字。
在进行字符串匹配时不需要再
O
(
s
t
r
l
e
n
(
s
)
)
O(strlen(s))
O(strlen(s)) 匹配字符串本身,而只需要匹配两个字符串的哈希值就好了。
字符串哈希值计算
对于一个字符串 s s s ,定义它的哈希值为:
f ( s ) = ∑ i = 1 l s [ i ] ∗ p l − i f(s) = \sum ^{l} _{i = 1} s[i] * p ^ {l-i} f(s)=i=1∑ls[i]∗pl−i
即:
f ( s ) = s [ 1 ] ∗ p l − 1 + s [ 2 ] ∗ p l − 2 + ⋯ + s [ l ] ∗ p 0 f(s) = s[1] * p ^ {l - 1} + s[2] * p ^ {l -2} + \dots + s[l] * p ^ {0} f(s)=s[1]∗pl−1+s[2]∗pl−2+⋯+s[l]∗p0
其中,
p
p
p 是一个质数,
l
l
l 是字符串长度,
s
[
i
]
s[i]
s[i] 转化为它在
A
S
C
I
I
ASCII
ASCII 码对应的数值。
举个例子:字符串 “
a
b
c
abc
abc” 的哈希值为:
a
∗
p
2
+
b
∗
p
+
c
a * p^2 + b * p + c
a∗p2+b∗p+c。
字符串前缀哈希值算
在代码中,我们会用到字符串的前缀的哈希值(至于为什么等会会说),
因此我们用
h
u
s
h
[
i
]
hush[i]
hush[i] 表示字符串
s
[
1
…
i
]
s[1 \dots i]
s[1…i] 的哈希值。
例如一个字符串
s
1
s
2
s
3
s
4
s
5
s_1 s_2 s_3 s_4 s_5
s1s2s3s4s5,
h
u
s
h
[
1
]
=
s
1
hush[1] = s_1
hush[1]=s1,
h
u
s
h
[
2
]
=
s
1
∗
p
+
s
2
hush[2] = s_1 * p + s_2
hush[2]=s1∗p+s2,
h
u
s
h
[
3
]
=
s
1
∗
p
2
+
s
2
∗
p
+
s
3
hush[3] = s_1 * p ^ 2 + s_2 * p + s_3
hush[3]=s1∗p2+s2∗p+s3,
h
u
s
h
[
4
]
=
s
1
∗
p
3
+
s
2
∗
p
2
+
s
3
∗
p
+
s
4
hush[4] = s_1 * p ^ 3 + s_2 * p ^ 2 + s_3 * p + s_4
hush[4]=s1∗p3+s2∗p2+s3∗p+s4,
h
u
s
h
[
5
]
=
s
1
∗
p
4
+
s
2
∗
p
3
+
s
3
∗
p
2
+
s
4
∗
p
+
s
5
hush[5] = s_1 * p ^ 4 + s_2 * p ^ 3 + s_3 * p ^ 2 + s_4 * p + s_5
hush[5]=s1∗p4+s2∗p3+s3∗p2+s4∗p+s5.
那么就可以得到这样一个公式:
h
u
s
h
[
i
]
=
h
u
s
h
[
i
−
1
]
∗
p
+
s
[
i
]
hush[i] = hush[i - 1] * p + s[i]
hush[i]=hush[i−1]∗p+s[i]
当然
h
u
s
h
[
i
]
hush[i]
hush[i] 有可能溢出,所以再对一个数
m
o
d
mod
mod (最好是素数) 取模就好了。
子串哈希值计算
在字符串匹配中,我们实际上用的是模式串的哈希值与主串的子串的哈希值进行比对。
因此,在比对过程中我们不能再对子串进行哈希值计算,因为这样合暴力匹配原串的复杂度一样。
这时前缀哈希值就派上用场了。
依旧拿上面的例子
e g 1 : eg1: eg1:
要求 s 3 s 4 s_3 s_4 s3s4 的哈希值,它显然是 s 3 ∗ p + s 4 s_3 * p + s_4 s3∗p+s4,
现在看如何用前缀哈希值来算。
从感觉上讲,这有点像前缀和,所以考虑如何用 h u s h [ 4 ] hush[4] hush[4] 和 h u s h [ 3 − 1 ] hush[3 - 1] hush[3−1] 算出他。
h u s h [ 3 − 1 = 2 ] = s 1 ∗ p + s 2 hush[3 - 1 = 2] = s_1 * p + s_2 hush[3−1=2]=s1∗p+s2,
h u s h [ 4 ] = s 1 ∗ p 3 + s 2 ∗ p 2 + s 3 ∗ p + s 4 hush[4] = s_1 * p ^ 3 + s_2 * p ^ 2 + s_3 * p + s_4 hush[4]=s1∗p3+s2∗p2+s3∗p+s4。
貌似确实可以,只需要用 h u s h [ 4 ] − h u s h [ 3 − 1 ] ∗ p 2 hush[4] - hush[3 - 1] * p ^ {2} hush[4]−hush[3−1]∗p2 即可得到。
e g 2 : eg2: eg2:
求 s 2 s 3 s 4 s_2 s_3 s_4 s2s3s4 的哈希值,是 s 2 ∗ p 2 + s 3 ∗ p + s 4 s_2 * p ^ 2 + s_3 * p + s_4 s2∗p2+s3∗p+s4,
h u s h [ 2 − 1 = 1 ] = s 1 hush[2 - 1 = 1] = s_1 hush[2−1=1]=s1,
h u s h [ 4 ] = s 1 ∗ p 3 + s 2 ∗ p 2 + s 3 ∗ p + s 4 hush[4] = s_1 * p ^ 3 + s_2 * p ^ 2 + s_3 * p + s_4 hush[4]=s1∗p3+s2∗p2+s3∗p+s4。
可以用 h u s h [ 4 ] − h u s h [ 2 − 1 ] ∗ p 4 − 2 + 1 = 3 hush[4] - hush[2 - 1] * p ^ {4 - 2 + 1 = 3} hush[4]−hush[2−1]∗p4−2+1=3 算出来。
整体可以发现,
如果要算 s [ l … r ] s[l \dots r] s[l…r] 的哈希值,
那么 h u s h [ r ] hush[r] hush[r] 就包含了所有要计算的部分,
但是还多了 s [ 1 ] ∗ p r − 1 + s [ 2 ] ∗ p r − 2 + ⋯ + s [ l − 1 ] ∗ p r − l + 1 s[1] * p ^ {r - 1} + s[2] * p ^ {r - 2} + \dots + s[l - 1] * p ^ {r - l + 1} s[1]∗pr−1+s[2]∗pr−2+⋯+s[l−1]∗pr−l+1 一部分,
而 h u s h [ l − 1 ] = s [ 1 ] ∗ p l − 2 + s [ 2 ] ∗ p l − 3 + ⋯ + s [ l − 1 ] hush[l - 1] = s[1] * p ^ {l - 2} + s[2] * p ^ {l - 3} + \dots + s[l - 1] hush[l−1]=s[1]∗pl−2+s[2]∗pl−3+⋯+s[l−1],
发现只需要给 h u s h [ l − 1 ] hush[l - 1] hush[l−1] 乘以 p r − l + 1 p ^ {r - l + 1} pr−l+1,就可以得到多出的那一部分,
再用 h u s h [ r ] hush[r] hush[r] 减去就可以了。
但由于取模的存在,以上这个式子还要取模。
综上可得:要计算 s [ l … r ] s[l \dots r] s[l…r] 的哈希值就用:
( ( h u s h [ r ] − h u s h [ l − 1 ] ∗ p r − l + 1 ) % m o d + m o d ) % m o d ((hush[r] - hush[l - 1] * p ^ {r - l + 1}) \% mod + mod) \% mod ((hush[r]−hush[l−1]∗pr−l+1)%mod+mod)%mod O ( 1 ) O(1) O(1) 算出。
注:之所以这么写是因为 h u s h [ r ] hush[r] hush[r] 和 h u s h [ l − 1 ] hush[l - 1] hush[l−1] 都是已经被取模过的,也就是说
\space\space\space\space\space h u s h [ r ] − h u s h [ l − 1 ] ∗ p r − l + 1 hush[r] - hush[l - 1] * p ^ {r - l + 1} hush[r]−hush[l−1]∗pr−l+1 可能小于 0 0 0,只有这样才能保证子串哈希值是正的。
如此一来,计算字串的哈希值就轻而易举了。
双值哈希
双值哈希是为了解决哈希冲突而设计的。
简单来说,就是取两个
p
p
p,两个
m
o
d
mod
mod,分别对字符串进行哈希值的计算,
当且仅当两个哈希值相同时,两个字符串才相同。
更具体的:
h
u
s
h
1
[
i
]
=
h
u
s
h
1
[
i
−
1
]
∗
p
1
%
m
o
d
1
hush1[i] = hush1[i - 1] * p1 \% mod1
hush1[i]=hush1[i−1]∗p1%mod1,
h
u
s
h
2
[
i
]
=
h
u
s
h
2
[
i
−
1
]
∗
p
2
%
m
o
d
2
hush2[i] = hush2[i - 1] * p2 \% mod2
hush2[i]=hush2[i−1]∗p2%mod2,
要比较字符串
s
t
r
1
str1
str1
s
t
r
2
str2
str2,
当且仅当:
\space\space
s
t
r
1
str1
str1 的
h
u
s
h
1
[
s
t
r
l
e
n
(
s
t
r
1
)
]
hush1[strlen(str1)]
hush1[strlen(str1)] 等于
s
t
r
2
str2
str2 的
h
u
s
h
1
[
s
t
r
l
e
n
(
s
t
r
2
)
]
hush1[strlen(str2)]
hush1[strlen(str2)] 且
\space\space
s
t
r
1
str1
str1 的
h
u
s
h
2
[
s
t
r
l
e
n
(
s
t
r
1
)
]
hush2[strlen(str1)]
hush2[strlen(str1)] 等于
s
t
r
2
str2
str2 的
h
u
s
h
2
[
s
t
r
l
e
n
(
s
t
r
2
)
]
hush2[strlen(str2)]
hush2[strlen(str2)] 时,
两字符串相等。
这种双值哈希十分可靠,可以直接使用,而且不会被卡掉。
最后一些细节及优化
-
在选定 p p p 和 m o d mod mod 时,两者都最好选一个较大的质数,可以降低哈希的冲突率。
在这里推荐 p p p 取 131 131 131 或 1331 1331 1331 或 1313131 1313131 1313131, m o d mod mod 一般取 23333333 23333333 23333333。 -
更为方便地, m o d mod mod 可以取 2 64 − 1 2^{64} - 1 264−1,即 u n s i g n e d l o n g l o n g unsigned \ long \ long unsigned long long 所能储存的最大
值,然后将 h u s h hush hush 数组开为 u n s i g n e d l o n g l o n g unsigned \ long \ long unsigned long long,这样就可以用 u n s i g n e d l o n g l o n g unsigned \ long \ long unsigned long long 的自然溢出代替取模。 -
如果有多个模式串,一个一个算它们的哈希值和暴力没什么区别。
不如将他们都拼成一个总模式串,整体算一次,要用哪个模式串的哈希值时,直接
用:
( ( h u s h [ p o s [ i ] + s t r l e n ( t [ i ] ) − 1 ] − h u s h [ p o s [ i ] − 1 ] ∗ p s t r l e n ( t [ i ] ) ) % m o d + m o d ) % m o d ((hush[pos[i] + strlen(t[i]) - 1] - hush[pos[i] - 1] * p ^ {strlen(t[i])}) \% mod + mod) \% mod ((hush[pos[i]+strlen(t[i])−1]−hush[pos[i]−1]∗pstrlen(t[i]))%mod+mod)%mod
其中, t [ i ] t[i] t[i] 表示第 i i i 个模式串, p o s [ i ] pos[i] pos[i] 表示第 i i i 个模式串的第一个字符在总
模式串中的下标。 -
在计算 p r − l + 1 p ^ {r - l + 1} pr−l+1 时,可以用快速幂,也可以用一个数组 p o w p i powp_i powpi 预处理好 p p p
的 i i i 次方,注意别忘了取模。