【瞎口胡】字符串 Hash
字符串 Hash(译名:哈希)是一种字符串到整数的映射。该映射使得可以在较快时间内判断两个字符串是否相等。
Hash 函数的构造方法
形式化地讲,对于字符串 \(s = s_{1 \cdots n}\),\(\operatorname{hash}(s) =(\sum \limits_{i=1}^{n} s_i \times b^{n-i}) \mod p\)。
即把 \(s\) 当作一个 \(b\) 进制数,再对 \(p\) 取模。
代码:
for(int i=1;i<=n;++i)
hash[i]=(hash[i-1]*b+s[i])%p;
Hash 函数的性质及构造技巧
\(\operatorname{hash}()\) 具有以下性质:
-
两个相同的字符串,它们的 \(\operatorname{hash}\) 值一定相同。
推论(逆否命题):两个 \(\operatorname{hash}\) 值不同的字符串,它们一定不同。
-
两个 \(\operatorname{hash}\) 值相同的字符串,它们可能相同。
如果出现了两个 \(\operatorname{hash}\) 值相同但本质不同的字符串,那么称这是一次「哈希冲突」。
有一些避免 Hash 冲突的技巧:
- \(b\) 至少大于字符集大小:使得在不考虑取模的情况下,每个 \(\operatorname{hash}\) 值对应不同的字符串。
- \(p\) 选取较大的质数:使得 \(\operatorname{hash}\) 值取模后的结果尽可能地多,从而降低冲突的可能性。
在随机数据下,每次比较产生冲突的概率可以近似地估计为 \(\dfrac 1p\),当 \(p\) 取 \(10^9\) 量级的质数时,进行 \(10^6\) 次比较后,仍未产生冲突的概率约 \(\dfrac {999}{1000}\)。这并不是一个理想的正确率。为了提高这一正确率,可以设置两个模数 \(p_1,p_2\),分别计算 \(\operatorname{hash}\) 值,这样单次比较产生冲突的概率约为 \(\dfrac{1}{p_1p_2}\),大大减小了。这种设置两个模数的技巧叫做双哈希,同理,朴素的只有一个模数的哈希算法称为单哈希。
实际实现单哈希时,建议尽量避免使用常见的质数作为模数,例如 \(10^9+7,10^9+9,998244353,19260817\)。
直接使用 unsigned int
类型而避免进行取模的哈希称为自然溢出的哈希算法,即自动对 \(2^{32}\) 取模。该算法实现简单,代码常数较小,但有被人为构造的数据卡掉的风险。
Hash 函数的应用
字符串判等
题意
给定 \(n\) 个字符串 \(s_1,s_2,\cdots,s_n\),求其中不同的字符串个数。
\(1 \leq n \leq 10^5, \sum |s_i| \leq 10^6\)
题解
对于每个 \(s_i\) 计算出其 \(\operatorname{hash}\) 值,然后利用排序和 unique
统计出不同的 \(\operatorname{hash}\) 值数量。
时间复杂度 \(O(\sum |s_i| + n \log n)\)。
子串 Hash 值比较
题意
给定长度为 \(n\) 的字符串 \(s_{1\cdots n}\),\(m\) 次询问,每次询问两个子串是否相等。
\(1 \leq n,m\leq 10^6\)
题解
设 \(f_i\) 表示前 \(i\) 位的 \(\operatorname{hash}\) 值。假设我们已经求出了所有 \(f_i\),现在怎么求出 \([l,r]\) 的 \(\operatorname{hash}\) 值 \(S(l,r)\)?
如果我们有一个正整数 \(4321\),要求出它最右侧两位的值呢,把左侧两位的值减去即可。但答案不是 \(4321-43\),而是 \(4321-4300\),于是我们要把左侧两位的值乘上 \(10^2\),再相减。
推广到 \(\operatorname{hash}\) 值上,有 \(S(l,r) =( f_r - f_{l-1}\times b^{r-l+1}) \mod p\)。
这样,我们可以在 \(O(1)\) 的复杂度内计算子串的 \(\operatorname{hash}\) 值,从而在 \(O(1)\) 的复杂度内比较子串是否相等。
时间复杂度 \(O(n + m)\)。
判断回文串
题意
给定长度为 \(n\) 的字符串 \(s_{1\cdots n}\),\(m\) 次询问,每次询问某个子串是否是回文串。
\(1 \leq n,m \leq 10^6\)
题解
回文串的就是是正读和反读都一样的字符串。于是我们只需要判断某个子串是否和它的反串的 \(\operatorname{hash}\) 值一致即可。
维护原串前 \(i\) 位的 \(\operatorname{hash}\) 值和后 \(i\) 位的 \(\operatorname{hash}\) 值就可以完成该计算。
例题 SPOJ EPALIN
题意
给定长度为 \(n\) 的字符串 \(s\),在 \(s\) 的后面添加尽可能少的字符,使该串成为一个回文串。
\(1 \leq n \leq 10^5\)
题解
求出 \(s\) 的最长回文后缀即可,这一部分不用被添加。
# include <bits/stdc++.h>
const int N=100010,INF=0x3f3f3f3f,BASE=233;
typedef long long ll;
const ll MOD=998244853;
char s[N];
ll lhash[N],rhash[N],bpow[N];
int n;
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
int main(void){
bpow[0]=1;
for(int i=1;i<=N-10;++i)
bpow[i]=bpow[i-1]*BASE%MOD;
while(~scanf("%s",s+1)){
n=strlen(s+1);
lhash[0]=0;
for(int i=1;i<=n;++i)
lhash[i]=(lhash[i-1]*BASE+s[i])%MOD;
rhash[n+1]=0;
for(int i=n;i;--i)
rhash[i]=(rhash[i+1]*BASE+s[i])%MOD;
int pos=0;
for(int i=1;i<=n;++i){
if((lhash[n]-lhash[i-1]*bpow[n-i+1]%MOD+MOD)%MOD==rhash[i]){
pos=i;
break;
}
}
for(int i=1;i<=n;++i)
putchar(s[i]);
for(int i=pos-1;i;--i)
putchar(s[i]);
puts("");
}
return 0;
}