Loading

【瞎口胡】字符串 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;
}
posted @ 2021-08-23 15:11  Meatherm  阅读(163)  评论(0编辑  收藏  举报