1. 问题描述:
字符串hash是指将一个字符串S映射为一个整数,使得该整数可以尽可能唯一地代表字符串S,那么在一定程度上,如果两个字符串转换成的整数相等,那么就可以认为这两个字符串相同
在字符串hash方法中,对只有大写字母的字符串,它将字符串当做26进制的数字,然后将其转换为十进制,也就是下面的式子,其中str[i]表示的是字符串的i号位置,index函数将A~Z转换为0~25,H[I]表示字符串的前i个字符的哈希值,这样当i取遍了0~len - 1之后得到的H[len - 1]就是整个字符串的哈希值
H[i] = H[i - 1] * 26 + index(str[i]);
虽然在这个转换方式中,虽然字符串与整数是一一对应的但是由于没有进行适当的处理,因此当字符串特别长的时候那么产生的整数会非常大,没有办法使用一般的数据类型来进行保存,为了应对这些情况只能够舍弃一些"唯一性"将产生的结果对一个整数mod取模,也就是使用下面的散列函数
H[i] = (H[i - 1] * 26 + index(str[i])) % 26;
通过这个方式来把字符串转换为范围上能够接受的整数,但是这又会产生另外一个问题,也就是有可能多个字符的哈希中是一样的,导致了冲突,不过幸运的是在实践中发现在int类型范围内如果将一个进制数设置为10 ^ 7(10000019)级别的素数,而且将mod设置为一个10 ^ 9级别的素数(1000000007)
H[i] = (H[i - 1] * p + index(str[i])) % MOD;
2. 下面来计算字符串的子串的哈希值,也就是求解H[i...j],散列函数如下面的式子所示:
H[i] = H[i - 1] * p + index(str[i])
首先我们可以从进制的角度来考虑,H[i...j]实际上是等于将str[i...j]从p进制转换为10进制的结果,也就是:
H[i...j] = index(str[i]) * p ^ (j - i)+ index(str[i + 1]) * p ^ (j - i - 1)+ ... index(str[0]) * p ^ 0
然后使用H[j]的散列函数来进行推导:
H[j] = H[j - 1] * p + index(str[i])
= (H[j - 2] * p + index(str[j - 1])) * p + index(str[i])
= H[j - 2] * p ^ 2 + index(str[j - 1])) * p + index(str[i])
=...
= H[i - 1] + p ^ (j - i + 1) + index(str[i]) * p (j - i) + ... + index(str[j]) * p0
= H[i - 1] * p ^ (j - i + 1) + H[i...j]
因此有下式成立
H[i...j] = H[j] - H[i - 1] * p * (j - i + 1)
于是就得到了子串str[i..j]的哈希值,加上原先的取模操作就可以了:
H[i...j] =((H[j] - H[i - 1] * p * (j - i + 1)) % MOD + MOD) % MOD(括号内部有可能小于MOD所以需要先加上MOD)
3. 下面通过一个使用字符串哈希解决的例子:
问题描述如下:
输入两个长度均不超过1000的字符串,求解他们的最长公共子串的长度
对于这道题目我们就可以使用上面所说的字符串哈希的方法来进行解决,具体的代码如下:
#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<map>
#include<algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1000000007;
const LL P = 10000019;
const LL maxn = 200010;
LL powP[maxn], H1[maxn], H2[maxn];
vector<pair<int, int> > pr1, pr2;
void init(int len){
powP[0] = 1;
for(int i = 1; i <= len; ++i){
powP[i] = (powP[i - 1] * P) % MOD;
}
}
void calH(LL H[], string &str){
H[0] = str[0];
for(int i = 1; i < str.length(); ++i){
H[i] = (H[i - 1] * P + str[i]) % MOD;
}
}
int calSingleSubH(LL H[], int i, int j){
if(i == 0) return H[j];
return ((H[j] - H[i - 1] * powP[j - i + 1]) % MOD + MOD) % MOD;
}
void calSubH(LL H[], int len, vector<pair<int, int> > &pr){
for(int i = 0; i < len; ++i){
for(int j = i; j < len; ++j){
int hashValue = calSingleSubH(H, i, j);
pr.push_back(make_pair(hashValue, j - i + 1));
}
}
}
int getMax(){
int ans = 0;
for(int i = 0; i < pr1.size(); ++i){
for(int j = 0; j < pr2.size(); ++j){
if(pr1[i].first == pr2[j].first){
ans = max(ans, pr1[i].second);
}
}
}
return ans;
}
int main(void){
string str1, str2;
getline(cin, str1);
getline(cin, str2);
init(max(str1.length(), str2.length()));
calH(H1, str1);
calH(H2, str2);
calSubH(H1, str1.length(), pr1);
calSubH(H2, str2.length(), pr2);
printf("ans = %d\n", getMax());
return 0;
}
4. 假如需要输出两个字符串中最长的公共子串本身,那么我们需要增加一个vector<pair<int, int> >来记录其中的字符串哈希的起始位置,当我们在更新最长公共子串的时候通过这个记录的vector知道了起始位置,通过之前的ans来记录两个字符串的最长公共子串那么就可以输出最长的公共子串本身了,下面是具体的代码:
#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<map>
#include<algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1000000007;
const LL P = 10000019;
const LL maxn = 200010;
LL powP[maxn], H1[maxn], H2[maxn];
vector<pair<int, int> > pr1, pr2, rec;
int begin = 0;
void init(int len){
powP[0] = 1;
for(int i = 1; i <= len; ++i){
powP[i] = (powP[i - 1] * P) % MOD;
}
}
void calH(LL H[], string &str){
H[0] = str[0];
for(int i = 1; i < str.length(); ++i){
H[i] = (H[i - 1] * P + str[i]) % MOD;
}
}
int calSingleSubH(LL H[], int i, int j){
if(i == 0) return H[j];
return ((H[j] - H[i - 1] * powP[j - i + 1]) % MOD + MOD) % MOD;
}
void calSubH(LL H[], int len, vector<pair<int, int> > &pr){
for(int i = 0; i < len; ++i){
for(int j = i; j < len; ++j){
int hashValue = calSingleSubH(H, i, j);
pr.push_back(make_pair(hashValue, j - i + 1));
rec.push_back(make_pair(hashValue, i));
}
}
}
int getMax(int &begin){
int ans = 0;
for(int i = 0; i < pr1.size(); ++i){
for(int j = 0; j < pr2.size(); ++j){
if(pr1[i].first == pr2[j].first){
if(ans < pr1[i].second){
ans = pr1[i].second;
begin = rec[i].second;
}
}
}
}
return ans;
}
int main(void){
string str1, str2;
getline(cin, str1);
getline(cin, str2);
init(max(str1.length(), str2.length()));
calH(H1, str1);
calH(H2, str2);
calSubH(H1, str1.length(), pr1);
calSubH(H2, str2.length(), pr2);
cout << pr1.size() << " " << pr2.size() << endl;
int ans = getMax(begin);
printf("ans = %d\n", ans);
for(int i = begin; i < begin + ans; i++){
cout << str1[i];
}
return 0;
}