【题目链接】
ybt 1455:【例题1】Oulipo
LOJ #103. 子串查找
LOJ 103与ybt 1455的输入顺序不同(先输入主串,后输入模式串),字符要求不同(可能是各种字符),而且LOJ 103的数据更强。
【题目考点】
1. 字符串哈希
哈希相关概念见:【模板:哈希表】信息学奥赛一本通 1456:【例题2】图书管理
使用基数转换法 求一个字符串的哈希值:
给出一个固定进制 b b b,将一个字符串上的每个字符看做一个进制位上的数字,所以这个字符串就可以看做一个 b b b进制的数,可以配合除留余数法 ,将这个数的数值再对一个模数取模,就可以得到这个字符串的哈希值。
选取两个互质的常数进制 b b b和模数 M M M ( b < M ) (b < M) (b<M),假设有字符串 s s s,长度为 n n n, c i c_i ci是字符 s i s_i si对应的数字,那么我们定义哈希函数:
H ( s ) = ( c 0 b n − 1 + c 1 b n − 2 + . . . + c n − 1 b 0 ) m o d M H(s) = (c_0b^{n-1} + c_1b^{n-2} + ... + c_{n-1}b^0)\bmod M H(s)=(c0bn−1+c1bn−2+...+cn−1b0)modM
注意:
- 新的基数 b b b一般大于字符串 s s s对应的数字串的基数。
- 基数 b b b与模数 M M M互质, b b b一般选择质数。
- 字符转为对应的数字选择:一般不转为数字 0 0 0。
如果转为数字0,会增加哈希冲突。
假设基数 b = 10 , m = 101 b = 10, m = 101 b=10,m=101
如果 c i = s i − A c_i = s_i-A ci=si−A,那么'A'对应0, 'B'对应1
H ( " A B " ) = ( 0 ∗ 10 + 1 ∗ 1 ) m o d 101 = 1 H("AB") = (0*10 + 1*1) \bmod 101 = 1 H("AB")=(0∗10+1∗1)mod101=1
H ( " B " ) = ( 1 ∗ 1 ) m o d 101 = 1 H("B") = (1*1) \bmod 101 = 1 H("B")=(1∗1)mod101=1,发生了哈希冲突。如果 c i = s i − A + 1 c_i = s_i-A+1 ci=si−A+1,那么'A'对应1, 'B'对应2
H ( " A B " ) = ( 1 ∗ 10 + 2 ∗ 1 ) m o d 101 = 12 H("AB") = (1*10 + 2*1) \bmod 101 = 12 H("AB")=(1∗10+2∗1)mod101=12
H ( " B " ) = ( 1 ∗ 2 ) m o d 101 = 2 H("B") = (1*2) \bmod 101 = 2 H("B")=(1∗2)mod101=2
自然溢出
对于C++中的 X X X位无符号整型,当计算结果超出了X位二进制数,变量中只保留最低的X位二进制数,这种机制为自然溢出,其效果相当于对数值取模 2 X 2^X 2X。
可以使用32位无符号整数(unsigned )计算哈希值,并取 M = 2 32 M = 2^{32} M=232。
或使用64位无符号整数(unsigned long long )计算哈希值,并取 M = 2 64 M = 2^{64} M=264。
利用自然溢出省去求模运算。
2. KMP算法
KMP算法概念本题做法见:<>
【解题思路】
给定两个字符串 S S S、 T T T,判断 T T T是否是 S S S的子串。如果 T T T是 S S S的子串,返回 T T T在 S S S中的起始位置,或 T T T在 S S S中出现的次数。
对子串的定位操作通常称为字符串的模式匹配问题 ,其中等待匹配的S串为主串 ,用来匹配的T串为模式串。
解决字符串模式匹配问题,有以下多种解法:
非正解 :枚举算法
记主串 S S S的长度为 n n n,模式串 T T T的长度为 m m m
- 枚举 S S S中所有长为 m m m的子串
- 判断 S S S中长为 m m m的子串与模式串 T T T是否相同,如果相同则计数加1。
- 最后输出计数
该算法的时间复杂度为 O ( n ⋅ m ) O(n\cdot m) O(n⋅m),无法通过此题。
正解解法1:滚动哈希算法
1. 字符串哈希解题算法
枚举子串求哈希值
- 求出模式串 T T T的哈希值 h t ht ht
- 枚举主串中每个长为 m m m的子串,求出其哈希值 h h h
如果 h h h等于 h t ht ht,再判断子串与模式串是否相同,如相同则进行计数。
单独求长为 m m m的子串哈希值的时间复杂度为 O ( m ) O(m) O(m)。如果我们可以通过滚动哈希算法,以 O ( 1 ) O(1) O(1)的时间复杂度求出每个长为 m m m的子串的哈希值,那么解决该问题的总体时间复杂度为 O ( n ) O(n) O(n)。
2. python字符串切片表示方法
以下使用python 中字符串切片的写法表示字符串的子串:
s s s是字符串型变量, s i s_i si表示 s s s的下标 i i i位置的字符,下标从0开始数。
s [ x : y ] s[x:y] s[x:y] s s s下标区间 [ x , y ) [x, y) [x,y)中的字符构成的子串,即 s x ∼ s y − 1 s_x\sim s_{y-1} sx∼sy−1
s [ : y ] s[:y] s[:y] s s s下标区间 [ 0 , y ) [0, y) [0,y)中的字符构成的子串,即 s s s的前 y y y个字符构成的子串。
s [ x : ] s[x:] s[x:] s s s下标区间 [ x , 0 ) [x, 0) [x,0)中的字符构成的子串,即 s s s从 s x s_x sx到末尾字符构成的子串。
s [ x : x + n ] s[x:x+n] s[x:x+n] s s s下标区间 [ x , x + n ) [x, x+n) [x,x+n)中的字符构成的子串,即从 s x s_x sx开始长为 n n n的子串。
3. 滚动哈希算法
为了可以以 O ( 1 ) O(1) O(1)时间复杂度求出 S S S的每个长为 m m m子串的哈希值,我们需要做一些预处理操作。
根据使用基数转换法 进行字符串哈希的方法:
选取两个互质的常数进制 b b b和模数 M M M ( b < M ) (b < M) (b<M),假设有字符串 s s s,长度为 n n n, c i c_i ci是字符 s i s_i si对应的数字,那么我们定义哈希函数:
H ( s ) = ( c 0 b n − 1 + . . . + c n − 1 b 0 ) m o d M H(s) = (c_0b^{n-1} + ... + c_{n-1}b^0)\bmod M H(s)=(c0bn−1+...+cn−1b0)modM
在本题中 b b b取 31 31 31,大于字符串中字符的转成的数值(A~Z转为0~25)。 M M M取 2 32 2^{32} 232,进行自然溢出。
(为了表示方便,以下公式中不再写出 m o d M \bmod\ M mod M,实际是要进行取模的。)
根据上式,可知:
H ( s [ : k ] ) = c 0 b k − 1 + c 1 b k − 2 + . . . + c k − 1 b 0 H(s[:k]) = c_0b^{k-1}+c_1b^{k-2}+ ... +c_{k-1}b^0 H(s[:k])=c0bk−1+c1bk−2+...+ck−1b0
H ( s [ : k − 1 ] ) = c 0 b k − 2 + c 1 b k − 3 + . . . + c k − 2 b 0 H(s[:k-1]) = c_0b^{k-2}+c_1b^{k-3}+ ... +c_{k-2}b^0 H(s[:k−1])=c0bk−2+c1bk−3+...+ck−2b0
b H ( s [ : k − 1 ] ) = c 0 b k − 1 + c 1 b k − 2 + . . . + c k − 2 b 1 bH(s[:k-1]) = c_0b^{k-1}+c_1b^{k-2}+ ... +c_{k-2}b^1 bH(s[:k−1])=c0bk−1+c1bk−2+...+ck−2b1
b H ( s [ : k − 1 ] ) + 1 = c 0 b k − 1 + . . . + c k − 2 b 1 + b 0 bH(s[:k-1]) +1= c_0b^{k-1}+ ... +c_{k-2}b^1+b^0 bH(s[:k−1])+1=c0bk−1+...+ck−2b1+b0
因此: H ( s [ : k ] ) = b H ( s [ : k − 1 ] ) + 1 H(s[:k])=bH(s[:k-1]) +1 H(s[:k])=bH(s[:k−1])+1
可以先预处理出 H ( s [ : k ] ) , k ∈ [ 1 , n ] H(s[:k]), k\in[1, n] H(s[:k]),k∈[1,n]
对于字符串 s s s从下标 k k k开始长为 n n n的子串为 s [ k : k + n ] s[k:k+n] s[k:k+n]
H ( s [ k : k + n ] ) = c k b n − 1 + c k + 1 b n − 2 + . . . + c k + n − 1 b 0 H(s[k:k+n]) = c_kb^{n-1}+c_{k+1}b^{n-2}+... +c_{k+n-1}b^0 H(s[k:k+n])=ckbn−1+ck+1bn−2+...+ck+n−1b0
H ( s [ : k + n ] ) = c 0 b k + n − 1 + . . . + c k + n − 1 b 0 H(s[:k+n]) =c_0b^{k+n-1}+...+c_{k+n-1}b^0 H(s[:k+n])=c0bk+n−1+...+ck+n−1b0
已知: H ( s [ : k ] ) = c 0 b k − 1 + . . . + c k − 1 b 0 H(s[:k]) = c_0b^{k-1}+ ... +c_{k-1}b^0 H(s[:k])=c0bk−1+...+ck−1b0
所以: b n H ( s [ : k ] ) = c 0 b n + k − 1 + . . . + c k − 1 b n b^nH(s[:k])=c_0b^{n+k-1}+...+c_{k-1}b^n bnH(s[:k])=c0bn+k−1+...+ck−1bn
b n H ( s [ : k ] ) + H ( s [ k : k + n ] ) b^nH(s[:k])+H(s[k:k+n]) bnH(s[:k])+H(s[k:k+n])
= c 0 b n + k − 1 + . . . + c k − 1 b n + c k b n − 1 + . . . + c k + n − 1 b 0 =c_0b^{n+k-1}+...+c_{k-1}b^n+c_kb^{n-1}+... +c_{k+n-1}b^0 =c0bn+k−1+...+ck−1bn+ckbn−1+...+ck+n−1b0
= H ( s [ : k + n ] ) =H(s[:k+n]) =H(s[:k+n])
所以:
H ( s [ k : k + n ] ) = H ( s [ : k + n ] ) − b n H ( s [ : k ] ) H(s[k:k+n])=H(s[:k+n])-b^nH(s[:k]) H(s[k:k+n])=H(s[:k+n])−bnH(s[:k])
根据此公式,可以在预处理出 H ( s [ : i ] ) H(s[:i]) H(s[:i])与 b i b^i bi后,以 O ( 1 ) O(1) O(1)时间复杂度求出 s s s字符串的任意子串的哈希值。
4. 具体实现
- 设 b = 131 b=131 b=131
bp数组,bp[i]表示 b i b^i bi,递推求出bp数组的值。 - 设
h数组,h[i]表示字符串 s s s的前 i i i个字符的哈希值,即 H ( s [ : i ] ) H(s[:i]) H(s[:i])
根据递推式 H ( s [ : k ] ) = b H ( s [ : k − 1 ] ) + 1 H(s[:k])=bH(s[:k-1]) +1 H(s[:k])=bH(s[:k−1])+1,可以求出 h h h数组。 - 根据公式: H ( s ) = c 0 b n − 1 + . . . + c n − 1 b 0 H(s) = c_0b^{n-1} + ... + c_{n-1}b^0 H(s)=c0bn−1+...+cn−1b0求出模式串 T T T的哈希值
ht。 - 枚举主串 S S S的每个长为 m m m的子串,对于从下标 i i i开始长为 m m m的子串,通过公式 H ( s [ k : k + n ] ) = H ( s [ : k + n ] ) − b n H ( s [ : k ] ) H(s[k:k+n])=H(s[:k+n])-b^nH(s[:k]) H(s[k:k+n])=H(s[:k+n])−bnH(s[:k])可以求出该子串的哈希值。将该哈希值与
ht进行比较,如果二者相同,则认为主串 S S S中出现了一次 T T T,计数器cnt增加1。最后输出子串的数量cnt。 子串 s [ i : i + m ] s[i:i+m] s[i:i+m]与模式串 T T T的哈希值相同,此时理论上应该再判断两字符串各字符是否完全相同。但如果再判断两字符串是否完全相同,本题就会时间超限。只要哈希的方法合理,两字符串哈希值相同时,大概率二者的字符串是相同的。但如果两字符串不同,但哈希值相同,就发生了哈希冲突 ,这种情况也是可能存在的。为了减少哈希冲突,我们可以让哈希值的范围更大(如设为unsigned long long类型),或使用双哈希方法,降低哈希冲突发生的概率。
但只要是哈希算法,总是有概率发生哈希冲突的。我们只是人为忽略了这样的小概率事件发生的情况,去赌该题的的测试数据不会让我的哈希算法发生暗哈希冲突。
【题解代码】:ybt 1455:【例题1】Oulipo
非正解:枚举算法(样例正确,无法通过此题)
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005, M = 10005;
char s1[M], s2[N];
bool isSame(char *s, char *t, int m)//s与t的前m个字符是否相同
{
for(int i = 0; i < m; ++i)
if(s[i] != t[i])
return false;
return true;
}
int bruteFroce(char *s, char *t)//枚举求主串s中出现过多少次模式串t
{
int cnt = 0, n = strlen(s), m = strlen(t);
for(int i = 0; i <= n-m; ++i)//判断s[i]~s[i+m-1]是否与t相同
if(isSame(s+i, t, m))
cnt++;
return cnt;
}
int main()
{
int t;
cin >> t;
while(t--)
{
cin >> s1 >> s2;
cout << bruteFroce(s2, s1) << endl;
}
return 0;
}
解法1:滚动哈希
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005, M = 10005;
char s1[M], s2[N];
int b = 31;
unsigned h[N], bp[N];//h[i]:字符串a下标0开始长为i的字符串的哈希值, bp[i]:b^i%mod
void initBp()
{
bp[0] = 1;
for(int i = 1; i < N; ++i)
bp[i] = bp[i-1]*b;
}
int rollingHash(char s[], char t[])//滚动哈希求s中包含多少个c
{
int cnt = 0, n = strlen(s), m = strlen(t);//n:主串长度 m:模式串长度
unsigned ht = 0;
for(int i = 1; i <= n; ++i)//已知h[0] = 0
h[i] = h[i-1]*b+s[i-1]-'A'+1;
for(int i = 1; i <= m; ++i)
ht = ht*b+t[i-1]-'A'+1;//t的哈希值
for(int i = 0; i <= n-m; ++i)
if(h[i+m]-h[i]*bp[m] == ht)//s[i:i+m]与t的哈希值相同
cnt++;//注意这里理论上应该再做一遍字符串比较,比较s[i:i+m]与t是否相同。因为哈希值相同不代表字符串相同。但如果做比较的话,本题就超时了。
return cnt;
}
int main()
{
int t;
cin >> t;
initBp();
while(t--)
{
cin >> s1 >> s2;
cout << rollingHash(s2, s1) << endl;
}
return 0;
}
【题解代码】:LOJ #103. 子串查找
解法1:滚动哈希
cpp
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1000005;
char s[N], t[N];
int b = 131, cnt;
ULL h[N], bp[N], ht;//h[i]:字符串a下标0开始长为i的字符串的哈希值, bp[i]:b^i%mod
void initBp()
{
bp[0] = 1;
for(int i = 1; i < N; ++i)
bp[i] = bp[i-1]*b;
}
int main()
{
initBp();
cin >> s >> t;
int n = strlen(s), m = strlen(t);//n:主串长度 m:模式串长度
if(n < m)
{
cout << 0;
return 0;
}
for(int i = 1; i <= n; ++i)//已知h[0] = 0
h[i] = h[i-1]*b+s[i-1];
for(int i = 1; i <= m; ++i)
ht = ht*b+t[i-1];//t的哈希值
for(int i = 0; i <= n-m; ++i)
if(h[i+m]-h[i]*bp[m] == ht)//s[i:i+m]与t的哈希值相同
cnt++;
cout << cnt << '\n';
return 0;
}