核心思想
考虑定义一个函数
f
f
f,从字符串映射到整数。并且我们希望这个函数
f
f
f 可以在某些方面上帮助我们。
比如:快速判断两个字符串是否相等?
对于任意两个字符串
s
1
s_1
s1 和
s
2
s_2
s2,哈希函数
f
f
f 具有如下性质:
- 若 f ( s 1 ) ≠ f ( s 2 ) f(s_1) \neq f(s_2) f(s1)=f(s2),则 s 1 ≠ s 2 s_1 \neq s_2 s1=s2;
- 若 f ( s 1 ) ≠ f ( s 2 ) f(s_1) \neq f(s_2) f(s1)=f(s2),则大概率可能 s 1 = s 2 s_1 = s_2 s1=s2,但若此时 s 1 ≠ s 2 s_1 \neq s_2 s1=s2,则称哈希函数 f f f 在 s 1 s_1 s1 和 s 2 s_2 s2 上出现了哈希碰撞(简称碰撞)。(当然我们总是希望不出现哈希碰撞。)
经典哈希函数
朴素的算法
通常使用的哈希函数
h
(
s
)
=
∑
i
−
1
n
s
[
i
]
×
b
n
−
i
(
m
o
d
M
)
h(s) = \sum_{i-1}^n s[i] \times b^{n-i} \pmod M
h(s)=i−1∑ns[i]×bn−i(modM)
其中,
b
b
b 为任意正整数,
M
M
M 为一个大素数(
M
≥
∣
Σ
∣
M \ge |\Sigma|
M≥∣Σ∣)。
代码实现(效率低下版本,但是我好像不会其他写法了 T-T):
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M=998244353;
const int b=233;
int h(const string &s)
{
int res=0;
for (int i=0;i<=s.length()-1;i++)
{
res=((ll)res*b+s[i])%M;
}
return res;
}
bool cmp(const string &s1,const string &s2)
{
return h(s1)==h(s2);
}
减少碰撞?
根据公式,我们可以得到哈希函数
h
(
s
)
h(s)
h(s) 的值域为
[
0
,
M
−
1
]
[0,M - 1]
[0,M−1],大小为
M
M
M。假定我们现在一共有
n
n
n 个字符串,则不出现哈希碰撞的概率为:
P
=
∏
i
=
0
n
−
1
M
−
i
M
P=\prod_{i=0}^{n-1} \frac{M - i}{M}
P=i=0∏n−1MM−i
代入数据,
M
=
998244353
,
n
=
1
0
6
M = 998244353, n=10^6
M=998244353,n=106,在随机数据下表现优秀。
但倘若我们为了更加安全,可是使用双哈希的技术,也就是对两个模数分别取模,这样的话可以扩大
h
(
s
)
h(s)
h(s) 的值域,减少出现碰撞的概率。
加速?
单次计算哈希函数的时间复杂度为
O
(
n
)
O(n)
O(n)(
n
n
n 为字符串长度),与暴力计算无异。当我们需要多次计算哈希函数时,这种算法的效率便显得低下。
一般考虑采取计算前缀的方法(此处
s
s
s 为
1
1
1 开头)。我们考虑计算
h
(
s
[
1
⋯
i
]
)
h(s[1 \cdots i])
h(s[1⋯i]) 的值,按照定义有:
h
(
s
[
1
⋯
i
]
)
=
s
[
1
]
×
b
i
−
1
+
s
[
1
]
×
b
i
−
2
+
⋯
+
s
[
i
−
1
]
×
b
+
s
[
i
]
(
m
o
d
M
)
h(s[1 \cdots i]) = s[1] \times b^{i-1} + s[1] \times b^{i-2} + \cdots + s[i - 1] \times b + s[i] \pmod M
h(s[1⋯i])=s[1]×bi−1+s[1]×bi−2+⋯+s[i−1]×b+s[i](modM)
类似于前缀和,我们考虑计算
h
(
s
[
l
⋯
r
]
)
h(s[l \cdots r])
h(s[l⋯r]) 的值。易得:
h
(
s
[
l
⋯
r
]
)
=
s
[
l
]
×
b
r
−
l
+
s
[
l
+
1
]
×
b
r
−
l
−
1
+
⋯
+
s
[
r
−
1
]
×
b
+
s
[
r
]
(
m
o
d
M
)
h(s[l \cdots r]) = s[l] \times b^{r - l} + s[l + 1] \times b^{r - l - 1} + \cdots + s[r-1] \times b + s[r] \pmod M
h(s[l⋯r])=s[l]×br−l+s[l+1]×br−l−1+⋯+s[r−1]×b+s[r](modM)
观察一下,得到:
h
(
s
[
l
⋯
r
]
)
=
h
(
s
[
1
⋯
r
]
)
−
h
(
s
[
1
⋯
l
−
1
]
)
×
b
r
−
l
+
1
h(s[l \cdots r]) = h(s[1 \cdots r]) - h(s[1 \cdots l-1])\times b^{r-l+1}
h(s[l⋯r])=h(s[1⋯r])−h(s[1⋯l−1])×br−l+1
由此,我们可以
O
(
n
)
O(n)
O(n) 地预处理出
b
r
−
l
+
1
b^{r-l+1}
br−l+1 的值,查询哈希值时
O
(
1
)
O(1)
O(1) 根据公式计算即可。
应用
允许 k k k 次失配的字符串匹配
给定长为
n
n
n 的文本串
s
s
s 和长为
m
m
m 的模式串
p
p
p,求出
s
s
s 中有多少子串与
p
p
p 匹配。注意:在本题中,我们称
s
′
s'
s′ 和
s
s
s 匹配,当且仅当
∣
s
′
∣
=
∣
s
∣
|s'| = |s|
∣s′∣=∣s∣ 且至多有
k
k
k 个字符不同。其中
1
≤
n
,
m
≤
1
0
6
,
0
≤
k
≤
5
1 \le n,m \le 10^6, 0 \le k \le 5
1≤n,m≤106,0≤k≤5。
考虑使用哈希 + 二分解决。我们枚举所有可能匹配的子串
s
′
s'
s′,通过哈希 + 二分快速找到
s
′
s'
s′ 与
p
p
p 失配的第一个位置,之后删除
s
′
s'
s′ 以及
p
p
p 在失配位置之前的字符串,继续查找下一个失配位置。这个过程至多发生
k
k
k 次。
时间复杂度
O
(
m
+
k
n
log
m
)
O(m+kn \log m)
O(m+knlogm)。
最长公共子串
给定
m
m
m 个长度为
n
n
n 的字符串,求出所有字符串的最长公共子串,若有多个,任意输出其中一个。 其中
1
≤
m
,
n
≤
1
0
6
1 \le m,n \le 10^6
1≤m,n≤106。
子串具有一个递推性质。即如果长度为
x
x
x 的公共子串存在,则长度为
x
−
1
x-1
x−1 的公共子串一定存在。由此我们可以二分所求子串的长度,假设当前长度为
c
u
r
cur
cur,则 check(cur)
的逻辑就是所有字符串的所有长度为
c
u
r
cur
cur 的子串进行哈希,然后放入
n
n
n 个不同的哈希表中,最后求交集。
时间复杂度
O
(
n
log
n
m
)
O(n \log \frac{n}{m})
O(nlogmn)。