AcWing 841. 字符串哈希

题目来源: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 1n,m105

输入样例:

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 Q1之间的一个数了。

注意

  1. 字符最好不要映射成为 0 0 0,不然会产生二义性,一般映射的数字先从 1 1 1 开始
  2. 我们知道传统的 h a s h hash hash 是要设计冲突解决策略的,但是字符串哈希我们假设人品足够好,不会出现冲突。换言之,该方法发生冲突的可能性非常非常非常低!。
  3. 经验值 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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁头娃撞碎南墙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值