字符串哈希

字符串哈希

在前面的字符串匹配问题中,我们介绍了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}prl+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}prl+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;
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值