字符串Hash入门+例题详解

最近打暑假多校,发现有许多字符串算法自己有所遗忘,今天就借着补题在这开一个坑,把那几个基础的字符串算法总结复习一下,顺便写几个模板,供今后使用。这篇博客主要就是总结一下字符串Hash,并提一下例题。

简介


我们学习一个算法,肯定要先知道它要解决的是什么问题。字符串Hash,就是解决 字符串匹配问题的良药,即寻找长度为 n 的主串 S 中的匹配串 T(长度为 m)出现的位置或次数问题。
对于上述问题,朴素的想法是枚举 S 所有起始位置,再直接检查是否匹配,我们可以不使用 O(m) 的直接比较字符串的方法,而是 比较长度为 m 的主串 S 的子串的哈希值与 T 的哈希值是否相等,这就是哈希算法解决这类问题的原理,这个原理称为字符串 Hash。
大多数字符串 Hash 问题可以用 KMP 求解,但如果是从主串中每次选出两个子串判断是否匹配的问题,还是要用字符串 Hash 求解(不必纠结,遇到自然懂)。

实现方法


如果我们用 O(m) 的时间计算长度为 m 的字符串的哈希值,则总的时间复杂度并没有改观,这里就需要一个叫做 滚动哈希的优化技巧。
我们 选取两个合适的互质常数 b 和 h(b<h),假设字符串 C=c 1c 2…c m ,那么我们定义哈希函数: H(C)=(c1bm-1 + c2bm-2 + … + cmb0) mod h。
正常的数字是十进制的,这里 b 是基数,相当于把字符串看作是 b 进制数。
这一过程是 递推计算的,设 H(C,k) 为前 k 个字符构成的字符串的哈希值,则:(以下均不考虑取模的情况)。
H(C,k+1) = H(C,k)*b + ck+1
如字符串 C=“ACDA”(为方便处理,我们令"A"表示 1,"B"表示 2,以此类推),则:
H(C,1) = 1
H(C,2) = 1*b + 3
H(C,3) = 1*b 2 + 3*b + 4
H(C,4) = 1*b 3 + 3*b 2 + 4*b + 1

代码实现如下

    H[0]=0;
    for(int i=1;i<=4;i++)
    H[i]=H[i-1]*b+(C[i]-'A'+1);

通常,题目要求的是判断主串的一段字符与另一个匹配串是否匹配,即判断字符串 C=c1c2…cm 从位置 k+1 开始的长度为 n 的子串 C’=ck+1ck+2…ck+n 的哈希值与另一匹配串 S=s1s2…sn 的哈希值是否相等,则:
H(C’) = H(C,k+n) - H(C,k)*bn
于是我们只要预处理求得 b^n ,就能在 O(1)时间内得到任意字符串的子串哈希值,从而完成字符串匹配,那么上述字符串匹配问题的算法时间复杂度就为 O(n+m)。

代码实现如下

  for(int i=0;i<=m-n;++i)
    {
        ull hash=hashC[i+n]-hashC[i]*pow[n];
        if(hash==hashS)
        ans++;
    }

如字符串 C=“ACDA”,S=“CD”,当 k=1, n=2 时:
H(C’) = H(C,1+2) - H(C,1)*b2
= (1*b2+3*b+4) - (1*b2)
= 3*b + 4
H(S) = 3*b + 4
因此子串 C’ 与匹配串 S 匹配。
在实现算法时,可以利用 32 位或 64 位无符号整数计算哈希值,此时 h=232 或 h=264,通过自然溢出省去求模运算。(因为无符号整数,大于最大值后会以最大值+1为模数取模)
注1:众所周知,hash算法有时会产生冲突,但是在一般比赛中用字符串Hash产生冲突的概率是很小的,如果发现错了,可以换个基数或模数,或者采用“双哈希”来避免冲突。
注2:我们在预处理 b^n 时,要根据题目来选择预处理方式(虽然一般不会卡这个),如果只有一组输入,匹配串长度固定,那么利用快速幂求即可。如果有多组输入,每组匹配串长度皆不同,递推更好一些。
递推代码

    pow[0]=1;
    for(int i=1;i<=10002;i++)//预处理base^n
    pow[i]=pow[i-1]*base;

