字符串之字符串哈希

字符串之字符串哈希

前言

Hash 函数有助于解决很多问题,如果我们想有效地解决比较字符串的问题,最朴素的办法是直接比较两个字符串,这样做的时间复杂度是 O ( min ⁡ ( n 1 , n 2 ) ) O(\min(n_1,n_2)) O(min(n1,n2)),字符串哈希的想法在于,我们将每个字符串转换为一个整数,然后比较它们而不是字符串。

多项式哈希

哈希函数简单来说就是一个函数 f f f,它将输入映射到另外一个空间,以便于我们的操作。

哈希函数最重要的性质可以概括为下面两条:

  1. 如果两个对象相等,则它们的哈希值相等
  2. 如果两个哈希值相等,则对象很可能相等。

Hash 函数值一样时原字符串却不一样的现象我们成为哈希碰撞

当选择 Hash 函数时,你需要确保碰撞概率尽可能低

对于一个长度为 l l l的字符串 s s s来说,我们可以这样定义多项式 Hash 函数:

h a s h ( s ) = ∑ i = 1 l s [ i ] × p l − i (   m o d   m ) hash(s) = \sum_{i=1}^l s[i] \times p^{l-i}(\bmod m) hash(s)=i=1ls[i]×pli(modm)

更进一步,考虑序列 { a 0 , a 1 , … , a n − 1 } \{a_0,a_1,\dots,a_{n-1}\} {a0,a1,,an1}, 在这个序列从左到右的多项式散列下,我们可以得到多项式 Hash 函数:

h a s h ( a , p , m ) = ( a 0 + a 1 ⋅ p + a 2 ⋅ p 2 + ⋯ + a n − 1 ⋅ p n − 1 )   m o d   m hash(a,p,m) = (a_0 + a_1 \cdot p + a_2 \cdot p^2 + \cdots + a_{n-1} \cdot p^{n-1}) \bmod m hash(a,p,m)=(a0+a1p+a2p2++an1pn1)modm

这里, b b b m m m分别是哈希模块

其中 max ⁡ ( a i ) < p < m , g c d ( p , m ) = 1 \max(a_i) < p < m, gcd(p, m) = 1 max(ai)<p<m,gcd(p,m)=1

O(1)比较时间

为了比较给定序列 { a 0 , a 1 , … , a n − 1 } \{a_0,a_1,\dots,a_{n-1}\} {a0,a1,,an1}的片段,我们需要计算原始序列的每个前缀上的多项式散列。

将前缀上的多项式散列定义为:

p r e f ( k , a , p , m ) = ( a 0 + a 1 ⋅ p + a 2 ⋅ p 2 + ⋯ + a k − 1 ⋅ p k − 1 )   m o d   m pref(k,a,p,m) = (a_0 + a_1 \cdot p + a_2 \cdot p^2 + \cdots + a_{k-1} \cdot p^{k-1}) \bmod m pref(k,a,p,m)=(a0+a1p+a2p2++ak1pk1)modm

我们将 p r e f ( k , a , p , m ) pref(k,a,p,m) pref(k,a,p,m)简要表示为 p r e f ( k ) pref(k) pref(k)

一般形式:

p r e f ( n ) = ( a 0 + a 1 ⋅ p + a 2 ⋅ p 2 + ⋯ + a n − 1 ⋅ p n − 1 ) pref(n) = (a_0 + a_1 \cdot p + a_2 \cdot p^2 + \cdots + a_{n-1} \cdot p^{n-1}) pref(n)=(a0+a1p+a2p2++an1pn1)

每个前缀上的多项式散列可以在 O ( n ) O(n) O(n)时间内计算,使用递推关系:

p r e f ( k + 1 ) = p r e f ( k ) + a k ⋅ p k pref(k+1) = pref(k) + a_k \cdot p^k pref(k+1)=pref(k)+akpk

现在假设我们需要比较两个分别以 a i a_i ai a j a_j aj开头且长度为 l e n len len的子字符串

a i , a i + 1 , ⋯   , a i + l e n − 1 A N D a j , a j + 1 , ⋯   , a j + l e n − 1 a_i,a_{i+1},\cdots,a_{i+len-1} \quad AND \quad a_j,a_{j+1},\cdots,a_{j+len-1} ai,ai+1,,ai+len1ANDaj,aj+1,,aj+len1

考虑 p r e f ( i + l e n ) − p r e f ( i ) pref(i+len)-pref(i) pref(i+len)pref(i) p r e f ( j + l e n ) − p r e f ( j ) pref(j+len)-pref(j) pref(j+len)pref(j)可以得到:

p r e f ( i + l e n ) − p r e f ( i ) = a i ⋅ p i + a i + 1 ⋅ p i + 1 + ⋯ + a i + l e n − 1 ⋅ p i + l e n − 1 pref(i+len) - pref(i) = a_i \cdot p^i + a_{i+1} \cdot p^{i+1} + \cdots + a_{i+len-1} \cdot p^{i+len-1} pref(i+len)pref(i)=aipi+ai+1pi+1++ai+len1pi+len1

