详解HASH(字符串哈希)
HASH意为(散列),是OI的常⽤算法。
我们常⽤哈希的原因是,hash可以快速(⼀般来说是O(段长))的求出⼀个⼦段的hash值,然后就可以快速的判断两个串是否相同。
今天先讲string类的hash。
可以发现,与⼀个string有关的HASH值不仅仅跟每个字符的个数有关,还和字符的位⼦有关。
通过简单的思考,我们可以构造如图的模型:
写⼀个⽐较正常的hash模板吧
const int EE = 97;
const int MOD = 100000007;
int HASH(string p)
{
int E = 1;
int ret = 0;
int tl = p.size();
for (int i=0;i<tl;i++)
ret += E*p[i], E *= EE;
return ret;
}
题⽬来了:
KMP问题
题⽬描述
如题,给出两个字符串s1和s2,其中s2为s1的⼦串,求出s2在s1中所有出现的位置。
输⼊输出格式
输⼊格式:
第⼀⾏为⼀个字符串,即为s1
第⼆⾏为⼀个字符串,即为s2
输出格式:
⼀⾏包含⼀个整数,表⽰s2在s1中出现的位置的个数
输⼊输出样例
输⼊样例#1:
ABABABC
ABA
输出样例#1:
2
说明
时空限制:1000ms,128M
数据规模:
设s1长度为N,s2长度为M
对于30%的数据:N<=15,M<=5
cstring转为int
对于70%的数据:N<=10000,M<=100
对于100%的数据:N<=1000000,M<=1000000
思路
⾸先说明:此题正解为KMP,不为hash。如果想知道KMP算法,请百度⼀下。
但是我们学的可是“hash”呀,不能直接预处理,如果直接预处理的话,时间为O(n*m),炸掉。
我们就可以递推:
  "已知长度为m的序列a[1]...a[m],现在已知"a[1]...a[m]"的hash值为K,欲求a[2]...a[m+1]的hash值。"
我⾸先想到的是乘法逆元,但还有其他的更简便的⽅法。
可以这⼀样想:"改变EE的赋值⽅式,反过来赋值,这样的话可以直接删去第⼀个'a[1]*EE^(m-1)',再乘⼀个'EE',往后再移⼀位,再加上⼀个a[m+1]."
那么,转移⽅程也很容易写了,为HASH[i]=(HASH[i-1]-a[i-2]*E[1]%M+M)%M*EE%M+a[i-2+m];(HASH[i]表⽰a[i-1]到a[i+m-2]的hash值。
另附代码:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,k,len1,len2;
int next1[1000001];
char s1[1000001];
char s2[1000001];
long long HASH[1000001];
long long E[1000001],M=1234567898765;
long long EE = 97;
int init()
{
long long Key=0;
int ans=0;
memset(E,0,sizeof(E));
memset(HASH,0,sizeof(HASH));
E[len2]=1;
for (int i=len2-1;i>=1;i--)
E[i]=E[i+1]*EE%M;
for (int i=1;i<=len2;i++)
HASH[1]=(HASH[1]+E[i]*(s1[i-1]))%M;
for (int i=1;i<=len2;i++)
Key=(Key+E[i]*(s2[i-1]))%M;
if (HASH[1]==Key) ans++;
for (int i=2;i<=len1-len2+1;i++)
{
HASH[i]=(HASH[i-1]-s1[i-2]*E[1]%M+M)%M*EE%M+s1[i-2+len2];
if (HASH[i]==Key) ans++;
}
printf("%d\n",ans);
}
int main(){
scanf("%s",s1) ;
scanf("%s",s2) ;
len1=strlen(s1);
len2=strlen(s2);
init();
return0;
}