【题目链接】

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)=(c0bn1+c1bn2+...+cn1b0)modM
注意:

  1. 新的基数 b b b一般大于字符串 s s s对应的数字串的基数。
  2. 基数 b b b与模数 M M M互质, b b b一般选择质数。
  3. 字符转为对应的数字选择:一般不转为数字 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=siA,那么’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")=(010+11)mod101=1
H ( " B " ) = ( 1 ∗ 1 )   m o d   101 = 1 H("B") = (1*1) \bmod 101 = 1 H("B")=(11)mod101=1,发生了哈希冲突。
如果 c i = s i − A + 1 c_i = s_i-A+1 ci=siA+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")=(110+21)mod101=12
H ( " B " ) = ( 1 ∗ 2 )   m o d   101 = 2 H("B") = (1*2) \bmod 101 = 2 H("B")=(12)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. 双哈希

使用再散列法降低哈希冲突。
双哈希:选择不同的基数 b b b和模数 M M M,用不同的方法对字符串求哈希值。

  • 如果两字符串在两种哈希方法下的哈希值都一样,则可以判定两字符串相同。
  • 如果两字符串在两种哈希方法下得到的哈希值存在不一样的情况,则判断两字符串不同。

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

  1. 枚举 S S S中所有长为 m m m的子串
  2. 判断 S S S中长为 m m m的子串与模式串 T T T是否相同,如果相同则计数加1。
  3. 最后输出计数

该算法的时间复杂度为 O ( n ⋅ m ) O(n\cdot m) O(nm),无法通过此题。

解法1:滚动哈希算法

1. 字符串哈希解题算法

枚举子串求哈希值

  1. 求出模式串 T T T的哈希值 h t ht ht
  2. 枚举主串中每个长为 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} sxsy1
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)=(c0bn1+...+cn1b0)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])=c0bk1+c1bk2+...+ck1b0
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[:k1])=c0bk2+c1bk3+...+ck2b0
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[:k1])=c0bk1+c1bk2+...+ck2b1
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[:k1])+1=c0bk1+...+ck2b1+b0
因此: H ( s [ : k ] ) = b H ( s [ : k − 1 ] ) + 1 H(s[:k])=bH(s[:k-1]) +1 H(s[:k])=bH(s[:k1])+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])=ckbn1+ck+1bn2+...+ck+n1b0
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+n1+...+ck+n1b0
已知: 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])=c0bk1+...+ck1b0
所以: 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+k1+...+ck1bn
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+k1+...+ck1bn+ckbn1+...+ck+n1b0
= 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. 具体实现

将初始化哈希值数组以及求哈希值的过程包装在类Hash中,类内逻辑固定,与外部问题无关。
如何设定模数 M M M与基数 b b b?,为了减少哈希冲突,二者都要设为质数,基数 b b b一般要大于字符串中字符的数值127,模数设大一些可以减少哈希冲突。在以上原则下,两变量的值可以任意设定。

  1. 设属性: b = 131 b=131 b=131,设bp数组,bp[i]表示 b i b^i bi,设h数组,h[i]表示字符串 s s s的前 i i i个字符的哈希值,即 H ( s [ : i ] ) H(s[:i]) H(s[:i])
    关于模数 M M M,可以设置模数,而后计算的过程中进行手动取模(包括进行数学取模)。而且h、bp数组要设为有符号整型,避免结果为负数时发生数值错误。
    或者可以将h、bp数组设为无符号整型,进行自然溢出,等价于对 2 32 2^{32} 232(unsigned),或 2 64 2^{64} 264(unsinged long long)进行取模。
  2. 在构造函数中递推求出bp数组的值。同时根据递推式 H ( s [ : k ] ) = ( b H ( s [ : k − 1 ] ) + 1 )   m o d   M H(s[:k])=(bH(s[:k-1]) +1)\bmod M H(s[:k])=(bH(s[:k1])+1)modM,求出 h h h数组的值。
  3. getHash(k, n)函数,求 H ( s [ k : k + n ] ) H(s[k:k+n]) H(s[k:k+n]),使用公式: H ( s [ k : k + n ] ) = ( H ( s [ : k + n ] ) − b n H ( s [ : k ] ) )   m o d   M H(s[k:k+n])=(H(s[:k+n])-b^nH(s[:k]))\bmod M H(s[k:k+n])=(H(s[:k+n])bnH(s[:k]))modM,可以求出该子串的哈希值。如果设置了模数 M M M,注意要使用数学取模。

