字符串哈希算法

目录

字符串Hash入门

一个疑问

Hash算法自然溢出模板​​​​​​​

一些解释:

思考

AC代码


 

例题引入:138. 兔子与兔子 - AcWing题库

字符串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

posted @ 2022-05-05 08:42  光風霽月  阅读(142)  评论(0编辑  收藏  举报