p r e f ( j + l e n ) − p r e f ( j ) = a j ⋅ p j + a j + 1 ⋅ p j + 1 + ⋯ + a j + l e n − 1 ⋅ p j + l e n − 1 pref(j+len) - pref(j) = a_j \cdot p^j + a_{j+1} \cdot p^{j+1} + \cdots + a_{j+len-1} \cdot p^{j+len-1} pref(j+len)pref(j)=ajpj+aj+1pj+1++aj+len1pj+len1

接着我们对两个式子进行简单转换,将第一个方程乘以 p j p^j pj,将第二个方程乘以 p i p^i pi。我们得到:

p j ⋅ ( p r e f ( i + l e n ) − p r e f ( i ) ) = p i + j ⋅ ( a i + a i + 1 ⋅ p + ⋯ + a i + l e n − 1 ⋅ p l e n − 1 ) p^j \cdot (pref(i+len) - pref(i)) = p^{i+j} \cdot(a_i + a_{i+1} \cdot p + \cdots + a_{i+len-1} \cdot p^{len-1}) pj(pref(i+len)pref(i))=pi+j(ai+ai+1p++ai+len1plen1)

p i ⋅ ( p r e f ( j + l e n ) − p r e f ( j ) ) = p i + j ⋅ ( a j + a j + 1 ⋅ p + ⋯ + a j + l e n − 1 ⋅ p l e n − 1 ) p^i \cdot (pref(j+len) - pref(j)) = p^{i+j} \cdot(a_j + a_{j+1} \cdot p + \cdots + a_{j+len-1} \cdot p^{len-1}) pi(pref(j+len)pref(j))=pi+j(aj+aj+1p++aj+len1plen1)

可以发现,等式右侧括号中的多项式 Hash 正是我们期望的序列

因此,为了确定所需的序列段是否重合,我们需要检查以下等式

p j ⋅ ( p r e f ( i + l e n ) − p r e f ( i ) ) = p i ⋅ ( p r e f ( j + l e n ) − p r e f ( j ) ) p^j \cdot (pref(i+len) - pref(i)) = p^i \cdot (pref(j+len) - pref(j)) pj(pref(i+len)pref(i))=pi(pref(j+len)pref(j))

这样比较的时间复杂度是 O ( 1 ) O(1) O(1),最后加上 m m m可以得到:

p j ⋅ ( p r e f ( i + l e n ) − p r e f ( i ) )   m o d   m = p i ⋅ ( p r e f ( j + l e n ) − p r e f ( j ) )   m o d   m p^j \cdot (pref(i+len) - pref(i)) \bmod m = p^i \cdot (pref(j+len) - pref(j)) \bmod m pj(pref(i+len)pref(i))modm=pi(pref(j+len)pref(j))modm

我们也可以在等式两边同时乘以 p − i − j p^{-i-j} pij, 最终得到:

p − i ⋅ ( p r e f ( i + l e n ) − p r e f ( i ) )   m o d   m = p − j ⋅ ( p r e f ( j + l e n ) − p r e f ( j ) )   m o d   m p^{-i} \cdot (pref(i+len) - pref(i)) \bmod m = p^{-j} \cdot (pref(j+len) - pref(j)) \bmod m pi(pref(i+len)pref(i))modm=pj(pref(j+len)pref(j))modm

{% note info %}

对于另外一种哈希多项式,这里就不在赘述了,方法是相同的。

h a s h ( a , p , m ) = ( a 0 ⋅ p n − 1 + a 1 ⋅ p n − 2 + ⋯ + a n − 2 ⋅ p + a n − 1 )   m o d   m hash(a,p,m) = (a_0 \cdot p^{n-1} + a_1 \cdot p^{n-2} + \cdots + a_{n-2} \cdot p + a_{n-1} ) \bmod m hash(a,p,m)=(a0pn1+a1pn2++an2p+an1)modm

( p r e f ( i + l e n ) − p r e f ( i ) ⋅ p l e n )   m o d   m = ( p r e f ( j + l e n ) − p r e f ( j ) ⋅ p l e n )   m o d   m (pref(i+len) - pref(i) \cdot p^{len}) \bmod m = (pref(j+len) - pref(j) \cdot p^{len}) \bmod m (pref(i+len)pref(i)plen)modm=(pref(j+len)pref(j)plen)modm
{% endnote %}

计算子串的哈希值

在上面,我们定义了 Hash 函数,单次计算一个字符串的哈希值复杂度是 O ( n ) O(n) O(n), 如果需要多次询问一个字符串的子串的哈希值,每次重新计算效率非常低下。

考虑序列 { a i , a i + 1 , … , a j } \{a_i,a_{i+1},\dots,a_{j}\} {ai,ai+1,,aj}

按照定义我们可以得到

h a s h ( s [ i ⋯ j ] ) = a i + a i + 1 ⋅ p + ⋯ + a j ⋅ p j − i hash(s[i \cdots j]) = a_i + a_{i+1} \cdot p + \cdots + a_{j} \cdot p^{j-i} hash(s[ij])=ai+ai+1p++ajpji

