字符串哈希

下面介绍的字符串Hash函数把一个任意长度的字符串映射成一个非负整数,并且其冲突概率几乎为零。

取一固定值P,把字符串看作P进制数,并分配一个大于0的数值,代表每种字符。 一般来说,我们分配的数值都远小于P。例如,对于小写字母构成的字符串,可以令 a = 1 , b = 2 , . . . , z = 26 。 a=1,b=2,…,z=26。 a=1,b=2,...,z=26 取一固定值M,求出该P进制数对M的余数,作为该字符串的Hash值。

一般来说,我们取P=131或P=13331,此时Hash值产生冲突的概率极低,只要Hash值相同,我们就可以认为原字符串是相等的。通常我们取 M = 2 64 M=2^{64} M=264,即直接使用unsigned long long类型存储这个Hash值,在计算时不处理算术溢出问题,产生溢出时相当于自动对 2 6 4 2^64 264取模,这样可以避免低效的取模运算。

除了在及特殊构造的数据上,上述Hash很难产生冲突,一般情况下上述Hash算法完全可以出现在题目的标准解答中。我们还可以多取一些恰当的P和M值(例如大质数),多进行几组Hash运算,当结果都相同时才认为原字符串相等,就更难以构造出使这个Hash产生错误的数据。

对字符串的各种操作,都可以直接对P进制数进行算数运算反映到Hash值上。

如果我们已知字符串S的Hash值为H(S),那么在S后添加一个字符c构成的新字符串S+c的Hash值就是 H ( S + c ) = ( H ( S ) ∗ P + v a l u e [ c ] ) m o d    M H(S+c) = (H(S)*P +value[c]) \mod M H(S+c)=(H(S)P+value[c])modM。其中乘P就相当于P进制下的左移运算,value[c]是我们的为c选定的代表数值。

如果我们已知字符串S的Hash值为H(S),字符串S+T的Hash值为 H ( S + T ) H(S+T) H(S+T),那么字符串T的Hash值 H ( T ) = ( H ( S + T ) − H ( S ) ∗ P l e n g t h ( T ) ) m o d    M H(T) = (H(S+T)-H(S)*P^{length(T)})\mod M H(T)=(H(S+T)H(S)Plength(T))modM。这就相当于通过P进制下在S后边补0的方式,把S左移到与S+T的左端对其,然后二者相减就得到了H(T)。

例如,S=“abc”,c=“d”,T=“xyz”,则:
S表示为P进制数: 1 2 3
H ( S ) = 1 ∗ P 2 + 2 ∗ P + 3 H(S) = 1*P^2+2*P+3 H(S)=1P2+2P+3
H ( S + c ) = 1 ∗ P 3 + 2 ∗ P 2 + 3 ∗ P + 4 = H ( S ) ∗ P + 4 H(S+c) = 1*P^3+2*P^2+3*P+4=H(S)*P+4 H(S+c)=1P3+2P2+3P+4=H(S)P+4
S+T表示为P进制数: 1 2 3 24 25 26
H ( S + T ) = 1 ∗ P 5 + 2 ∗ P 4 + 3 ∗ P 3 + 24 ∗ P 2 + 25 ∗ P + 26 H(S+T) = 1*P^5+2*P^4+3*P^3+24*P^2+25*P+26 H(S+T)=1P5+2P4+3P3+24P2+25P+26
S在P进制下左移length(T) 位: 1 2 3 0 0 0
二者相减就是T表示为P进制数: 24 25 26
H ( T ) = H ( S + T ) − ( 1 ∗ P 2 + 2 ∗ P + 3 ) ∗ P 3 = 24 ∗ P 2 + 25 ∗ P + 26 H(T)=H(S+T)-(1*P^2+2*P+3)*P^3=24*P^2+25*P+26 H(T)=H(S+T)(1P2+2P+3)P3=24P2+25P+26

根据上面两种操作,我们可以通过O(N)的时间预处理字符串所有前缀Hash值,并在O(1)的时间内查询它的任意子串的Hash值。

例题:兔子与兔子 CH1401

很久很久以前,森林里住着一羣兔子。有一天,兔子们想要研究自己的DNA序列。我们首先选取一个好长好长的DNA序列(小兔子是外星生物,DNA序列可能包含26个小写英文字母),然后我们每次选择两个区间,询问如果用两个区间里的DNA序列分别生产出来两只兔子,这两只兔子是否一模一样。注意两只兔子一模一样只可能是它们的DNA序列一模一样。 1 ≤ l e n g t h ( s ) , Q ≤ 1 0 6 1\le length(s),Q \le 10^6 1length(s),Q106

记我们选取的DNA序列为S,

点赞