字符串哈希
在前面的字符串匹配问题中,我们介绍了KMP算法, 但是如果判断某两个区间上的子串是否相等,就无法使用KMP算法解决,于是在这里提出了字符串哈希算法
首先我们先看一个例题
【例】
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数 n 和 m,表示字符串长度和询问次数。
第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。
接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从 1 开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤10510^5105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
这一题需要进行m次询问,每次都需要判断两个区间上的子串是否相同
‘我们的处理思路可以如下,我们将这个字符串假设是一个p进制的数,从右到左依次是{p0,p1,...,pnp^0, p^1,..., p^np0,p1,...,pn}

我们定义一个数组,unsigned long long[] hash,存储从第一个字符开始,到第i个字符按照p进制(字符串末尾是p0p^0p0)计算后对某个数q取模的哈希值

那么既然进行了取模操作,那么肯定会出现冲突,我们可以令p = 131或者p = 13331 ,q = 2642^{64}264,这样可以极大减少哈希冲突
肯定会有人问:主播主播,为什么取这两个数?
答:
①131和13331都是质数,质数作为进制基数可以使哈希值分布更均匀,且这两个数是经过大量实践验证的,在处理英文字符串时冲突概率极低
②在使用unsigned long long类型时,计算结果超过2642^{64}264会自动对2642^{64}264取模
这就解释了笔者上文中为什么定义哈希数组为unsigned long long类型的了
可是如果在某些语言中没有定义无符号长整形怎么办,那我们可以用显示的模运算,比如选择一个较大的质数(如109+710^9+7109+7)显式取模
接下来就是如何求某个区间字符串的哈希值了,我们预处理的是从第一个字符到第i个字符这个区间的哈希值,乍一看是不是很像前缀和,但是又和前缀和不太一样,假如在上图中我们要求x的哈希值,我们可以先让zws的哈希值乘p,在让zwsx的哈希值减去zws乘p的值
其实就是高位对齐然后作差,这两个字符串缺多少位就左移多少位
于是得到了这样一个公式
区间[l,r]字符串的哈希值是
(hash[r] - hash[l - 1] * pr−l+1p^{r - l + 1}pr−l+1 % q + q) % q
说明:计算hash[r] - hash[l - 1] * mod[r - l + 1] % q 时,结果可能为负数,因此做了又加上q再%q的操作
如果所使用的语言有无符号长整型这一基本数据类型,可以直接
hash[r] - hash[l - 1] * pr−l+1p^{r - l + 1}pr−l+1
以下是两种语言的代码
c++版
#include <iostream>
#include <cstring>
using namespace std;
typedef unsigned long long ull;
const int N = 100010;
const int p = 131;
int n, m;
ull h[N], P[N];
char str[N];
ull get(int l, int r){
return h[r] - h[l - 1] * P[r - l + 1];
}
int main(){
scanf("%d%d%s",&n, &m, str + 1);
//预处理p[N], p数组存的是p的i次方
P[0] = 1;
for(int i = 1; i <= n; i++){
h[i] = h[i - 1] * p + str[i];
P[i] = P[i - 1] * p;
}
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;
}
java版
import java.io.BufferedReader;
import java.io.InputStreamReader;
class Main {
static int p = 131;
static int N = 100010;
static long[] mod = new long[N];
static long[] hash = new long[N];
static final long MOD = (long) 1e9 + 7; // 取模的值,避免溢出
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String nAndM = br.readLine();
String[] split = nAndM.split(" ");
int n = Integer.parseInt(split[0]);
int m = Integer.parseInt(split[1]);
String str = br.readLine();
// 初始化mod和hash数组
mod[0] = 1;
hash[0] = 0;
//预处理字符串哈希
for (int i = 1; i <= n; i++){
mod[i] = (mod[i - 1] * p) % MOD;
hash[i] = (hash[i - 1] * p + str.charAt(i - 1)) % MOD;
}
while (m-- > 0){
String[] query = br.readLine().split(" ");
int l1 = Integer.parseInt(query[0]);
int r1 = Integer.parseInt(query[1]);
int l2 = Integer.parseInt(query[2]);
int r2 = Integer.parseInt(query[3]);
if (getHash(l1, r1) == getHash(l2, r2)) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
br.close();
}
public static long getHash(int l, int r){
return (hash[r] - hash[l - 1] * mod[r - l + 1] % MOD + MOD) % MOD;
}
}
550

被折叠的 条评论
为什么被折叠?



