数据结构与算法学习笔记----字符串哈希

数据结构与算法学习笔记----字符串哈希

@@ author: 明月清了个风

@@ first publish: 2024.12.4

字符串哈希(string hash)

字符串哈希和上一篇的整数哈希一样,通过将字符串映射到一个数字来表示该字符串,只是对于字符串来说,这个哈希函数映射的方法会更特殊。

实现原理(多项式哈希)

基本的思想是通过将字符串中的每个字符映射到一个数字,通常使用ASCII码值,通过加权求和的方式计算出该字符串的哈希值

假设字符串 S S S S = s 0 s 1 s 2 . . . s n − 1 S = s_0 s_1 s_2 ... s_{n-1} S=s0s1s2...sn1,则其哈希值可以通过以下多项式进行计算:

h ( S ) = ( s 0 ⋅ p n − 1 + s 1 ⋅ p n − 2 + s 2 ⋅ p n − 3 + . . . + s n − 1 ⋅ p 0 )    m o d    Q (1) h(S) = (s_0 \cdot p^{n - 1} + s_1 \cdot p^{n - 2} + s_2 \cdot p^{n - 3} + ... + s_{n-1} \cdot p^0 ) \; mod \; Q \tag{1} h(S)=(s0pn1+s1pn2+s2pn3+...+sn1p0)modQ(1)

其中:

  • s i s_i si字符串中的第 i i i个字符的数值,可以是ASCII码,也可以是别的方法进行计算(比如减去字符a的值)
  • p p p是一个常数,通常选取为131,上式括号中的部分就是将字符串 S S S看做一个 p p p进制的数转换为十进制的结果
  • Q Q Q是一个模数,选取为 2 64 2^{64} 264,也就是unsigned long long的范围,这样数值越界就可以直接当做取模了

前缀哈希

字符串哈希的目的就是为了能够快速判断子串的性质,比如是否存在,那如何快速求出子串的哈希值就很关键。

对于字符串 S S S,我们通过前缀哈希构造一个哈希数组h[],其中h[i]表示字符串 S S S从第1个字符到第i个字符的哈希值,哈希值可以由以下公式得到:

h [ i ] = ( h [ i − 1 ] × p + s i ) m o d Q (2) h[i] = (h[i - 1] \times p + s_i) mod Q \tag{2} h[i]=(h[i1]×p+si)modQ(2)

式中各值含义与上述相同,其中h[0] = 0,表示空字符串的哈希值。

那么我们要获取区间[l, r]子串的哈希值就可以通过类似前缀和的思想进行计算,利用以下公式:

h ( s [ l , r ] ) = ( h [ r ] − h [ l − 1 ] ∗ p r − l + 1 ) m o d Q (3) h(s[l, r]) = (h[r] - h[l - 1] * p^{r-l+1}) mod Q \tag{3} h(s[l,r])=(h[r]h[l1]prl+1)modQ(3)

解释:每个从第1个字符开始的子串都被当预处理成了一个p进制的数,那么对于子串 s [ 1 , r ] s[1, r] s[1,r] s [ 1 , l − 1 ] s[1,l - 1] s[1,l1]来说,他们转化为10进制数后为:

D e c ( s [ 1 , r ] ) = s 1 ⋅ p r − 1 + s 2 ⋅ p r − 2 + s 3 ⋅ p r − 3 + . . . + s r ⋅ p 0 (4) Dec(s[1, r]) = s_1 \cdot p^{r - 1} + s_2 \cdot p^{r - 2} + s_3 \cdot p^{r - 3} + ... + s_{r} \cdot p^0 \tag{4} Dec(s[1,r])=s1pr1+s2pr2+s3pr3+...+srp0(4)
D e c ( s [ 1 , l − 1 ] ) = s 1 ⋅ p l − 2 + s 2 ⋅ p l − 3 + s 3 ⋅ p l − 4 + . . . + s l − 1 ⋅ p 0 (5) Dec(s[1, l - 1]) = s_1 \cdot p^{l - 2} + s_2 \cdot p^{l - 3} + s_3 \cdot p^{l - 4} + ... + s_{l - 1} \cdot p^0 \tag{5} Dec(s[1,l1])=s1pl2+s2pl3+s3pl4+...+sl1p0(5)

可以看出,两者的公共部分子串 s [ 1 , l − 1 ] s[1, l - 1] s[1,l1]差了 p r − l + 1 p^{r - l + 1} prl+1倍,因此,式(5)乘上 p r − l + 1 p^{r - l + 1} prl+1就相当于将两者的 s [ 1 , l − 1 ] s[1, l - 1] s[1,l1]部分对齐,再将式(4)-式(5)即可得式(3)括号部分。

AcWing 841. 字符串哈希

[原题链接](841. 字符串哈希 - AcWing题库)

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

输入格式

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

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

接下来 m m m行,每行包含四个整数 l 1 , r 1 , l 2 , r 2 l_1,r_1,l_2,r_2 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

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

输出格式

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

数据范围

1 ≤ n , m ≤ 1 0 5 1 \leq n,m \leq 10^{5} 1n,m105,

代码

#include 
#include 

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;

char str[N];
int n, m;
ULL h[N], p[N];

ULL gets(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    cin >> 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;
        cin >> l1  >> r1 >> l2 >> r2;
        if(gets(l1, r1) == gets(l2, r2)) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    
}

你可能感兴趣的:(数据结构与算法笔记(基础课),哈希算法,学习,笔记)