字符串哈希算法
目录
Hash算法自然溢出模板
字符串Hash入门
字符串Hash可以通俗的理解为,把一个字符串当中的每一个字符映射为一个整数。
如果我们通过某种方法,将字符串转换为一个整数,就可以便的确定某个字符串是否重复出现过,这是最简单的字符串Hash应用情景了。
哈希算法不等于哈希表!!!
一个疑问
对于该题,判断两个字串是否相等,为什么不能用头文件 <string> 当中的 substr(begin, pos)呢?这里就涉及到时间复杂度的问题:substr的时间复杂度为 O(n),n是查找字符串的长度,这是一个线性复杂度
substr 源码具体参考:(24条消息) subString源码分析_weixin_30765505的博客-CSDN博客
那么,这里就合理的引出了哈希算法,它对于查找子串操作的时间复杂度仅为 O(1)
Hash算法自然溢出模板
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef unsigned long long ull;
const int N = 1000010, base = 131;
ull h[N]; //h为字符串子串的前缀和
char s[N];
int main()
{
scanf("%s", s + 1); //从第1位开始录入数据方便计算
int len = strlen(s + 1);
p[0] = 1; //不是p[1] = 1,我们要使P[i] = base^i
for(int i = 1; i <= len; i ++ )
{
h[i] = h[i - 1] * base + s[i] - 'a' + 1; //计算哈希子串的前缀和,不要忘记加1
}
for(int i = 1; i <= len; i ++)
printf("%llu\n", h[i]); //llu输出,实现自然溢出
return 0;
}
一些解释:
1.什么叫自然溢出:
(1)当我们赋给无符号类型一个超出它范围的数据,结果是初始值对无符号类型表示数值总数取模后的余数,这就是自然溢出。
(2)当我们赋给带符号类型一个超出它表示范围的值,结果是未定义的。
此时,程序可能继续工作,可能崩溃,也可能产生垃圾数据。
2.如何计算子串前缀和
通过递推的方式推导出公式,一共有两步
h[i] = h[i - 1] * base ;将上一个子串的所有元素往后移动一位,将第一位空出来
h[i] = h[i] +(s[i] - 'a' + 1);添加当前新串多出来的元素,相当于添加在了第一位上
3.关于基数的问题,在将字符串中所有元素的值映射为一个数字值之后,按顺序排列,这时候字符串相当于一个base进制的编码,通常我们采用的是二进制,但是为了确保所有的子串尽可能没有重复的值,即,假如对于一个串:asdsdfg,使得其中一个字串 asd 和 dfg 的哈希值不会相同,实际上我们希望对于任何一个子串它的哈希值都不会相同,所以我们这里有两个基数经验值,131(更常用)和13331
4.开头的 scanf("%s", s + 1); 这里有一点需要注意,在涉及到前缀和的时候,尽量让数组或者字符串从下标 1 开始,这样可以方便计算, 所以 scanf 中为 s+1,这样就可以从字符数组的的一位开始录入数据了
5.一个不容易发现的会导致TLE的错误
代码:for( int i = 1; i <= strlen(s); i ++ )
因为在 for 循环中,每遍历一次,就会调用一次 strlen() 函数,而 strlen() 函数的时间复杂度为O(n),所以说这个 for 循环时间复杂度为 O(n^2) ,有点可怕!
6.关于 llu 和其他的一些类型说明:
(24条消息) %llu 64位无符号%d、%u、%x/%X、%o%f、%e/%E或%g/%G_CSDN博客-CSDN博客
思考
(1)对于该题,有点类似于前缀和的一个应用:求一个区间 [ L,R ] 的值。
在普通前缀和中,直接求 sum[R] - sum[L - 1] 即可,但在本题中,这就刚好进入了一个误区。
例如:假设我们要求区间 sum[ 4, 7 ] 的值,根据上图所示,很显然 sum[7] - sum[3 ]是不等于 sum[ 4, 7 ] 的值的。
(2)因为我们前面说过了,我们把一个字符串通过映射来达到把这个字符串转换成一个base进制编码的问题,其中,位置越往前的元素它的进制的位数越高(即base的指数越大),如上图所示,sum[ 7 ] 中第一个元素a1是p^6,而最后一个元素a7是p^0。
并且我们可以发现一个规律,每一个元素的位数等于它后面所有元素的数量。例如a1后面还有a2到a7一共6个元素,正好等于它的位数6;a7后面没有元素,所以它的位数是0。
也就是说,在元素个数不同的前缀和中,sum[ L , R ]的值是不相同的。所以说前缀和求区间之和的公式不适用于求哈希数组中的区间之和。
(3)但是,我们可以通过修改 原公式:sum[R] - sum[L - 1] 中的 sum[ L - 1 ] 使得sum[ L - 1 ]等于sum[R]当中的sum[ L - 1 ],具体操作就是提高sum[ L - 1 ]中a1,a2......a(L-1) 的位数大小,而这提高的位数,就恰好等于我们所要求的区间的长度(R - L + 1)。
至此,我们就可以得出求哈希前缀和中求一个区间 [ R, L ] 的和的公式了。
公式:sum[ R, L ] = sum[ R ] - sum[L] * p[R - L + 1]
(4)还记得 p 数组嘛?在前面提到过,p[ i ] 是用来保存 base 的 i 次方的一个数组,由这一步求前缀和我们也可以知道为什么p[0] = 1了,因为我们要让p[i] = base的 i 次方,这样就不用考虑+1或者-1的问题了。
AC代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef unsigned long long ull;
const int N = 1000010, base = 131;
ull h[N],p[N];
char s[N];
ull getHash(int l,int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%s", s + 1);
int len = strlen(s + 1);
p[0] = 1;
for(int i = 1; i <= len; i ++ )
{
h[i] = h[i - 1] * base + s[i] - 'a' + 1;
p[i] = p[i-1] * 131;
}
// for(int i = 1; i <= len; i ++)
// printf("%l64u\n", h[i]);
int m; cin >> m;
while(m -- )
{
int l,r,x,y;
cin >> l >> r >> x >> y;
if(getHash(x,y) == getHash(l,r))
cout << "Yes" << endl;
// printf("Yes\n");
else
cout << "No" << endl;
// printf("No\n");
}
return 0;
}
附
该题大佬的视频讲解(前35分钟):icpc.upc.edu.cn/flv/lyd20.html
关于哈希算法的大佬讲解:【算法学习】字符串Hash入门_Pengwill's Blog-CSDN博客_字符串hash
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通