【10.24】字符串哈希

定义

我们定义一个把字符串映射到整数的函数 \(f\) ,这个 \(f\) 称为是 Hash 函数。

我们希望这个函数 \(f\) 可以方便地帮我们判断两个字符串是否相等。

\(f\) 值域

在字符串哈希中,值域需要小到能够快速比较(\(10^9\)\(10^{18}\) 都是可以快速比较的)。

同时,为了降低哈希冲突率,值域也不能太小。

性质

哈希值不一样,则字符串一定不一样。

哈希值一样,字符串不一定一样。

常用 Hash 方法

对于一个字符串 \(s\) ,有以下两种常用多项式 Hash 法:

  • \(hash(s)=s[1]*b^{n-1}+...+s[i]*b^{n-i}+...+s[n]\)

  • \(hash(s)=s[1]+...+s[i]*b^{i-1}+...+s[n]*b^{n-1}\)

相较于第二种,第一种方法更常用,且可以类比为一个 \(b\) 进制数。

取模采用 unsigned long long 自然溢出,可以防止出题人卡大模数 Hash 。至于卡自然溢出 Hash ,可以采用双模数 Hash 。

双模数 Hash

顾名思议,双模数 Hash 就是采用两个不同的 Hash 值,只有当两个 Hash 值都相同时才判断字符串相同。

在实际使用时,常常通过变更 \(b\) 的值来实现双 Hash ,注意两个不同的 \(b\) 最好奇怪一点。

快速计算字串 Hash

有的时候题目会要求重复多次计算字符串字串的 Hash 值,这时候如果每次重新计算则会显得很劣,考虑利用前缀和的思想计算字串 Hash ,这里因 Hash 方法而异。

以第一种方法 \(hash(s)=s[1]*b^{n-1}+...+s[i]*b^{n-i}+...+s[n]\) 为例:

定义 \(f_i(s)\) 为字符串 \(s\)\(i\) 位的 Hash ,有:

\(f_i(s)=s[1]*b^{i-1}+...+s[i]\)

由此可得:

\(hash(s[l..r])=f_r(s)-f_{l-1}(s)*b^{r-l+1}\)

因此我们只需预处理 \(f_i(s)\)\(b^k\) ,然后即可 \(O(1)\) 求得字串 Hash 。

示例代码

单模数 Hash

#define MAXN 10004
#define MAXM 1503 //字符串长度
#define ull unsigned long long
int n;
ull Hash[MAXN]; //Hash值
const int b=127;
ull B[MAXM];
void init(){
    B[0]=1;
    for (int i=1;i<=MAXM-2;i++){
        B[i]=B[i-1]*b;
    }
}
int main(){
    init();
    cin>>n;
    for (int i=1;i<=n;i++){
        string s; cin>>s;
        int len=s.length();
        for (int j=0;j<=len-1;j++){
            Hash[i]+=(ull)(s[j]*B[len-j-1]);
        }
    }
    return 0;
}

双模数 Hash

#define MAXN 10003
#define MAXM 1503
#define ull unsigned long long
int n;
pair <ull,ull> Hash[MAXN]; //pair存双Hash值
const int b=127,b2=175;
unsigned long long B[MAXM],B2[MAXM];
void init(){
    B[0]=B2[0]=1;
    for (int i=1;i<=MAXM-2;i++){
        B[i]=B[i-1]*b;
        B2[i]=B2[i-1]*b2;
    }
    return;
}
int main(){
    init();
    cin>>n;
    for (int i=1;i<=n;i++){
        string a; 
        cin>>a;
        int len=a.length();
        for (int j=0;j<=len-1;j++){
            Hash[i].first+=(ull)(a[j]*B[len-j-1]);
            Hash[i].second+=(ull)(a[j]*B2[len-j-1]);
        }
    }
    return 0;
}

快速计算字串 Hash

#define MAXN 10000003
#define ull unsigned long long
int n,m;
char a[MAXN];
const int base=157;
ull B[MAXN];
ull Hash[MAXN]; //Hash前缀
void init(){
    B[0]=1;
    for (int i=1;i<=n;i++){
        B[i]=B[i-1]*base;
        Hash[i]=Hash[i-1]*base+a[i];
    }
    return;
}
int main(){
    cin>>a+1;
    n=strlen(a+1);
    init();
    cin>>m;
    while (m--){
        int l,r,l2,r2; cin>>l>>r>>l2>>r2;
        if (Hash[r]-Hash[l-1]*B[r-l+1]==Hash[r2]-Hash[l2-1]*B[r2-l2+1])
            cout<<"Yes\n";
        else
            cout<<"No\n";
    }
    return 0;
}

例题

P3370【模板】字符串哈希

Code

