字符串哈希

引例

题目描述

给定一个字符串 A A A 和一个字符串 B B B,求 B B B A A A 中的出现次数。 A A A B B B 中的字符均为英语大写字母或小写字母。

A A A 中不同位置出现的 B B B 可重叠。

输入格式

输入共两行,分别是字符串 A A A 和字符串 B B B

输出格式

输出一个整数,表示 B B B A A A 中的出现次数。

样例输入

zyzyzyz
zyz

样例输出

3

数据范围与提示

1 ≤ A , B 1 \leq A, B 1A,B 的长度 ≤ 1 0 6 \leq 10 ^ 6 106 A A A B B B 仅包含大小写字母。

暴力求解思路

逐一枚举 A A A 中的位置 i i i 作为 B B B 的起点,检查是否可以匹配,时间复杂度为 O(n2),显然会超时。

一、进制

通过对各种进制的观察,我们不难发现:

  • 任意一个 R R R 进制的数,都可以看成是一个满足如下条件的字符串:
    • 每个位上都是 [ 0 , R − 1 ] [0, R-1] [0,R1] 之间的一个数字;
    • 两个字符串相等,当且仅当这两个字符串代表的 R R R 进制数相等。
  • 判断两个字符串相等,需要一层循环,是 O(n) 的,而判断两个数相等,是 O(1) 的。
  • 所有英文字母的取值范围都在 128 以内,因此,每个英文字母均可以看成是一个 R ( R > = 128 ) R(R>=128) R(R>=128) 进制数的基数值,任意一个字符串均可看作有一个或多个位的 R R R 进制数
    H ( " a b c d " ) = 97 ⋅ R 3 + 98 ⋅ R 2 + 99 ⋅ R + 100 H ( " a b " ) = 97 ⋅ R + 98 H ( " c d " ) = 99 ⋅ R + 100 = H ( a b c d ) − H ( a b ) ⋅ R 2 \begin{aligned} H("abcd") &= 97\cdot R^3+98\cdot R^2+99\cdot R+100 \\ H("ab") &= 97\cdot R+98 \\ H("cd") &= 99\cdot R+100=H(abcd)-H(ab)\cdot R^2 \end{aligned} H("abcd")H("ab")H("cd")=97R3+98R2+99R+100=97R+98=99R+100=H(abcd)H(ab)R2

不难看出,在已知某个字符串的所有前缀的 R R R 进制数值的前提下,计算任意一个子串的 R R R 进制数值只需 O(1) 的时间(当然还需要预处理出 R i R^i Ri 的值)。

至此,对于上面的题目,我们可以:

  1. B B B 转为一个 R R R 进制数 h b hb hb,时间复杂度为 O(n)。
  2. 逐一枚举 A A A 中的位置 i i i,预处理出 A A A 的前 i i i 位构成的 R R R 进制数的数值 h [ i ] h[i] h[i],时间复杂度为 O(n)。
  3. 逐一枚举 A A A 中的位置 i i i,用 O(1) 的时间 A A A 中从第 i i i 个位开始的与 B B B 相同的一个字符串对应的 R R R 进制数 h a ha ha,检查是否满足 h b = = h a hb==ha hb==ha

按照这个思路,整个算法的时间复杂度就降到了 O(n),可以通过了。

但是等等,这里好像有一个问题:由于 R R R 是大于等于 128 的数, R i R^i Ri 很容易就会超出 i n t int int 甚至 l o n g   l o n g long\ long long long 的取值范围,我们根本无法存储。而如果采用大整数来运算及存储,就得不偿失了。

那该怎么办呢?

我们遇到了一个取值范围远大于表示范围的对应问题,就如同关键字与位置下标的对应问题,要将取值范围非常大的一组数(字符串的 R R R 进制数值),尽量没有冲突地均匀存入一个空间有限的数组(基础变量类型的取值范围)中,这是标准的散列问题

二、散列

设计这种散列函数一定要简单且快,通常采用经典的“除留余数法”,为了减少冲突,我们需要做 2 件事情:

  • 要让余数的取值范围尽量大(采用最大的数据类型 unsigned long long,相当于模 264)。
  • R R R 选取一个大于 128 的素数,例如:131,13331 等等。

H ( " a b c d " ) = 97 × 13 1 3 + 98 × 13 1 2 + 99 × 131 + 100 = 218064827 + 1681778 + 12969 + 100 = 219746605 \begin{aligned} H("abcd") &= 97\times 131^3+98\times 131^2+99\times 131+100\\ &=218064827+1681778+12969+100\\ &=219746605 \end{aligned} H("abcd")=97×1313+98×1312+99×131+100=218064827+1681778+12969+100=219746605
那么,上面为什么没有去模 264 呢?因为 unsigned long long 本身恰好就是 64 位,它计算出来的结果本来就是只保留小于 264 的部分,这称作自然溢出

