AcWing 841. 字符串哈希(哈希表)

题目链接 :点击查看

题目描述 :

给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2]这两个区间所包含的字符串子串是否完全相同。字符串中只包含大小写英文字母和数字。

输入输出格式 :

输入

第一行包含整数 n 和 m,表示字符串长度和询问次数。

第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。

接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 1 开始编号。

输出

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No

每个结果占一行。

输入输出样例 :

输入

8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2

输出

Yes
No
Yes

题目分析 :

字符串哈希本质就是将字符串的前缀和转换为P进制整数的形式,对于每一个前缀,其转换成为的P进制整数即为此前缀字符串的哈希值。因此我们要对比两子串是否相同,只需要比较两子串的hash值即可。那么怎么将字符串转化成为P进制整数呢?比如字符串"abcd"我们可以把其拆成四位字符'a' 'b' 'c' 'd', 以 a为高位,因为每个字符都有相应的ASCII码,因此字符串的hash值为‘’ASCII('a') * p ^ 3 + ASCII('b') * p ^ 2 + ASCII('c') * p ^ 1 + ASCII('d') * p ^ 0。由前面的前缀和知识可以推知,以第i位字符为结尾的前缀其哈希值h[i] = h[i - 1] * P + str[i];(h[i]为整型数组,这里str[i]会自动转换为相应字符的ASCII码)。初始化完字符串每个前缀的哈希值,然后我们来看看怎么计算字符串l ~ r区间子串的hash值,由之前的前缀和知识可知,若要计算l ~ r 的前缀和,算出s[r] - s[l - 1]即可,计算l~r的hash值可以类比于此。在之前初始化的过程,我们已经计算出了h[r]的值与h[l - 1]的值,但是h[l, r]并不是二者直接相减,且看上述字符串"abcd"其中 a是高位,d为最低位,所以对于h[r]与h[l - 1]其中的h[0]反倒是哈希值的高位,又由前缀和可知,两者的高位相同,所以由于h[l - 1]值比h[r]的值短,我们要先将h[l - 1]与h[r]中数高位(左端)进行对齐, 具体做法为将h[l - 1]向左移动r - l + 1位。比如十进制数"1230“与“10”其中第一个数中的1与第二个数的1都是表示千位,所以将“10” 乘 “100”得“1000”,"1230"再与“1000”相减。此操作的代码为 h[r] - h[l - 1] * P ^ (l - r + 1)。为了方便上述的移位操作,我们在初始化h数组时同时用p数组记录P的n次幂,即p[i] = P ^ i  = p[i - 1]  * P,且p[0] = 1,所以P^ (l - r + 1) = p[ l - r + 1]。特别地,由于hash问题常常伴随hash冲突,在这里P可以取131或13331,而且每一次求得的hash值为了保证范围(在某一区间),都要进行取模运算,即将hash{i] mod Q, 由经验值可知,P取131||13331 且 Q取 2 ^ 64时,哈希冲突的概率最小,此时可以忽略此问题。关于将hash[i]取模问题,在这里我们可以将h数组定义为unsigned long long类型,如若hash值大于等于2^64直接从0开始,相当于取模功能。详见如下代码。

代码 :

#include<iostream>
#include<cstdio>
using namespace std;
typedef unsigned long long ULL;//减少2^64取模环节 
const int N = 1e5 + 7, P = 131;//经验值 
int n, m;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r) {
	return h[r] - h[l - 1] * p[r - l + 1];//将h[l - 1]与h[r]左端对齐后再进行相减,即为当前字符串的哈希值 
} 
int main() {
	cin >> n >> m;
	cin >> str + 1; //下标从1开始
	p[0] = 1;
	for (int i = 1; i <= n; i ++ ) {
		h[i] = h[i - 1] * P + str[i];
		p[i] = p[i - 1] * P; //p的每个位置,都存相应的p^i值,方便之后的移位运算 
	} 
	while (m -- ) {
		int l1, r1, l2, r2;
		cin >> l1 >> r1 >> l2 >> r2;
		if (get(l1, r1) == get(l2, r2)) cout << "Yes" << endl;
		else cout << "No" << endl; 
	}
	return 0;
} 

-----------------------------------------------------------------------------------

下面给出字符串哈希的相关模板

核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

在森林中麋了鹿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值