浅谈哈希

· · 算法·理论

本期题单

前言:

最近几天 Clare613 我又复习了一下哈希,这篇文章算是对这几天的总结。本期题目难度分为为入门和进阶,有搬来的题目和原创题,这里会全部讲完。

哈希简介:

何为哈希:

Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射 pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

有何作用:

依靠本人的理解,就是优化了字符串时间复杂度高的问题,因为在同样的代码中,字符串在使用过程中,可能会多一倍 \mathcal{O}(n),最后 TLE。\ 同样,哈希也不是万能的,有可能会发生哈希冲突,即不同的字符串所对应的数字相同了。

实现方法:

关于哈希的实现方法,这里我用 ClassIn 画了下图,将就着看吧。\ 哈希分为三类,如下图:\ 这是我们三种哈希,很明显,只有单哈希是切实可行的。当然,后面会提到“双哈希”,但不是现在写的双哈希。\ 有人肯定会问:为什么只写单哈希?原因很简单,因为满哈希冲突率高,而双哈希又要用 map,浪费了时间,没有用处,所以我们以单哈希为主。\ 有人肯定又要问了,单哈希怎么实现?我们主要是将字符串转为 P 进制。怎么转?我们都学过进制转换,代码如下:

#include<bits/stdc++.h>
using namespace std;
long long d=1,sum=0;
int main(){
    string a;
    cin>>a;
    for(int i=a.size()-1;i>=0;i--)
    {
        if(a[i]=='1'){
            sum+=d;
        }
        d*=2;
    }
    cout<<sum;
    return 0;
}

以上为二进制转十进制,我们现在是字符串转 P 进制,但道理是一样的,如下:

for(int i=1;i<=n;i++){
    a[i]=a[i-1]*13331+x[i];
    pw[i]=pw[i-1]*13331;
}

而转换的结果便是:\ \ 接下来便是至关重要的问题:我们如果只选取子串该怎么查?我们可以发现,我们假设字符串为:abcde,那么 a_i 的值如下:\ \ 假设我们现在要求 bcd 的值,那么就可以按如下图的方法求:\ \ 上图也讲了 l~r 的方法,下面就不赘述了。

例题精讲:

这是最长的一部分,如果有人自己本来就会做,就可以跳过了。这里的两部分都有各有五道题,一共十道题,我挑选了其中几道较为重要的题目进行讲解。

P3370 【模板】字符串哈希:

这是最简单一道题目,可以说 map 轻松秒杀,但我们需要用哈希来写,于是得到了这个代码:

#include<bits/stdc++.h> 
using namespace std;
int q1=131,q2=13331;
const int mod=999823;
int a[1000005];
int b[1000005];
int hash1(string x){
    int sum=0;
    for(int i=0;i<x.size();i++){
        sum=sum*q1+x[i];
        sum%=mod;
    }
    return sum%mod;
}
int main(){
    int n,ans=0;
    cin>>n;
    for(int i=1;i<=n;i++){
        string x;
        cin>>x;
        if(a[hash1(x)]==1) continue;
        ans++;
        a[hash1(x)]=1;
    }
    cout<<ans;
    return 0;
}

不难发现,只有 63 分。难道是卡哈希了?并不是,我们需要用“双哈希”,也就是两个哈希,这样可以加强准确率。于是,用这个代码就可以 AC 了:

#include<bits/stdc++.h> 
using namespace std;
int q1=131,q2=13;
const int mod=999823;
int a[1000005];
int b[1000005];
int hash1(string x){
    int sum=0;
    for(int i=0;i<x.size();i++){
        sum=sum*q1+x[i];
        sum%=mod;
    }
    return sum%mod;
}
int hash2(string x){
    int sum=0;
    for(int i=0;i<x.size();i++){
        sum=sum*q2+x[i];
        sum%=mod;
    }
    return sum%mod;
}
int main(){
    int n,ans=0;
    cin>>n;
    for(int i=1;i<=n;i++){
        string x;
        cin>>x;
        if(a[hash1(x)]==1&&b[hash2(x)]==1) continue;
        ans++;
        a[hash1(x)]=1;
        b[hash2(x)]=1;
    }
    cout<<ans;
    return 0;
}

P10468 兔子与兔子:

这一道题是我们遇到的第一个求子串的题目,按照前面的方法,其实就没有什么难度了,代码如下:

#include<map>
#include<iostream>
#define int unsigned long long
using namespace std;
int a[1000005],pw[1000005];
int hashh(int l,int r){
    return a[r]-a[l]*pw[r-l];
}
signed main(){
    string x;
    cin>>x;
    int n,m=x.size();
    cin>>n;
    x=" "+x;
    pw[0]=1;
    for(int i=1;i<=m;i++){
        a[i]=a[i-1]*13331+x[i];
        pw[i]=pw[i-1]*13331;
    }
    for(int i=1;i<=n;i++){
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(hashh(l1,r1)==hashh(l2,r2)) cout<<"Yes\n";
        else cout<<"No\n";
    }
    return 0;
}