好啦!到此为止,我们就完成了真个算法设计,看看代码吧!

#include <iostream>
#include <cstring>
using namespace std;
using ULL = unsigned long long;
const int N = 1e6 + 7, P = 131;
ULL sum[N], sa, pw[N];
char s[N];
int main() {
	scanf("%s", s + 1);
	pw[0] = 1;
	int len = strlen(s + 1);
	for (int i = 1; s[i]; ++i) {
		sum[i] = sum[i-1] * P + s[i];
		pw[i] = pw[i-1] * P;
	}
	scanf("%s", s + 1);
	int lena = strlen(s + 1), ans = 0;
	for (int i = 1; s[i]; ++i)
		sa = sa * P + s[i];
	for (int i = 1; i+lena-1 <= len; ++i) {
		ULL d = sum[i+lena-1] - sum[i-1]*pw[lena];
		if (d == sa) 
			++ans;
	}
	printf("%d", ans);
	return 0;
}

三、遗留问题

我们都知道散列一定会出现冲突的,理论上一定存在两个不同字符串的散列值相同,对策有两条:

  • 仅用散列判断两个字符串不同,即若两个字符串的散列值不同,那它们一定是两个不同的字符串。
  • 当两个字符串的散列值相同时,可以采用以下两种策略之一:
    • 双哈希,即再用另一个素数计算以下散列,看看是否相同。
    • 直接用循环判断以下字符串是否相同。

四、拓展问题

在字符串匹配问题中,经常需要用判断一个串在另一个串中的匹配次数,例如下面的题目:

题目描述

给定若干个长度为 ≤ 1 0 6 \le 10^6 106 的由可见字符构成的字符串,询问每个字符串最多是由多少个相同的子字符串重复连接而成的。如:ababab 则最多有 3 3 3ab 连接而成。

输入格式

输入若干行,每行有一个字符串。特别的,字符串可能为 . 即一个半角句号,此时输入结束。

样例输入

abcd
aaaa
ababab
.

样例输出

1
4
3

数据范围与提示

字符串长度 ≤ 1 0 6 \le 10^6 106

枚举思路

直接枚举前缀子串的长度(长度显然是总长度的一个因子),检查是否能够重复覆盖整个字符串,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

有没有一种办法,可以直接判断出一个字符串 a a a 是否可以通过重复连接构成另一个字符串 b b b 呢?

仔细观察下图,长为 10 的字符串,abcdefgABCDEFG 都是 char 类型变量,各自代表对应位置的一个字符。

如果其前 7 7 7 个字符构成的前缀与最后 7 7 7 个字符构成的后缀能够匹配,即 abcdefg=ABCDEFG,是否就说明整个字符串可以用前 3 3 3 个字符重复连接构成?

12345678910
***abcdefg
ABCDEFG

证明
首先,对应位置的字符分别相等,即 abcd=DEFG
又由 abcdefg=ABCDEFG,可知 abc=ABC
由此可得 abc=ABC=DEF=defEFG=efg=bcd=bca,即该字符串为 abcabcabca,得证。

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
字符串哈希算法是一种将字符串映射为数字的算法,常用于字符串的比较和匹配。在C++中,可以使用字符串哈希算法来加速字符串的比较操作。 引用\[1\]中的代码示例展示了一个使用字符串哈希算法的C++代码。该代码使用了前缀和数组和字符串数组来存储字符串,并通过计算哈希值来比较两个子串是否相等。其中,哈希值的计算使用了前缀和数组和幂运算。 引用\[2\]中的解释指出,使用字符串哈希的目的是为了比较字符串时不直接比较字符串本身,而是比较它们对应映射的数字。这样可以将子串的哈希值的时间复杂度降低到O(1),从而节省时间。 引用\[3\]中的代码示例也展示了一个使用字符串哈希算法的C++代码。该代码使用了前缀和数组和字符串数组来存储字符串,并通过计算哈希值来比较两个子串是否相等。与引用\[1\]中的代码类似,哈希值的计算也使用了前缀和数组和幂运算。 综上所述,字符串哈希算法是一种将字符串映射为数字的算法,常用于字符串的比较和匹配。在C++中,可以使用前缀和数组和幂运算来计算字符串哈希值,并通过比较哈希值来判断两个子串是否相等。 #### 引用[.reference_title] - *1* [C++算法题 # 33 字符串哈希](https://blog.csdn.net/weixin_44536804/article/details/123425533)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [字符串哈希(c++)](https://blog.csdn.net/qq_41829492/article/details/120980055)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [AcWing 841. 字符串哈希(C++算法)](https://blog.csdn.net/YSA__/article/details/108453403)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值