字符串哈希详解

哈希(Hash)函数是将输入(字符串)映射到输出(哈希值)的函数。

注意:函数名称不要用 hash,C++11 标准库中提供了这个函数。

想偷懒也可以用 unordered_map(底层为 Hash),不过容易被卡。

字符串哈希值分为三种情况:

  1. 单射:每个字符串都拥有唯一的哈希值(不全部用完)。
  2. 满射:每个字符串都有哈希值且哈希值全部用完。
  3. 双射:每个字符串都拥有唯一的哈希值且哈希值全部用完。

双射是最理想的情况,但是几乎不可能出现。

日常生活中通常出现的是单射或满射。

一般的计算公式为:

H a s h ( s ) = ∑ i = 0 n − 1 s i × p n − i − 1   m o d   m Hash(s)=\sum_{i=0}^{n-1} s_i\times p_{n-i-1} \bmod m Hash(s)=i=0n1si×pni1modm

p 通常为一个冷门的偏小的质数(如: 131 , 1151 , 13331 131,1151,13331 131,1151,13331), m m m 一般为一个大质数(不要用 998244353 , 1 0 9 + 7 998244353,10^9+7 998244353,109+7 之类的常见的模数,容易被卡)。

哈希冲突

若两个字符串不同但是哈希函数值相同,则称之为哈希冲突。

最直观的体现就是代码 Wrong Answer。

如何解决?

1:自然溢出

我们都知道,unsigned 类型(无符号整型)仅能存储无符号整型。

我们可以理解为,若数字超出了 2 64 − 1 2^{64}-1 2641 的最大限制,将自动对 2 64 2^{64} 264 取模

注意加粗的那一部分,虽然 2 64 2^{64} 264(为 18446744073709551616 18446744073709551616 18446744073709551616)不是质数,但是因为过于巨大,所以哈希冲突的概率较小。

2:多重哈希

一般上双哈希就够了,不过也有三个甚至以上的,就统称为多重哈希好了。

单哈希即使用自然溢出,也可能会哈希冲突,所以多重哈希相当于又上了一道保险。

一个哈希函数中,两个字符串函数值相同并不代表这两个字符串一定相同,只有用多个哈希函数进行验证,才能降低哈希冲突的概率。

所以多重哈希的判断条件是:无论使用哪个哈希函数判断,得到的哈希值都是相等的。


双哈希例题:P3370 【模板】字符串哈希

单哈希过不了。

因此使用自然溢出+多重哈希。

对于两个哈希函数值,如果两个哈希函数值均不相同,则这时候可以判断这两个字符串不相同。

实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,ans=1;
pair<unsigned long long,unsigned long long>a[10005];
string s[10005];
unsigned long long Hash1(string s){
    unsigned long long base=1129,sum=0;
    for(auto z:s){
        sum=sum*base+z;
    }
    return sum;
}
unsigned long long Hash2(string s){
    unsigned long long base=2287,sum=0;
    for(auto z:s){
        sum=sum*base+z;
    }
    return sum;
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>s[i];
        a[i]={Hash1(s[i]),Hash2(s[i])};
    }
    sort(a+1,a+n+1);
    for(int i=2;i<=n;i++){
        if(a[i]!=a[i-1]){
            ans++;
        }
    }
    cout<<ans;
    return 0;
}

子串哈希

现在我们需要求字符串的某个区间的哈希值。

最直接的方法就是每次询问重新跑,但是时间不允许我们这样做。

把每个阶段的哈希值存储起来(类似于前缀和)。

假设字符串 s = abcd s=\texttt{abcd} s=abcd,则:

h s 0 = 0 h s 1 = ( h s 0 × p ) + a h s 2 = ( h s 1 × p ) + b h s 3 = ( h s 2 × p ) + c h s 4 = ( h s 3 × p ) + d hs_0=0\\ hs_1=(hs_0\times p)+\texttt{a}\\ hs_2=(hs_1\times p)+\texttt{b}\\ hs_3=(hs_2\times p)+\texttt{c}\\ hs_4=(hs_3\times p)+\texttt{d}\\ hs0=0hs1=(hs0×p)+ahs2=(hs1×p)+bhs3=(hs2×p)+chs4=(hs3×p)+d