样例分析


Oulipo(POJ3461)
题目大意
给出两个串 S 1,S 2(只有大写字母),求 S 1 在 S 2 中出现了多少次。例如 S 1=“ABA”,S 2=“ABABA”,答案为 2。输入 T 组数据,对每组数据输出结果。每组数据保证 strlen(S 1) <= 10 4,strlen(S 2) <= 10 6
解题思路
将匹配串 S 1 的哈希值求出来,再将母串 S 2 的哈希值求出来,根据 H(C’) = H(C,k+n) - H(C,k)*b n求出与匹配串长度相等的母串子串的哈希值,与匹配串 S 1 的哈希值比较,如果相等,答案+1。
代码实现

/**
快速幂预处理,取模
跑了 813MS
*/
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=1e6+5;
const int maxm=1e4+5;
const int base=29;
const long long mod=1e7+5;
int n;
char W[maxm],T[maxn];
long long quickpow(long long a,int b)//快速幂
{
    long long sum=1;
    while(b>0)
    {
        if(b&1)
        sum=(sum*a)%mod;
        b>>=1;
        a=(a*a)%mod;
    }
    return sum;
}
int main()
{
    scanf("%d",&n);
    while(n--)
    {
        scanf("%s",W);
        scanf("%s",T);
        int lenw=strlen(W);
        int lent=strlen(T);
        long long hashw=0;//初始hash
        long long hasht=0;
        int ans=0;//统计个数
        for(int i=0;i<lenw;++i)
        {
            hashw=(hashw*base+1ll*(W[i]-'A'+1))%mod;
            hasht=(hasht*base+1ll*(T[i]-'A'+1))%mod;
        }
        if(hashw==hasht)
        ans++;
        long long cnt=quickpow((long long)base,lenw-1);//预处理b^n
        for(int i=lenw;i<lent;++i)//T向后找子串去和W比较
        {
            hasht=(hasht-1ll*cnt*(T[i-lenw]-'A'+1))%mod;
            hasht=(hasht+mod)%mod;//防止为负数
            hasht=(hasht*base+1ll*(T[i]-'A'+1))%mod;
            if(hashw==hasht)
            ans++;
        }
        printf("%d\n",ans);
    }
    return 0;
}
/**
递推预处理,未取模
跑了 235MS
*/
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <cstring>
#include <algorithm>
typedef unsigned long long ull;
using namespace std;
const int maxn=1e6+5;
const int maxm=1e4+5;
const int base=29;
ull pow[maxm];
char W[maxm],T[maxn];
ull hasht[maxn];
int n;
ull hashs;
int ans;
void init()
{
    ans=0;
    hashs=0;
}
int main()
{
    pow[0]=1;
    for(int i=1;i<=10002;i++)//预处理base^n
    pow[i]=pow[i-1]*base;
    scanf("%d",&n);
    while(n--)
    {
        init();//初始化
        scanf("%s",&W);
        scanf("%s",&T);
        int lens=strlen(W);
        int lent=strlen(T);
        for(int i=1;i<=lens;++i)
        hashs=hashs*base+(ull)(W[i-1]-'A'+1);
        hasht[0]=(ull)(T[0]-'A'+1);
        for(int i=1;i<=lent;++i)
        hasht[i]=hasht[i-1]*base+(ull)(T[i-1]-'A'+1);
        for(int i=0;i<=lent-lens;++i)
        {
            ull hash=hasht[i+lens]-hasht[i]*pow[lens];
            if(hash==hashs)
            ans++;
        }
        printf("%d\n",ans);
    }
    return 0;
}

总结


字符串Hash,在比赛中还是经常出现的,因为用map可能会tle或者mle。在一些题目里,是作为某一关键的步骤。所以,多练多用,才能熟能生巧,在比赛中灵活运用。

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值