主函数中:
对主串 S S S和模式串 T T T构造Hash类对象。求出主串 S S S的长度 n n n和模式串 T T T的长度 m m m
枚举主串 S S S的每个长为 m m m的子串,对于子串 s [ i : i + m ] s[i:i+m] s[i:i+m],通过调用Hash对象的成员函数getHash判断该子串的哈希值 H ( s [ i : i + m ] H(s[i:i+m] H(s[i:i+m]与模式串的哈希值 H ( t [ 0 : m ] ) H(t[0:m]) H(t[0:m])是否相同。
如果二者相同,则认为主串 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类型),或使用双哈希方法,降低哈希冲突发生的概率。
但只要是哈希算法,总是有概率发生哈希冲突的。我们只是人为忽略了这样的小概率事件发生的情况,去该题的的测试数据不会让我的哈希算法发生哈希冲突。

解法2:双哈希算法

双哈希概念见 【题目考点】中的 3. 双哈希
上述哈希过程中用到了:
b:基数。
M:模数
h数组,h[i]表示 s [ : i ] s[:i] s[:i]的哈希值
bp数组,bp[i]表示 b i b^i bi
DoubleHash类,其中设两套哈希相关的属性
第一套:模数m1=10007,基数b1=131,有h1数组对应h数组,bp1数组对应bp数组。
第二套:模数m2=10009,基数b2=137,有h2数组对应h数组,bp2数组对应bp数组。
构造函数中,对于传入的字符串,同时初始化bp1, bp2, h1, h2数组。
getHash(k, n)函数中,需要求出 s [ k : k + n ] s[k:k+n] s[k:k+n]在两套哈希方法下的哈希值,将两个哈希值构成一个pair<int, int>类型数对返回。
在比较哈希值时,可以直接使用pair<int, int>类型已经重载的==运算判断两个字符串的双哈希值是否对应相同。
主函数逻辑不变。

【题解代码】

解法1:单哈希+自然溢出
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
struct Hash
{
	int b = 131; 	
	unsigned h[N], bp[N];//h[i]:s[:i]的哈希值, bp[i]:b^i%M
	Hash(string s)
	{
		bp[0] = 1;
		h[0] = 0;
		for(int i = 1; i <= s.length(); ++i)
		{
			bp[i] = bp[i-1]*b;
			h[i] = (h[i-1]*b+s[i-1]);
		}
	}
	int getHash(int k, int n)//s[k:k+n]的哈希值 
	{
		return h[k+n]-h[k]*bp[n];
	}
}; 
int main()
{
	string s, t;
	cin >> s >> t;
	int cnt = 0, n = s.length(), m = t.length();//n:主串长度 m:模式串长度 
	Hash hs(s), ht(t);
	for(int i = 0; i <= n-m; ++i)
		if(hs.getHash(i, m) == ht.getHash(0, m))//s[i]~s[i+m-1]与t的两种哈希值都相同 
			cnt++;//注意这里理论上应该再做一遍字符串比较,比较s[i]~s[i+m-1]与t是否相同。因为哈希值相同不代表字符串相同。但如果做比较的话,就超时了。 
	cout << cnt << '\n'; 
    return 0;
}
解法2:单哈希+自定义模数
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define MOD(a, b) (((a)%(b)+(b))%(b))
const int N = 1000005;
struct Hash
{
	int b = 131, M = 1e9+7; 	
	LL h[N], bp[N];//h[i]:s[:i]的哈希值, bp[i]:b^i%M
	Hash(string s)
	{
		bp[0] = 1;
		h[0] = 0;
		for(int i = 1; i <= s.length(); ++i)
		{
			bp[i] = bp[i-1]*b%M;
			h[i] = (h[i-1]*b+s[i-1])%M;
		}
	}
	int getHash(int k, int n)//s[k:k+n]的哈希值 
	{
		return MOD(h[k+n]-h[k]*bp[n], M);
	}
}; 
int main()
{
	string s, t;
	cin >> s >> t;
	int cnt = 0, n = s.length(), m = t.length();//n:主串长度 m:模式串长度 
	Hash hs(s), ht(t);
	for(int i = 0; i <= n-m; ++i)
		if(hs.getHash(i, m) == ht.getHash(0, m))//s[i]~s[i+m-1]与t的两种哈希值都相同 
			cnt++;//注意这里理论上应该再做一遍字符串比较,比较s[i]~s[i+m-1]与t是否相同。因为哈希值相同不代表字符串相同。但如果做比较的话,就超时了。 
	cout << cnt << '\n'; 
    return 0;
}
解法3:双哈希
#include <bits/stdc++.h>
using namespace std;
#define MOD(a, b) (((a)%(b)+(b))%(b))
const int N = 1000005;
struct DoubleHash
{
	int b1 = 131, b2 = 137, m1 = 10007, m2 = 10009; 	
	int h1[N], h2[N], bp1[N], bp2[N];//h1[i]:第1种哈希方法下s[0]~s[i-1]的哈希值, bp1[i]:b1^i%m1 h2[i]:第2种哈希方法下s[0]~s[i-1]的哈希值, bp2[i]:b2^i%m2 
	DoubleHash(string s)
	{
		bp1[0] = bp2[0] = 1;
		h1[0] = h2[0] = 0;
		for(int i = 1; i <= s.length(); ++i)
		{
			bp1[i] = bp1[i-1]*b1%m1;
			bp2[i] = bp2[i-1]*b2%m2;
			h1[i] = (h1[i-1]*b1+s[i-1])%m1;
			h2[i] = (h2[i-1]*b2+s[i-1])%m2;
		}
	}
	pair<int, int> getHash(int k, int n)//取s从k开始长为n的子串的双哈希值 
	{
		return {MOD(h1[k+n]-h1[k]*bp1[n], m1), MOD(h2[k+n]-h2[k]*bp2[n], m2)};
	}
}; 
int main()
{
	string s, t;
	cin >> s >> t;
	int cnt = 0, n = s.length(), m = t.length();//n:主串长度 m:模式串长度 
	DoubleHash hs(s), ht(t);
	for(int i = 0; i <= n-m; ++i)
		if(hs.getHash(i, m) == ht.getHash(0, m))//s[i]~s[i+m-1]与t的两种哈希值都相同 
			cnt++;//注意这里理论上应该再做一遍字符串比较,比较s[i]~s[i+m-1]与t是否相同。因为哈希值相同不代表字符串相同。但如果做比较的话,就超时了。 
	cout << cnt << '\n'; 
    return 0;
}
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