文章目录
写在最前面的话
为了便于广大的算法爱好者和本人复习、加深对算法的理解,故持续更新各种基础算法,每个算法都由深入介绍算法和实战操练两部分构成,题目由易入难,由浅入深,适合备考和复习,所选例题来自洛谷、牛客、LOJ、ZOJ、BZOJ、HDU等。总体上以牛客ACM竞赛算法知识点进行归类整理,希望大家学习以后都能有所收获!
哈希入门介绍
众所周知,比较两个字符串是否相同复杂度在最差情况下是 O ( n ) O(n) O(n),虽然期望是 O ( 1 ) O(1) O(1),但很容易就会被毒瘤出题人卡成 O ( n ) O(n) O(n),于是字符串哈希就应运而生了。不过还是很容易被卡,我在最后结束时会结合BZOJ上的 H a s h K i l l e r HashKiller HashKiller分析卡哈希的办法为什么不直接双哈希
哈希的主要思想就是把一个字符串转换成一个大整数,这样比较两个字符串是否相等的就只要比较两个整数是否相等就行了,妥妥稳定的 O ( 1 ) O(1) O(1)比较。下面先来介绍如何把字符串转变成一个整数。
我们考虑一个字符串中的每一个字符都可以对应一个ACSll码,于是就可以把每一个字符乘上一个进制数再取模,就像十进制一样,一个字符串 s s s的第 i i i位字符(下标从1开始)所对应的哈希值就是 s [ i ] ∗ b a s e n − i s[i]*base^{n-i} s[i]∗basen−i,这里 b a s e base base是我们自己选择的进制数,一般可以选233,23333,10007等,不要太小就行。这样对每一位字符进行哈希并且累加,我们就得到了一整串字符的哈希值。
如:当 n = 3 n=3 n=3时, h a s h ( s ) = s [ 1 ] ∗ M 2 + s [ 2 ] ∗ M + s [ 3 ] ; hash(s)=s[1]*M^2+s[2]*M+s[3]; hash(s)=s[1]∗M2+s[2]∗M+s[3];
当 n = 4 n=4 n=4时, h a s h ( s ) = s [ 1 ] ∗ M 3 + s [ 2 ] ∗ M 2 + s [ 3 ] ∗ M + s [ 4 ] ; hash(s)=s[1]*M^3+s[2]*M^2+s[3]*M+s[4]; hash(s)=s[1]∗M3+s[2]∗M2+s[3]∗M+s[4];
一般地 h a s h ( s ) = ∑ i = 1 n s [ i ] ∗ b a s e n − i hash(s)=\sum_{i=1}^{n}s[i]*base^{n-i} hash(s)=∑i=1ns[i]∗basen−i。
求一个字符串的哈希值代码如下:
int hash(char *s)
{
int len = strlen(s), ans = 0;
for (int i = 1; i <= len; i++)
ans = ((ll)ans * M + s[i]) % N;
return ans;
}
每次累加时模上一个大质数,大多采用1e9+7、1e9+9、998244353等,当然也可以选择自然溢出,就是不进行取模操作。选择大质数的原因是我们想要每一个字符串尽可能对应唯一的哈希值,选择的质数小了或者选择合数会使得发生冲突的概率大大上升,这边可以参考生日攻击,同时这也是我们hack哈希的原理。
字符串哈希的基本操作
比起得到整个字符串的哈希值,更多时候我们更希望得到该字符串某一子串的哈希值,即给定区间 [ l , r ] [l,r] [l,r],求这段区间内子串哈希值。或者求区间 [ l 1 , r 1 ] 、 [ l 2 , r 2 ] [l_1,r_1]、[l_2,r_2] [l1,r1]、[l