题目来源:AcWing 841. 字符串哈希
一、题目描述
给定一个长度为 n n n 的字符串,再给定 m m m 个询问,每个询问包含四个整数 l 1 , r 1 , l 2 , r 2 l1,r1,l2,r2 l1,r1,l2,r2,请你判断 [ l 1 , r 1 ] [l1,r1] [l1,r1] 和 [ l 2 , r 2 ] [l2,r2] [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数
n
n
n 和
m
m
m,表示字符串长度和询问次数。
第二行包含一个长度为 n n n 的字符串,字符串中只包含大小写英文字母和数字。
接下来 m m m 行,每行包含四个整数 l 1 , r 1 , l 2 , r 2 l1,r1,l2,r2 l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从 1 1 1 开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes
,否则输出 No
。
每个结果占一行。
数据范围
1
≤
n
,
m
≤
1
0
5
1≤n,m≤10^5
1≤n,m≤105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
二、字符串前缀哈希法
我们之前学过KMP字符串匹配算法,但是实际上我们很多字符串的问题可以使用字符串哈希来做。
那么如何求字符串的哈希值呢?
我们将一个字符串看成一个
P
P
P 进制的数,字符串的每一个字符都可以看成
P
P
P 进制下的某一位数字。
例如:对字符串
"
A
B
C
D
"
"ABCD"
"ABCD"进行哈希:
首先,每一个字符可以设计为映射某一个数字,
A
B
C
D
A B C D
ABCD可以看成
(
1234
)
p
=
1
×
p
3
+
2
×
p
2
+
3
×
p
1
+
4
×
p
0
(1234)_p=1×p^3+2×p^2+3×p^1+4×p^0
(1234)p=1×p3+2×p2+3×p1+4×p0,转化成一个数字。但是这样会出现一个问题,如果字符串特别长的话,比如说有10万个字符,那么会导致这个
h
a
s
h
hash
hash值非常大无法存储。因此要对这个结果取模一个较小的数
Q
Q
Q,即
(
1234
)
p
=
(
1
×
p
3
+
2
×
p
2
+
3
×
p
1
+
4
×
p
0
)
%
Q
(1234)_p=(1×p^3+2×p^2+3×p^1+4×p^0) \% Q
(1234)p=(1×p3+2×p2+3×p1+4×p0)%Q,因此这样就可以将一个字符串映射到从
0
0
0~
Q
−
1
Q-1
Q−1之间的一个数了。
注意:
- 字符最好不要映射成为 0 0 0,不然会产生二义性,一般映射的数字先从 1 1 1 开始
- 我们知道传统的 h a s h hash hash 是要设计冲突解决策略的,但是字符串哈希我们假设人品足够好,不会出现冲突。换言之,该方法发生冲突的可能性非常非常非常低!。
- 经验值:
P
P
P 取
131
131
131 或者
13331
13331
13331,
Q
Q
Q 取
2
64
2^{64}
264。
P
P
P、
Q
Q
Q 取成这对数值,则
99.99
%
99.99\%
99.99% 的情况下不会发生冲突。这里有一个技巧,可以用
unsigned long long
类型来存储哈希值,这样在计算时就无需取模。
什么是字符串前缀哈希呢?
假设现在存在一个字符串
s
t
r
=
"
A
B
C
A
B
C
D
E
Y
X
C
A
C
W
I
N
G
"
str = "ABCABCDEYXCACWING"
str="ABCABCDEYXCACWING"
h
[
0
]
=
0
h[0] = 0
h[0]=0
h
[
1
]
=
"
A
"
h[1] = "A"
h[1]="A"的哈希值
h
[
2
]
=
"
A
B
"
h[2] = "AB"
h[2]="AB"的哈希值
h
[
3
]
=
"
A
B
C
"
h[3] = "ABC"
h[3]="ABC"的哈希值
h
[
4
]
=
"
A
B
C
A
"
h[4] = "ABCA"
h[4]="ABCA"的哈希值,以此类推。
用上述的哈希方式,配合上前缀哈希,有什么好处?
好处在于我们可以利用前缀哈希,算出整个字符串任意一个子串的哈希值,类似于前缀和。
因为我们是将字符串看成
P
P
P 进制的数,因此字符串左边是高位,右边是低位。
因此,在我们预处理完所有前缀的哈希值后,就可以用
O
(
1
)
O(1)
O(1) 的时间算出任意一个子串的哈希值。
同时,预处理前缀的哈希值也非常简单,利用如下方式处理即可:
// 字符串前缀哈希预处理
h[0] = 0;
for (int i = 1; i <= n; i++) h[i] = h[i - 1] * P + str[i];
在实际应用中,如果两个子串的哈希值相同,则我们认为这两个子串完全相同。
很多特别困难的字符串题目,都可以用这个方法水掉~。
三、代码
#include <iostream>
using namespace std;
typedef unsigned long long ull;
const int N = 1e5 + 10, P = 131; // P取131或者13331,经验值
char str[N];
int n, m;
// 这里的p[i]的存在是因为在计算时,p的i次方可能经常用到
ull h[N], p[N];
ull get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
// 注意:和前缀和类似,下标从1开始存储
scanf("%d%d%s", &n, &m, str + 1);
p[0] = 1;
for (int i = 1; i <= n; i++)
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + str[i];
}
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;
}