【模板:字符串哈希】信息学奥赛一本通 1455:【例题1】Oulipo

【题目链接】

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

注意:

  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=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

  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(n⋅m),无法通过此题。

正解解法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} 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. 具体实现
  1. 设 b = 131 b=131 b=131bp数组,bp[i]表示 b i b^i bi,递推求出bp数组的值。
  2. 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数组。
  3. 根据公式: 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
  4. 枚举主串 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;
}
相关推荐
fengfuyao9852 小时前
基于Matlab的压缩感知梯度投影重构算法实现方案
算法·matlab·重构
快手技术2 小时前
打破信息茧房!快手搜索多视角正样本增强引擎 CroPS 入选 AAAI 2026 Oral
后端·算法·架构
e***98572 小时前
MATLAB高效算法实战:从基础到进阶优化
开发语言·算法·matlab
CoderCodingNo2 小时前
【GESP】C++五级练习(前缀和练习) luogu-P1387 最大正方形
开发语言·c++·算法
MicroTech20252 小时前
MLGO微算法科技通过 Lindbladians 设计线性微分方程的近似最优量子算法——开放量子系统框架下的量子ODE求解新范式
科技·算法·量子计算
知乎的哥廷根数学学派2 小时前
基于多尺度特征提取和注意力自适应动态路由胶囊网络的工业轴承故障诊断算法(Pytorch)
开发语言·网络·人工智能·pytorch·python·算法·机器学习
源代码•宸2 小时前
Leetcode—85. 最大矩形【困难】
经验分享·算法·leetcode·职场和发展·golang·单调栈
平哥努力学习ing2 小时前
《数据结构》-第八章 排序
数据结构·算法·排序算法
CoovallyAIHub2 小时前
为AI装上“纠偏”思维链,开源框架Robust-R1显著提升多模态大模型抗退化能力
深度学习·算法·计算机视觉