字符串前缀哈希法
有很多字符串的问题可以用字符串哈希来做,不一定要用KMP算法。
这里的哈希方式是一些比较特殊的哈希方式,即字符串前缀哈希。比如有一个字符串abcde
,则可以先预处理出来所有前缀的哈希,比如h[1]
就表示a
的哈希,h[2]
就表示ab
的哈希,特别的,定义h[0]=0
表示前0个字符的哈希值为0
。
要定义某一个前缀的哈希值,只要把字符串看成是一个
P
P
P进制的数,那么每一位的ASCII码就表示这一位的数字是多少。那么上面的例子的字符串哈希值(即对应的十进制的数值)为:
a
×
P
4
+
b
×
P
3
+
c
×
P
2
+
d
×
P
1
+
e
×
P
0
a \times P^4 + b \times P^3 + c \times P^2 + d \times P^1 + e \times P^0
a×P4+b×P3+c×P2+d×P1+e×P0
转化成数字之后这个数字可能非常大,所以要取模一下,一般是对
2
64
2^{64}
264取模,在C++里就只需要直接用unsigned long long
来存储就行了,就不需要真正的写取模这个动作了。
需要注意:
- 对字符串的每一位字符都不应该映射成
0
,假如把一个字符A
映射成0
了,那么AA
映射过来就也是0
,就不对了。 - 在哈希数字的时候可能会存在冲突,这个字符串哈希方法是不处理冲突的,经验上一般取 P = 131 P=131 P=131或者 P = 13331 P=13331 P=13331
使用这种方式,可以利用前缀哈希来求得每一个子串的哈希值,如果要求字符串下标从L
到R
的哈希值,只需要知道1..L-1
的哈希值h[L-1]
,1..R
的哈希值h[R]
。其实要计算的L..R
的哈希值就是这段数的
P
P
P进制表示,所以其实就是在
P
P
P进制的意义上把1..L-1
这个数左移到和1..R
对齐,然后再用1..R
的值给它减掉就行了。
因为取模运算对加减乘都是同态的,所以用取模后的结果来计算,再取模得到的结果也一定是正确的:
H
a
s
h
[
L
.
.
R
]
=
(
H
a
s
h
[
R
]
−
H
a
s
h
[
L
−
1
]
×
P
R
−
L
+
1
+
2
64
)
m
o
d
2
64
Hash[L..R] = (Hash[R] - Hash[L-1] \times P^{R-L+1} + 2^{64}) \ \ mod \ \ 2^{64}
Hash[L..R]=(Hash[R]−Hash[L−1]×PR−L+1+264) mod 264
因为直接用unsigned long long
来存,所以这里的加模再取模也是没必要做的了。
模板题
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5 + 10;
const int P = 131; // 表示成P进制数
ULL h[N], p[N]; // 哈希值和P的次幂
char str[N]; // 字符串,从1开始标号
ULL get(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
int main() {
int n, m;
scanf("%d%d%s", &n, &m, str + 1);
// 计算P的次幂和哈希值
p[0] = 1;
for (int i = 1; i <= n; i ++ ) {
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + str[i];
}
// 处理m此询问
while (m -- ) {
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
// 比较区间内子串的哈希值是否相等
if (get(l1, r1) == get(l2, r2))
puts("Yes");
else
puts("No");
}
return 0;
}