考虑

h a s h ( s [ 0 ⋯ i − 1 ] ) = a 0 + a 1 ⋅ p + ⋯ + a i − 1 ⋅ p i − 1 hash(s[0 \cdots i-1]) = a_0 + a_1 \cdot p + \cdots + a_{i-1} \cdot p^{i-1} hash(s[0i1])=a0+a1p++ai1pi1

h a s h ( s [ 0 ⋯ j ] ) = a 0 + a 1 ⋅ p + ⋯ + a j ⋅ p j hash(s[0 \cdots j]) = a_0 + a_1 \cdot p + \cdots + a_{j} \cdot p^{j} hash(s[0j])=a0+a1p++ajpj

根据前缀和思想,我们可以得到

h a s h ( s [ i ⋯ j ] ) = p − i ⋅ ( h a s h ( s [ 0 ⋯ j ] ) − h a s h ( s [ 0 ⋯ i − 1 ] ) ) hash(s[i \cdots j]) = p^{-i} \cdot (hash(s[0 \cdots j]) - hash(s[0 \cdots i-1])) hash(s[ij])=pi(hash(s[0j])hash(s[0i1]))

{% note info %}

对于多项式哈希的另外一种形式

h a s h ( a , p , m ) = ( a 0 ⋅ p n − 1 + a 1 ⋅ p n − 2 + ⋯ + a n − 2 ⋅ p + a n − 1 )   m o d   m hash(a,p,m) = (a_0 \cdot p^{n-1} + a_1 \cdot p^{n-2} + \cdots + a_{n-2} \cdot p + a_{n-1} ) \bmod m hash(a,p,m)=(a0pn1+a1pn2++an2p+an1)modm

按照同样的方法,我们也可以得到字串的哈希值:

h a s h ( s [ i ⋯ j ] ) = h a s h ( s [ 0 ⋯ j ] ) − h a s h ( s [ 0 ⋯ i − 1 ] ) ⋅ p j − i + 1 hash(s[i \cdots j]) = hash(s[0 \cdots j]) - hash(s[0 \cdots i-1]) \cdot p^{j-i+1} hash(s[ij])=hash(s[0j])hash(s[0i1])pji+1

{% endnote %}

Hash 实现

M = int(1e9 + 7)
B = 233

def get_hash(s):
    res = 0
    for char in s:
        res = (res * B + ord(char)) % M
    return res

def cmp(s, t):
    return get_hash(s) == get_hash(t)

实际上,我们不可能在每次比较字符串时都计算一遍 Hash,这样的效率是低下的。

就像我们之前讨论的, O ( 1 ) O(1) O(1)时间复杂度进行查询

h a s h ( s [ i ⋯ j ] ) = h a s h ( s [ 0 ⋯ j ] ) − h a s h ( s [ 0 ⋯ i − 1 ] ) ⋅ p j − i + 1 hash(s[i \cdots j]) = hash(s[0 \cdots j]) - hash(s[0 \cdots i-1]) \cdot p^{j-i+1} hash(s[ij])=hash(s[0j])hash(s[0i1])pji+1

B = 233
M = 1e9 + 7
h, p = [0] * (n + 1), [0] * (n + 1)
p[0] = 1

def get_pref(s:str):
    for i in range(len(s)):
        h[i + 1] = (h[i] * B + ord(s[i])) % M
        p[i + 1] = (p[i] * B) % M

def find_sub(i:int, j:int) -> int:
    return (h[j] - h[i - 1] * p[j - i + 1]) % M;

最小化碰撞概率

生日问题进行推广: 假设有 n 个人,每一个人都随机地从 N 个特定的数中选择出来一个数(N 可能是 365 或者其他的大于 0 的整数)。p(n)表示有两个人选择了同样的数字,这个概率有多大?

p ≈ 1 − exp ⁡ ( − n 2 2 N ) p \approx 1 - \exp(-\frac{n^2}{2N}) p1exp(2Nn2)

结论:在字符串哈希中,值域需要小到能够快速比较( 1 0 9 10^9 109 1 0 18 10^{18} 1018都是可以快速比较的)。同时,为了降低哈希冲突率,值域也不能太小。

Hash 应用

字符串匹配问题

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

最长回文子串

核心思想:二分答案,判断是否可行时枚举回文中心(对称轴),哈希判断两侧是否相等。需要分别预处理正着和倒着的哈希值。

最长公共子字符串

问题:给定 m m m个总长不超 n n n的非空字符串,查找所有字符串的最长公共子字符串,如果有多个,任意输出其中一个。

很显然如果存在长度为 k k k的最长公共子字符串,那么 k − 1 k-1 k1的公共子字符串也必定存在。因此我们可以二分最长公共子字符串的长度。假设现在的长度为 k k kcheck(k)的逻辑为我们将所有所有字符串的长度为 k k k的子串分别进行哈希,将哈希值放入 n n n个哈希表中存储。之后求交集即可。

练习

参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EmoryHuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值