P1481 魔族密码:

这一道题有很多做法,比如说这些大佬。我们的做法是哈希,由于是前缀,所以我们只需要将整体先记录,然后根据经过的数量记录一下,取最大值即可:

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
map<int,int> mp;
string x[2005];
int a[105],b[2005],pw[105];
int hash1(int l,int r){
    return a[r]-a[l-1]*pw[r-l+1];
}
bool cmp(string x,string y){
    if(x.size()!=y.size()) return x.size()<y.size();
    return x<y;
}
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    cout.tie(0)->sync_with_stdio(0);
    int n;
    cin>>n;
    pw[0]=1;
    int ans=0;
    for(int i=1;i<=n;i++){
        cin>>x[i];
    }
    sort(x+1,x+n+1,cmp);
    for(int i=1;i<=n;i++){
        int m=x[i].size();
        x[i]=" "+x[i];
        int sum=0;
        for(int j=1;j<=m;j++){
            a[j]=a[j-1]*13331+x[i][j];
            pw[j]=pw[j-1]*13331;
            sum+=mp[a[j]];
        }
        mp[a[m]]++;
        ans=max(ans,sum);
    }
    cout<<ans+1;
    return 0;
}

P10915 [蓝桥杯 2024 国 B] 最长回文前后缀:

这一道题我们要注意它的性质:单调性。不难发现,前后缀越短越好找到符合条件的。我们又可以发现,它只能删除一个子串,那么我们先去除本身的前后缀,然后假设端点为 l,r 时,会有以下两种情况:\ \ 也就是说,我们对此写两个二分,取一下最大值就可以 AC 了,要注意前后缀的颠倒。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[1000005],b[1000005],pw[1000005];
int l,r,n;
int hash1(int l,int r){
    return a[r]-a[l-1]*pw[r-l+1];
}
int hash2(int l,int r){
    return b[l]-b[r+1]*pw[r-l+1];
}
bool check(int x){
    for(int i=l;i<=r-2*x+1;i++){
        if(hash1(i,i+x-1)==hash2(r-x+1,r)) return 1;
    }
    return 0;
}
bool chick(int x){
    for(int i=l+2*x-1;i<=r;i++){
        if(hash1(l,l+x-1)==hash2(i-x+1,i)) return 1;
    }
    return 0;
}
signed main(){
    string x;
    cin>>x;
    int n=x.size();
    l=1,r=n;
    x=" "+x;
    while(l<=r){
        if(x[l]==x[r]){
            l++;
            r--;
        }
        else{
            break;
        }
    }
    if(l>r){
        cout<<n/2+1;
        return 0;
    } 
    else{
        pw[0]=1;
        for(int i=l;i<=r;i++){
            a[i]=a[i-1]*13331+x[i];
        }
        for(int i=r;i>=l;i--){
            b[i]=b[i+1]*13331+x[i];
        }
        for(int i=1;i<=n;i++){
            pw[i]=pw[i-1]*13331; 
        }
        int ll=0,rr=(r-l+1)/2;
        while(ll<rr){
            int mid=(ll+rr+1)/2;
            if(check(mid)) ll=mid;
            else rr=mid-1;
        }
        int q=ll;
        ll=0,rr=(r-l+1)/2;
        while(ll<rr){
            int mid=(ll+rr+1)/2;
            if(chick(mid)) ll=mid;
            else rr=mid-1;
        }
        cout<<max(l+q-1,l+ll-1);
    }
    return 0;
}

P10474 [ICPC-Beijing 2011] Matrix 矩阵哈希:

这是从我写的这道题的题解迁来的,你们可以去那道题看我的题解。

思路:

一道很好的哈希题,关键在于怎么写。其实我们可以发现,哈希一般都是一维的,而现在发现竟然是二维的。很明显,我们需要将二维的矩阵转为一维。\ 怎么转呢?我们先看一下样例,按表格来画像这样:\ \ 很明显,虽然一整个矩阵哈希值不好求小矩阵的哈希值,但我们可以对于每一行来求一次,使得每一行都为单独的一个,经过计算可以得到如下的 a_{i,j} 的表格:\ \ 接着,假设我现在要求矩阵左上角为 (1,1),右下角为 (2,2) 的小矩形,我们就可以用第一行的前两个和第二行的前两个加起来,如下:\ \ 那么现在我们再看一下题,它要我们求的是是否存在,也就是说我们需要全部找一遍。但是可以发现,如果每一个都单独算的话会超时,那么我们如何转移?\ 转移的关键其实是我们的上下两行,比如说上图的 1,3 行,其实可以直接转下来,也就是变成这样:\ \ 其中红色表示删除部分。那么删除这一行时,就要注意它多乘了多少个 P,要删除掉。再次看一下题目,可以发现它的子矩阵大小是固定的,于是我们就需要预处理一下,不然会超时,然后就没有什么了。

code:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[1005][1005],pw[1000005];
int hashh(int id,int l,int r){
    return a[id][r]-a[id][l-1]*pw[r-l+1];
}
map<int,int> f;
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    cout.tie(0)->sync_with_stdio(0);
    int n,m,A,B;
    cin>>n>>m>>A>>B;
    pw[0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            char x;
            cin>>x;
            a[i][j]=a[i][j-1]*13331+x;
            pw[(i-1)*m+j]=pw[(i-1)*m+j-1]*13331;
        }
    }
    for(int i=1;i<=m-B+1;i++){
        int num=0;
        for(int j=1;j<=A;j++){
            num=num*pw[B]+hashh(j,i,i+B-1);
        }
        f[num]=1;
        for(int j=A+1;j<=m;j++){
            num=num-pw[(A-1)*B]*hashh(j-A,i,i+B-1);
            num=num*pw[B]+hashh(j,i,i+B-1);
            f[num]=1;
        }
    }
    int q;
    cin>>q;
    while(q--){
        int s=0;
        for(int i=1;i<=A;i++){
            for(int j=1;j<=B;j++){
                char x;
                cin>>x;
                s=s*13331+x;
            }
        }
        if(f[s]==1) cout<<"1\n";
        else cout<<"0\n";
    }
    return 0;
}

相似的题目:P4398 [JSOI2008] Blue Mary的战役地图。

P3501 [POI 2010] ANT-Antisymmetry:

简单题,留做练习。

T628207 编码(数据已加强):

这是最杂的一道题,其实本质上是二分+线段树+哈希,这道题留做挑战题,难度在 S 组的 T3。

#include<bits/stdc++.h>
#define unsigned long long
#define ls(x)(x<<1)
#define rs(x)(x<<1|1)
using namespace std;
const int NA=1e6+5;
int n,m;
string s;
int pw[NA];
int sum[NA<<2];
void giup(int x,int l,int r){
    sum[x]=sum[ls(x)]*pw[r-(l+r)/2]+sum[rs(x)];
    return ;
}
void mt(int x,int l,int r){
    if(l==r){
        sum[x]=s[l];
        return ;
    }
    int mid=(l+r)/2;
    mt(ls(x),l,mid);
    mt(rs(x),mid+1,r);
    giup(x,l,r);
    return ;
}
void mn(int q,char w,int x,int l,int r){
    if(l==q){
        sum[x]=w;
        return ;
    }
    int mid=(l+r)/2;
    if(q<=mid) mn(q,w,ls(x),l,mid);
    if(q>mid) mn(q,w,rs(x),mid+1,r);
    giup(x,l,r);
    return ;
}
int ans(int ll,int rr,int x,int l,int r){
    if(ll>rr) return 0;
    if(ll<=l&&r<=rr) return sum[x];
    int mid=(l+r)/2;
    if(rr<=mid) return ans(ll,rr,ls(x),l,mid);
    else if(ll>mid) return  ans(ll,rr,rs(x),mid+1,r);
    return ans(ll,rr,ls(x),l,mid)*ans(ll,rr,rs(x),mid+1,r);
}
void init(){
    pw[0]=1;
    for(int i=1;i<=n;i++){
        pw[i]=pw[i-1]*13331;
    }
    return  ;
}
int main(){
    cin>>n>>m>>s;
    s=" "+s;
    init();
    mt(1,1,n);
    while(m--){
        int op;
        cin>>op;
        if(op==1){
            int x;
            char y;
            cin>>x>>y;
            mn(x,y,1,1,n);
        }
        else{
            int l1,r1,l2,r2;
            cin>>l1>>r1>>l2>>r2;
            if(r1-l1!=r2-l2){
                cout<<"NO\n";
                continue;
            }
            int l=0,r=r1-l1+1;
            while(l+1<r){
                int mid=(l+r)/2;
                if(ans(l1,l1+mid-1,1,1,n)==ans(l2,l2+mid-1,1,1,n)) l=mid;
                else r=mid;
            }
            if(ans(l1,l1+l-1,1,1,n)+ans(l1+l+1,r1,1,1,n)==ans(l2,l2+l-1,1,1,n)+ans(l2+l+1,r2,1,1,n)) cout<<"YES\n";
            else cout<<"NO\n";
        }
    }
    return 0;
}

后话:

如果觉得写的好的话请回复 Clyyds。