字符串哈希

字符串哈希从入门到精通

前言

字符串哈希是处理字符串匹配、子串查询等问题的利器。它的核心思想是将字符串映射为一个整数,从而将字符串比较转化为整数比较,将 O(n) 的比较降为 O(1)。

本文通过三道模板题,带你从零掌握字符串哈希。


一、哈希原理

1.1 基本概念

把一个字符串看作一个 B 进制数

hash(s) = s[0] * B^(n-1) + s[1] * B^(n-2) + ... + s[n-1]

其中 B 是一个大于字符集大小的数(通常取 131 或 13331)。

1.2 为什么用 unsigned long long

利用无符号整数自然溢出取模(相当于对 2^64 取模),省去取模运算,速度更快。

1.3 预处理

const int N=1e6+10;
unsigned long long hs[N], pk[N];
int k = 131;

// 预处理
pk[0] = 1;
for(int i=1;i<=n;i++){
    hs[i] = hs[i-1] * k + s[i];  // 前缀哈希
    pk[i] = pk[i-1] * k;          // k 的幂
}

// 子串哈希
ull gethash(int l,int r){
    return hs[r] - hs[l-1] * pk[r-l+1];
}

二、模板题:P3370 字符串哈希

题目

给定 n 个字符串,求不同字符串的个数。

思路

对每个字符串求哈希值,排序后去重。

代码

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const ull N=1e6+10;
ull k=131;
ull hs[N];
ull as[N];

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin>>n;
    string s;
    for(int op=1;op<=n;op++){
        cin>>s;
        hs[0]=0;
        int len=s.size();
        s=" "+s;
        for(int i=1;i<=len;i++){
            hs[i]=hs[i-1]*k+s[i];
        }
        as[op]=hs[len];
    }
    sort(as+1,as+1+n);
    int cnt=1;
    for(int i=1;i<n;i++){
        if(as[i]!=as[i+1]) cnt++;
    }
    cout<<cnt;
    return 0;
}

关键点

  • 每个字符串独立计算哈希,存放在 as[] 数组中
  • 排序后统计不同值

三、兔子与兔子:P10468

题目

给定一个字符串,多次询问两个子串是否相同。

思路

预处理前缀哈希和 k 的幂,O(1) 查询子串哈希。

代码

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const ull N=1e6+10;
ull k=131;
ull hs[N], pk[N];

ull gethash(ull l,ull r){
    return hs[r]-hs[l-1]*pk[r-l+1];
}

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    string s;
    cin>>s;
    ull len=s.size();
    s=" "+s;
    pk[0]=1;
    for(ull i=1;i<=len;i++){
        hs[i]=hs[i-1]*k+s[i];
        pk[i]=pk[i-1]*k;
    }
    ull m;
    cin>>m;
    while(m--){
        ull l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(gethash(l1,r1)==gethash(l2,r2)){
            cout<<"Yes\n";
        }else{
            cout<<"No\n";
        }
    }
    return 0;
}

关键点

  • pk[i] 预处理 k^i
  • 子串哈希公式:hs[r] - hs[l-1] * pk[r-l+1]

四、匹配统计:P10479

题目

给定文本串 a 和模式串 b,对 a 的每个后缀,求其与 b 的最长公共前缀(LCP)长度。最后回答若干次询问,输出 LCP 长度等于 x 的后缀个数。

思路

  1. 预处理 ab 的哈希
  2. 二分 LCP 长度(单调性:若前 mid 个字符匹配,则更短的也匹配)
  3. 统计每个 LCP 长度的出现次数

代码

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int N=2e5+10;
ull k=131;
int n,m,q;
string a,b;
ull hs1[N],hs2[N],pk[N];

ull gethash1(int l,int r){
    return hs1[r]-hs1[l-1]*pk[r-l+1];
}
ull gethash2(int l,int r){
    return hs2[r]-hs2[l-1]*pk[r-l+1];
}

int lcp(int i){
    int l=1,r=min(n-i+1,m),res=0;
    while(l<=r){
        int mid=(l+r)>>1;
        if(gethash1(i,i+mid-1)==gethash2(1,mid)){
            res=mid;
            l=mid+1;
        }else{
            r=mid-1;
        }
    }
    return res;
}

int cnt[N];

int main(){
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m>>q;
    cin>>a>>b;
    a=" "+a;
    b=" "+b;
    pk[0]=1;
    for(int i=1;i<=n;i++){
        hs1[i]=hs1[i-1]*k+a[i];
        pk[i]=pk[i-1]*k;
    }
    for(int i=1;i<=m;i++){
        hs2[i]=hs2[i-1]*k+b[i];
    }
    for(int i=1;i<=n;i++){
        int len=lcp(i);
        cnt[len]++;
    }
    while(q--){
        int x;
        cin>>x;
        if(x>m) cout<<"0\n";
        else cout<<cnt[x]<<"\n";
    }
    return 0;
}

关键点

  • 二分 LCP 时,mid 是长度,用哈希判断 a[i..i+mid-1] 是否等于 b[1..mid]
  • cnt[x] 统计 LCP 长度恰好为 x 的后缀个数

五、常见问题与避坑指南

5.1 哈希冲突

自然溢出(unsigned long long)在绝大多数情况下足够安全。若想更保险,可以使用双哈希(两个不同的进制或模数)。

5.2 进制选择

  • 常用 13113331
  • 进制应大于字符集大小(字母数字 128 足够)

5.3 下标处理

字符串建议从 1 开始,避免 0 的边界问题:

s = " " + s;

5.4 幂数组

pk[i] 需要预处理到字符串长度,否则子串哈希公式会出错。


总结

题目 核心技巧 复杂度
P3370 字符串哈希 + 排序去重 O(n L log n)
P10468 前缀哈希 O(1) 子串查询 O(n + m)
P10479 哈希 + 二分 LCP O(n log m + q)

掌握这三道题,字符串哈希的基本应用就过关了。核心在于理解 hash 的进制表示和 子串哈希 的计算公式。

posted @ 2026-07-08 16:18  diaitor  阅读(5)  评论(0)    收藏  举报