#include <bits/stdc++.h>
#define MAXN 10003
#define MAXM 1503
#define ull unsigned long long
using namespace std;
int n;
pair <ull,ull> Hash[MAXN];
const int b=127,b2=175;
unsigned long long B[MAXM],B2[MAXM];
void init(){
    B[0]=B2[0]=1;
    for (int i=1;i<=MAXM-2;i++){
        B[i]=B[i-1]*b;
        B2[i]=B2[i-1]*b2;
    }
    return;
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    init();
    cin>>n;
    for (int i=1;i<=n;i++){
        string a; 
        cin>>a;
        int len=a.length();
        for (int j=0;j<=len-1;j++){
            Hash[i].first+=(unsigned long long)(a[j]*B[len-j-1]);
            Hash[i].second+=(unsigned long long)(a[j]*B2[len-j-1]);
        }
    }
    sort(Hash+1,Hash+1+n);
    int ans=0;
    for (int i=1;i<=n;i++){
        if (Hash[i]!=Hash[i-1]) ans++;
    }
    cout<<ans<<endl;
    return 0;
}

P3805【模板】manacher

给出一个只由小写英文字符 \(\text{a,b,c,..,y,z}\) 组成的字符串 \(S\) ,求 \(S\) 中最长回文串的长度 。

字符串长度为 \(n\)

\(1 \leq n \leq 1.1 \times 10^7\)

Solution

manacher板题也可以用 Hash 来做(bushi 。

\(F_i\) 为右端点为 \(i\) 的最长回文串的长度,显然有 \(F_i \leq F_{i-1}+2\)

我们可以枚举右端点 \(i\) ,每次从 \(F_{i-1}+2\) 开始往下枚举答案,然后采用快速计算字串 Hash 的方法判断是否为回文串,总复杂度为 \(O(n)\) 级别,足以通过此题。

复杂度证明:
可以发现从 \(F_0=0\) 开始 ,\(F_n\) 最大为 \(2n\) ,所以在枚举过程中,至多进行 \(2n\) 次 check 。故复杂度为 \(O(n)\) 级别。

Code

Tip:由于空间限制,未使用双模数 Hash 。

#include <bits/stdc++.h>
#define MAXN 11000003
#define ull unsigned long long
using namespace std;
int n;
char a[MAXN],b[MAXN];
const int base=157;
ull B[MAXN];
ull Hasha[MAXN],Hashb[MAXN];
void init(){
    B[0]=1;
    for (int i=1;i<=n;i++){
        B[i]=B[i-1]*base;
        Hasha[i]=Hasha[i-1]*base+a[i];
    }
    for (int i=n;i>=1;i--){
        Hashb[i]=Hashb[i+1]*base+a[i];
    }
    return;
}
int ans[MAXN];
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>a+1;
    n=strlen(a+1);
    init();
    int ANS=0;
    for (int i=1;i<=n;i++){
        int j;
        for (j=ans[i-1]+2;;j--){
            int o=i-j+1;
            if (o<=0) continue;
            int k=i-j/2;
            if (j&1){
                if (Hasha[i]-Hasha[k]*B[i-k]==Hashb[o]-Hashb[k]*B[i-k]) break;
            } else {
                if (Hasha[i]-Hasha[k]*B[i-k]==Hashb[o]-Hashb[k+1]*B[i-k]) break;
            }
        }
        ANS=max(ANS,j);
        ans[i]=j;
    }
    cout<<ANS<<endl;
    return 0;
}

P5546 [POI2000] 公共串

给出几个由小写字母构成的单词,求它们最长的公共子串的长度。

\(1 \leq n \leq 5\)\(1 \leq m \leq 2000\)

Solution

二分公共字串的长度 \(x\) ,把所有长度为 \(x\) 的字串塞到每个串的哈希表里,相同哈希值累加到 \(n\) 即有长度为 \(x\) 的公共字串。

Code

#include <bits/stdc++.h>
#define MAXN 8
#define MAXM 2003
#define ull unsigned long long
using namespace std;
int n;
string s[MAXN];
int len[MAXN];
vector <ull> Hash[MAXN];
const int base=127;
ull B[MAXM];
void init(){
    B[0]=1;
    for (int i=1;i<=MAXM-2;i++){
        B[i]=B[i-1]*base;
    }
    for (int i=1;i<=n;i++){
        Hash[i].push_back(0);
        for (int j=1;j<=len[i];j++){
            Hash[i].push_back(Hash[i][j-1]*base+s[i][j]);
        }
    }
    return;
}
map <ull,bool> mp[MAXN];
map <ull,int> sum;
bool check(int x){
    for (int i=1;i<=n;i++) mp[i].clear();
    sum.clear();
    for (int i=1;i<=n;i++){
        for (int j=1;j<=len[i]-x+1;j++){
            int k=j+x-1;
            ull now=Hash[i][k]-Hash[i][j-1]*B[k-j+1];
            if (!mp[i][now]){
                sum[now]++;
                mp[i][now]=1;
            }
        }
    }
    map <ull,int>::iterator it;
    for (it=sum.begin();it!=sum.end();it++){
        if (it->second==n) return true;
    }
    return false;
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n;
    for (int i=1;i<=n;i++){
        cin>>s[i];
        len[i]=s[i].length();
        s[i]=" "+s[i];
    }
    init();
    int l=0,r=2003,ans=0;
    while (l<=r){
        int mid=(l+r)>>1;
        if (check(mid)) l=mid+1,ans=mid;
        else r=mid-1;
    }
    cout<<ans<<endl;
    return 0;
}

参考资料

posted @   EcapsXD  阅读(21)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示