字符串哈希号称是学了后再遇到字符串题就可以丢掉kmp的算法哈哈(当然bushi)。
它是一种将字符串映射成数字的算法,如何映射的呢?
利用的就是hash,其方式是将一个给定的字符串转成 PPP进制的数字,即对字符串每个位置iii上的字符赋予一个权值PiP^iPi(这个权值从左到右依次递减),然后用一个数字取替该字符(一般来讲用的是字符对应ASCII码值)并乘上权值后相加起来,可以预想到此时的值会非常地庞大,所以再给它mod上一个限定值QQQ。
用公式表示就是:
字符串 —> s1s2s3s4......sn−1sns_1s_2s_3s_4......s_{n-1}s_ns1s2s3s4......sn−1sn。
哈希值—>(s1×Pn−1+s2×Pn−2+s3×Pn−3+......sn−1×P1+sn×P0)%Q(s_1×P^{n-1}+s_2×P^{n-2}+s_3×P^{n-3}+......s_{n-1}×P^1+s_n×P^0) \% Q(s1×Pn−1+s2×Pn−2+s3×Pn−3+......sn−1×P1+sn×P0)%Q。
如果让h[i]存字符串s[1...i]s[1...i]s[1...i]的hash值,也即整个字符串的hash值存在h[n]里面;
代入上述公式有:
h[1]=s[1]h[1] = s[1]h[1]=s[1],
h[2]=s[1]×P+s[2]h[2] = s[1]×P+s[2]h[2]=s[1]×P+s[2]
h[3]=s[1]×P2+s[2]×P+s[3]h[3] = s[1]×P^2+s[2]×P+s[3]h[3]=s[1]×P2+s[2]×P+s[3]
h[4]=s[1]×P3+s[2]×P2+s[3]×P+s[4]h[4] = s[1]×P^3+s[2]×P^2+s[3]×P+s[4]h[4]=s[1]×P3+s[2]×P2+s[3]×P+s[4]
… …
(先不显示%Q\%Q%Q)
由此,就可以得到一个递推式:h[i]=h[i−1]×P+s[i],i∈[1,n]h[i] = h[i-1]×P+s[i],i∈[1,n]h[i]=h[i−1]×P+s[i],i∈[1,n]。
那么观察一下就可以发现递推式有点点像一个前缀和公式,其实也就说明了可以从111到nnn递推地求出个各个h[]值,不仅如此,进一步拓展还可以再求字符串某个子串的hash值:
例如,子串s[2..4]s[2..4]s[2..4]的hash值=s[2]×P2+s[3]×P+s[4]s[2]×P^2+s[3]×P+s[4]s[2]×P2+s[3]×P+s[4],同时,也=h[4]−h[2−1]×P4−2+1h[4]-h[2 -1]×P^{4-2+1}h[4]−h[2−1]×P4−2+1,因此还得到一个区间和的公式:h[l,r]=h[r]−h[l−1]×Pr−l+1h[l,r]=h[r]-h[l-1]×P^{r-l+1}h[l,r]=h[r]−h[l−1]×Pr−l+1。
有了映射,那么有个值得注意的问题就是如何解决hash冲突(这里是hash值相等的情况)。
'A',那么对于字符串"A"、"AAA"、"AAAAAA"来讲,它们的hash值均为0,这就引起冲突大了!所以要以非0的数字代替字符以区别开。有了上述定义后,思考一个问题,如何比较两个字符串是否相等,那就简单了,先将它们都进行hash映射,然后对其hash值进行比较是否相等作为依据就可以了,由于是整数上的比较,那么该操作的复杂度仅仅只有O(1)O(1)O(1)。
一般来讲,用C++写的话并不需要在每次计算hash值时%Q\%Q%Q,转而可以考虑使用unsigned long long(ULL)数据类型来存,用这种数据类型有啥好处呢?一是存的数值比较大,适合拿来映射大数据量的字符串,二是当一个ULL的数溢出时,编译器自动会将这个数进行%264\%2^{64}%264处理,这正好是跟QQQ设置成2642^{64}264时期望吻合的啊。
有关更多字符串哈希的数学解释以及详细定义还可参考:字符串哈希 - OI Wiki。
给定一个长度为 n 的字符串,再给定 mmm 个询问,每个询问包含四个整数 l1,r1,l2,r2l_1,r_1,l_2,r_2l1,r1,l2,r2,请你判断 [l1,r1][l_1,r_1][l1,r1] 和 [l2,r2][l_2,r_2][l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数 nnn 和 mmm,表示字符串长度和询问次数。
第二行包含一个长度为 nnn 的字符串,字符串中只包含大小写英文字母和数字。
接下来 mmm 行,每行包含四个整数 l1,r1,l2,r2l_1,r_1,l_2,r_2l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从 111 开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤10^51≤n,m≤105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
#include
#include
using namespace std;
typedef unsigned long long ULL;const int N = 1e5 + 10, P = 131;ULL h[N];
ULL p[N]; //p[i]其实就是存P的i次方
char s[N];
int n, m;ULL get(int l, int r){ //子串s[l...r]的hash值return h[r] - h[l - 1] * p[r - l + 1];
}int main(){ios :: sync_with_stdio(false);cin >> n >> m;cin >> s + 1;p[0] = 1;//预处理hash值for(int i = 1;i <= n;i ++){h[i] = h[i - 1] * P + s[i];p[i] = p[i - 1] * P;}while(m --){int l1, r1, l2, r2;cin >> l1 >> r1 >> l2 >> r2;int h1 = get(l1, r1), h2 = get(l2, r2);if(h1 == h2) cout << "Yes" << endl;else cout << "No" << endl;}return 0;
}
如题,给定 NNN 个字符串(第 iii 个字符串长度为 MiM_iMi,字符串内包含数字、大小写字母,大小写敏感),请求出 NNN 个字符串中共有多少个不同的字符串。
友情提醒:如果真的想好好练习哈希的话,请自觉,否则请右转PJ试炼场:)
输入格式
第一行包含一个整数 NNN,为字符串的个数。
接下来 NNN 行每行包含一个字符串,为所提供的字符串。
输出格式
输出包含一行,包含一个整数,为不同的字符串个数。
样例 #1
样例输入 #1
5
abc
aaaa
abc
abcc
12345
样例输出 #1
4
提示
对于 30%30\%30% 的数据:N≤10N\leq 10N≤10,Mi≈6M_i≈6Mi≈6,Mmax≤15Mmax\leq 15Mmax≤15。
对于 70%70\%70% 的数据:N≤1000N\leq 1000N≤1000,Mi≈100M_i≈100Mi≈100,Mmax≤150Mmax\leq 150Mmax≤150。
对于 100%100\%100% 的数据:N≤10000N\leq 10000N≤10000,Mi≈1000M_i≈1000Mi≈1000,Mmax≤1500Mmax\leq 1500Mmax≤1500。
样例说明:
样例中第一个字符串(abc)和第三个字符串(abc)是一样的,所以所提供字符串的集合为{aaaa,abc,abcc,12345},故共计4个不同的字符串。
#include
#include
#include
using namespace std;
typedef unsigned long long ULL;const int N = 1e5 + 10, P = 131;ULL hs[N]; //存字符串的hash值
int n;
char s[1510];ULL get(){int len = strlen(s + 1);int h = 0, p = 1;for(int i = 1;i <= len;i ++){h = h * p + s[i];p *= P;}return h;
}int main(){ios :: sync_with_stdio(false);cin >> n;for(int i = 0;i < n;i ++){cin >> s + 1;hs[i] = get();}sort(hs, hs + n);int ans = 1;for(int i = 1;i < n;i ++){if(hs[i] != hs[i - 1]) ans ++;}cout << ans << endl;return 0;
}
可以看到,经过一遍hash值的预处理后,给后续子串之间的查询操作带来了极大的方便,让原本繁琐的字符串匹配转变成了只是数字上的比较。
上一篇:2009年数学二真题复盘
下一篇:对字开头的成语接龙