假设我们截取的字符串叫 bc \texttt{bc} bc

类似于前缀和,此时的哈希值为:

h s r − h s l − 1 × p r − l + 1 hs_r-hs_{l-1}\times p^{r-l+1} hsrhsl1×prl+1


子串哈希例题:P10468 兔子与兔子

因为暴力枚举无法通过本题,所以需要使用字符串哈希。

首先记录下第 i i i 位字符的哈希值,以及 p i p^i pi 的值。

对于每次询问,求出两个区间的哈希值,判断哈希值是否相等即可。

实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,ans=1,m;
unsigned long long base=2341,mod=10000139,hs[1000005],pw[1000005];
string s;
unsigned long long get_Hash(int l,int r){
    return hs[r]-hs[l-1]*pw[r-l+1];
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>s;
    s=' '+s;
    pw[0]=1;
    for(int i=1;i<s.size();i++){
        hs[i]=hs[i-1]*base+s[i];
        pw[i]=pw[i-1]*base;
    }
    for(cin>>m;m;m--){
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        cout<<(get_Hash(l1,r1)==get_Hash(l2,r2)?"Yes":"No")<<'\n';
    }
    return 0;
}

回文哈希

回文哈希可以替代一部分 Manacher 的功能。

回文哈希的作用就截取一个子串,判断这个子串是否为回文串。

若正向得到的哈希值与反向得到的哈希值相等,则这个字符串是回文串。

将需要判断回文的字符串,一分为二。

M 形字符串 为例题。

判断是否为 M 串的条件:

  1. 本身为回文串。
  2. 左端点到中间端点分完之后的字串也是回文串。
实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,ans;
unsigned long long base=2777,hs[200005],hs2[200005],pw[200005];
string s,s1;
unsigned long long get_Hash1(int l,int r){
    return hs[r]-hs[l-1]*pw[r-l+1];
}
unsigned long long get_Hash2(int l,int r){
    return hs2[r]-hs2[l-1]*pw[r-l+1];
}
bool check(int l,int r){
    return (get_Hash1(l,r)==get_Hash2(n-r+1,n-l+1));
}
signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>s;
    s1=s;
    n=s.size();
    reverse(s1.begin(),s1.end());
    s=' '+s,s1=' '+s1;
    pw[0]=1;
    for(int i=1;i<=n;i++){
        hs[i]=hs[i-1]*base+s[i];
        hs2[i]=hs2[i-1]*base+s1[i];
        pw[i]=pw[i-1]*base;
    }
    for(int i=1;i<=n;i++){
        if(check(1,i)&&check(1,(i+1)/2)){
            ans++;
        }
    }
    cout<<ans;
    return 0;
}

矩阵哈希(二维哈希)

我们将 n × m n\times m n×m 的哈希值排列为一个矩阵。

如何求子矩阵的哈希值?

P10474 [ICPC-Beijing 2011] Matrix 矩阵哈希 为例,需要求子矩阵的哈希值:

考虑以行为单位,直接进行一维的哈希处理。

暴力存储所有长度的哈希值。

h s l e n , x , y hs_{len,x,y} hslen,x,y 表示长度为 l e n len len,起点为 ( x , y ) (x,y) (x,y) 的矩阵哈希值。

当前矩阵+新的一行的哈希值为:

原来矩阵的哈希值 × p 行数 − 1 × 列数。 原来矩阵的哈希值\times p_{行数-1}\times 列数。 原来矩阵的哈希值×p行数1×列数。

固定一个长度为 A × B A\times B A×B 的窗口 ( i , j ) (i,j) (i,j)

移动到下一行:

新的矩阵哈希值 = = = 上个矩阵的哈希值 − - 原来第 i i i 行的哈希值 × p A × B   +   \times p_{A\times B}~+~ ×pA×B + 最新一行的哈希值。

有点类似于滑动窗口。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值