字符串哈希 学习笔记
Lin_Yi_Rui · · 算法·理论
哈希
可将复杂的数据压缩:通过哈希函数将其映射成一个整数,即哈希值。
然后利用这个哈希值进行
注意:倘若你的哈希函数不够优秀,可能导致两个不同的数值转换为相同的哈希值,我们称之为冲突。
冲突的存在会导致你的算法出错,所以你必须着手解决它。
字符串哈希基础
当上文的复杂数据为字符串时,
我们称为字符串哈希。
哈希函数的构造
对于字符串
公式理解
感性理解
假设我们有一个数组
很显然可以用哈希值
原理是将数组的每个元素看作
迁移运用
字符串是一个字符数组,
而每个字符都可以用 ASCII 码表示为
因此,对应的哈希值应该至少是一个
用每个字符的 ASCII 码作为它的每一位。
注意:计算的结果可能很大,所以我们需要取模。
避免冲突
因为哈希值需要对
但字符串的种类却是无限的。
因此冲突不可避免,我们能做的只有减小冲突出现的概率。
数值的选取
底数
底数(进制)
通常选择
模数
模数
如果想减少码量,可以直接定义哈希值为 unsigned long long 类型,
运用其自然溢出,能轻松实现对
双值哈希
选择不同的模数和底数,计算两个哈希值,
当且仅当两个哈希值都相等时,才说明字符串相同。
- 第一种实现方式:定义哈希值为
pair< , >类型,分别存储两个哈希值,可以直接用==比较。 - 第二种实现方式:若两个哈希值
h_1 、h_2 均为9 位数,则可以用h_1\times 10^9 +h_2 合并为一个哈希值。
出题人角度
哈希可能出现冲突,使答案错误,
因此字符串哈希不是一个
站在出题人角度,他很可能会构造出现冲突的数据,进而卡掉这个不那么正确的算法。
因此我们需要使哈希函数变复杂,来避免冲突,避免被卡掉。
具体方法有很多,比如上面那些。
注意事项
-
尽量不要用很常见的底数和模数,比如
131 、10^9+7 以及自然溢出。
这些太容易被出题人猜到了。 -
也不要用非质数,否则你的哈希安全性会大大降低。
-
建议选择一些刁钻的质数,比如你的生日。
-
或者用一些其他手段,如双值哈希、用随机数取底数等。
不过不用担心,一般哈希是很难卡的,可以放心使用。
字符串哈希技巧
首先给出几份字符串哈希模版代码:
单值哈希
typedef long long ll;
ll mod = 1000000009,base = 13331;
ll Hash_str(string s) {
ll h = 0;
for(char c:s) {
h = (h*base+c)%mod;
}
return h;
}
自然溢出哈希
typedef unsigned long long ull;
ull base = 13331;
ull Hash_str(string s) {
ull h = 0;
for (char c:s) {
h = h*base+(ull)c;
}
return h;
}
双值哈希
typedef long long ll;
typedef pair<ll,ll> pll;
ll mod[2] = {1000000009,999999937},base[2] = {13331,1145141};
pll Hash_str(string s) {
ll h0 = 0,h1 = 0;
for(char c:s) {
h0 = (h0*base[0]+c)%mod[0];
h1 = (h1*base[1]+c)%mod[1];
}
return make_pair(h0,h1);
}
前缀哈希
考虑如何快速查询一段子串的哈希值。
定义
用
则其哈希值可以这样计算:
类似于前缀和,
预处理
::::info[感性理解]
假设我们有一个数组
很显然可以用哈希值
那么从下标
其是由
那么换到字符串上也是同理。 ::::
Code
单值前缀哈希
typedef long long ll;
ll powb[N],h[N];
ll base = 13331,mod = 1000000007;
void init() {
powb[0] = 1; //表示 base^i
for(int i = 1; i <= (int)s.size(); ++i) {
//我的字符串下标是从 1 开始的
h[i] = (h[i-1]*base+s[i])%mod;
powb[i] = powb[i-1]*base%mod;
}
}
ll Hash_substr(int l,int r) {
return (h[r]- h[l-1]*powb[r-l+1]%mod+mod)%mod;
}
双值前缀哈希
typedef long long ll;
typedef pair<ll,ll> pll;
pll h[N];
ll mod[2] = {1000000009,999999937},base[2] = {13331,1145141},powb[2][N];
void init() {
powb[0][0] = powb[1][0] = 1;
for(int i = 1; i <= (int)s.size(); ++i) {
h[i].first = (h[i-1].first*base[0]+s[i])%mod[0];
h[i].second = (h[i-1].second*base[1]+s[i])%mod[1];
powb[0][i] = powb[0][i-1]*base[0]%mod[0];
powb[1][i] = powb[1][i-1]*base[1]%mod[1];
}
}
pll Hash_substr(int l,int r) {
return make_pair((h[r].first- h[l-1].first*powb[0][r-l+1]%mod[0] +mod[0])%mod[0],
(h[r].second- h[l-1].second*powb[1][r-l+1]%mod[1] +mod[1])%mod[1]);
}
二维前缀哈希
若待处理的数据从字符串变为字符矩阵,
即由一维升到二维,该怎么哈希。
可以类比一维前缀和与二维前缀和。
定义
定义
具体的,我们先对每一行做一次一维字符串哈希。
对于每一行的哈希值,我们再在纵向上把它们哈希起来,即为二维字符串哈希。
初始化
按照上述过程,
这里提供一种更好写的初始化(不需要容斥)。
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
h[i][j] = h[i][j-1]*base[0]+c[i][j];
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
h[i][j] += h[i-1][j]*base[1];
base[0]和base[1]表示进制,
为了安全性,横向和纵向的进制可以不同。
计算子矩阵哈希值
结合容斥原理,计算左上角
unsigned long long matrix(int a,int b,int c,int d) {
return h[c][d]
- h[a-1][d]*powb[1][c-a+1]
- h[c][b-1]*powb[0][d-b+1]
+ h[a-1][b-1]*powb[1][c-a+1]*powb[0][d-b+1];
}
//powb[0][i] 和 powb[1][i] 需要预处理计算
//表示 base[0] 和 base[1] 的 i 次方
注意:
绿色部分乘base[1](列进制)的次方,
蓝色部分乘base[0](行进制)的次方,
交叉部分被两种进制计算过两次,因此要乘base[1]和base[0]。
感性理解一下,上图的计算结果应为:
哈希值
时间复杂度:
Code
单值哈希
ull base[2] = {1145141,13331},powb[2][N];
ull h[N][N];
void init(char c[N][N],int n,int m) {
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++) {
h[i][j] = h[i][j-1]*base[0]+c[i][j];
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++) {
h[i][j] += h[i-1][j]*base[1];
}
powb[0][0] = powb[1][0] = 1;
for(int i = 1; i <= max(n,m); i ++) {
powb[0][i] = powb[0][i-1]*base[0];
powb[1][i] = powb[1][i-1]*base[1];
}
}
ull matrix(int a,int b,int c,int d) {
return h[c][d]
- h[a-1][d]*powb[1][c-a+1]
- h[c][b-1]*powb[0][d-b+1]
+ h[a-1][b-1]*powb[1][c-a+1]*powb[0][d-b+1];
}
双值哈希
ull base[4] = {1145141,13331,131,137},powb[4][N];
pll h[N][N];
void init(char c[N][N],int n,int m) {
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++) {
h[i][j].first = h[i][j-1].first*base[0]+c[i][j];
h[i][j].second = h[i][j-1].second*base[2]+c[i][j];
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++) {
h[i][j].first += h[i-1][j].first*base[1];
h[i][j].second += h[i-1][j].second*base[3];
}
powb[0][0] = powb[1][0] = powb[2][0] = powb[3][0] = 1;
for(int i = 1; i <= max(n,m); i ++) {
powb[0][i] = powb[0][i-1]*base[0];
powb[1][i] = powb[1][i-1]*base[1];
powb[2][i] = powb[2][i-1]*base[2];
powb[3][i] = powb[3][i-1]*base[3];
}
}
pll matrix(int a,int b,int c,int d) {
ull fir = h[c][d].first
- h[a-1][d].first*powb[1][c-a+1]
- h[c][b-1].first*powb[0][d-b+1]
+ h[a-1][b-1].first*powb[1][c-a+1]*powb[0][d-b+1];
ull sec = h[c][d].second
- h[a-1][d].second*powb[3][c-a+1]
- h[c][b-1].second*powb[2][d-b+1]
+ h[a-1][b-1].second*powb[3][c-a+1]*powb[2][d-b+1];
return make_pair(fir,sec);
}
字符串哈希应用
字符串哈希的本质是数据压缩,
其应用也是围绕这个作用展开的。
字符串匹配
在大部分字符串匹配问题中,
哈希都是一个更好理解、更方便快捷的方法。
(前缀哈希的复杂度都
做法
用上述方法求出字符串或字符矩阵的哈希值,
若哈希值相同,则说明它们本身也相等。
一维字符串匹配
P3370 【模板】字符串哈希
CF79C Beaver
解析
二维字符串矩阵匹配
CF228C Fractal Detector
解析
二维矩阵展开为一维字符串
某些题目的字符矩阵不需要或不能使用
那么我们可以将二维矩阵展开成一个很长的一维字符串,对该字符串求哈希即可。
这种写法的码量较少
CF54B Cutting Jigsaw Puzzle
解析
哈希思想的其它应用
对于其它非字符串的题目,
若需要数据压缩及匹配,同样可以使用上文讲的多项式哈希。
P8026 [ONTAK2015] Bajtocja
解析