字符串哈希
1. 回忆:哈希函数与哈希冲突
• 哈希函数:将关键字映射成对应的地址的函数,记为 Hash(key) = Addr 。
• 哈希冲突:哈希函数可能会把两个或两个以上的不同关键字映射到同⼀地址,这种情况称为哈希冲 突。
2. 字符串哈希
定义⼀个把字符串映射到整数的函数hash ,这就是字符串哈希。说⽩了,就是将⼀个字符串⽤⼀个 整数表⽰。
3. 字符串哈希中的哈希函数
在字符串哈希中,有⼀种冲突概率较⼩的哈希函数,将字符串映射成 p 进制数字:
hash ( s ) = s [ i ] × p ( i =0 ∑ n −1 n − i −1 mod M )
其中p, 通常取质数131 或者13331 。如果把哈希值定义为 unsigned long long 类型,在 C++ 中,溢出就会⾃动取模。
(你没有看错,字符串哈希就是背⼀个公式即可......)
但是,实际求哈希值时,我们⽤的是 前缀哈希 的思想来求,这样会和下⾯的多次询问⼦串哈希⼀致。
4. 前缀哈希数组
单次计算⼀个字符串的哈希值复杂度是 O ( N )。如果需要多次询问⼀个字符串的⼦串的哈希值,每次 重新计算效率⾮常低下。
⼀般利⽤ 前缀和思想先预处理字符串中每个前缀的哈希值 ,这样的话每次就能快速求出⼦串的哈希
了。
cpp
typedef unsigned long long ULL;
const int N = 1e6 + 10, P = 13331;
char s[N];
int len;
ULL f[N]; // 前缀哈希数组
ULL p[N]; // 记录 p 的 i 次⽅
// 处理前缀哈希数组以及 p 的 i 次⽅数组
void init_hash()
{
f[0] = 0; p[0] = 1;
for(int i = 1; i <= len; i++)
{
f[i] = f[i - 1] * P + s[i];
p[i] = p[i - 1] * P;
}
}
// 快速求得任意区间的哈希值
ULL get_hash(int l, int r)
{
return f[r] - f[l - 1] * p[r - l + 1];
}
如果题⽬只是简单的求单个字符串的哈希值:
cpp
typedef unsigned long long ULL;
const int N = 1e6 + 10;
int len;
char s[N];
ULL gethash()
{
ULL ret = 0;
for(int i = 1; i <= len; i++)
{
ret = ret * p + s[i];
}
return ret;
}
1.1 【模板】字符串哈希
题⽬来源: 洛⾕
题⽬链接: P3370 【模板】字符串哈希
难度系数: ★★
题目描述
如题,给定 N 个字符串(第 i 个字符串长度为 Mi,字符串内包含数字、大小写字母,大小写敏感),请求出 N 个字符串中共有多少个不同的字符串。
友情提醒:如果真的想好好练习哈希的话,请自觉。
输入格式
第一行包含一个整数 N,为字符串的个数。
接下来 N 行每行包含一个字符串,为所提供的字符串。
输出格式
输出包含一行,包含一个整数,为不同的字符串个数。
输入输出样例
输入 #1复制
5
abc
aaaa
abc
abcc
12345
输出 #1复制
4
说明/提示
数据范围
对于 30% 的数据:N≤10,Mi≈6,Mmax≤15。
对于 70% 的数据:N≤1000,Mi≈100,Mmax≤150。
对于 100% 的数据:N≤10000,Mi≈1000,Mmax≤1500。
样例说明
样例中第一个字符串 abc 和第三个字符串 abc 是一样的,所以所提供字符串的集合为 {aaaa,abc,abcc,12345},故共计 4 个不同的字符串。
拓展阅读
以下的一些试题从不同层面体现出了字符串哈希算法的正确性分析。
- P12197 Hash Killer I
- P12198 Hash Killer II
- P12199 (目前无解)Hash Killer III
- P12200 Hash Killer Extra
- P12201 Hash Killer Phantasm
- P7350 「MCOI-04」Dream and Strings
【解法】
字符串哈希模板题,就是背⼀个哈希函数~
【参考代码】
cpp
#include <iostream>
#include <string> // 字符串操作必备(原代码漏了,会编译报错!)
#include <algorithm>// sort排序函数
using namespace std;
typedef unsigned long long ULL; // 无符号长整型:溢出自动取模,避免哈希冲突
const int N = 1e4 + 10; // 最多1万个字符串,留10余量
const int P = 131; // 哈希基数(经验值,131/13331都可以)
int n; // 字符串个数
ULL a[N]; // 存储每个字符串的哈希值
// 字符串哈希函数:输入字符串s,返回对应的哈希值
ULL get_hash(string& s)
{
ULL ret = 0; // 初始哈希值为0
// 遍历字符串每个字符(s.size()是字符串长度)
for(int i = 0; i < s.size(); i++)
{
// 核心公式:当前哈希值*P + 当前字符的ASCII码
ret = ret * P + s[i];
}
return ret;
}
int main()
{
cin >> n; // 读入字符串个数
// 遍历每个字符串,计算哈希值并存入数组a
for(int i = 1; i <= n; i++)
{
string s;
cin >> s; // 读入字符串
a[i] = get_hash(s); // 计算哈希值并存储
}
// 排序:相同哈希值会排在一起,方便去重统计
sort(a + 1, a + 1 + n);
int ret = 1; // 至少有1个不同字符串,初始值为1
// 遍历排序后的哈希数组,统计不同值的数量
for(int i = 2; i <= n; i++)
{
// 如果当前哈希值和前一个不同,说明是新字符串
if(a[i] != a[i - 1])
ret++;
}
cout << ret << endl; // 输出不同字符串的数量
return 0;
}
1.2 兔⼦与兔⼦
题⽬来源: 洛⾕
题⽬链接: P10468 兔⼦与兔⼦
难度系数: ★★
题目描述
很久很久以前,森林里住着一群兔子。
有一天,兔子们想要研究自己的 DNA 序列。
我们首先选取一个好长好长的 DNA 序列(小兔子是外星生物,DNA 序列可能包含 26 个小写英文字母)。
然后我们每次选择两个区间,询问如果用两个区间里的 DNA 序列分别生产出来两只兔子,这两个兔子是否一模一样。
注意两个兔子一模一样只可能是他们的 DNA 序列一模一样。
输入格式
第一行输入一个 DNA 字符串 S。
第二行一个数字 m,表示 m 次询问。
接下来 m 行,每行四个数字 l1,r1,l2,r2,分别表示此次询问的两个区间,注意字符串的位置从 1 开始编号。
输出格式
对于每次询问,输出一行表示结果。
如果两只兔子完全相同输出 Yes,否则输出 No(注意大小写)。
输入输出样例
输入 #1复制
aabbaabb
3
1 3 5 7
1 3 6 8
1 2 1 2
输出 #1复制
Yes
No
Yes
说明/提示
数据保证,1≤∣S∣,m≤106。其中,∣S∣ 为字符串 S 的长度。
【解法】
构造字符串的前缀哈希数组,在前缀哈希数组中快速拿到区间的哈希值。
【参考代码】
cpp
#include <iostream>
#include <string> // 字符串操作必备
using namespace std;
typedef unsigned long long ULL; // 无符号长整型:溢出自动取模,降低冲突
const int N = 1e6 + 10; // 字符串最长1e6,留10余量
const int P = 13331; // 哈希基数(比131更大,冲突概率更低)
int n; // 字符串长度(补空格后)
string s; // 存储DNA字符串(补前导空格,下标从1开始)
ULL f[N]; // 前缀哈希数组:f[i] = 前i个字符的哈希值
ULL p[N]; // 幂次数组:p[i] = P^i
// 初始化前缀哈希和幂次数组
void init_hash()
{
p[0] = 1; // 初始化:P^0 = 1(任何数的0次方都是1)
for(int i = 1; i <= n; i++)
{
// 前缀哈希公式:前i个字符的哈希 = 前i-1个的哈希*P + 第i个字符的ASCII码
f[i] = f[i - 1] * P + s[i];
// 幂次更新:P^i = P^(i-1) * P
p[i] = p[i - 1] * P;
}
}
// 计算区间[l,r]的哈希值
ULL get_hash(int l, int r)
{
// 核心公式:区间哈希 = 前r个哈希 - 前l-1个哈希 * P^(r-l+1)
return f[r] - f[l - 1] * p[r - l + 1];
}
int main()
{
cin >> s; // 读入DNA字符串(比如"aabbaabb")
n = s.size(); // 原字符串长度(8)
s = " " + s; // 补前导空格,让字符串下标从1开始(变成" aabbaabb")
init_hash(); // 初始化哈希数组
int m;
cin >> m; // 读入询问次数
while(m--)
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2; // 读入两个区间
// 计算两个区间的哈希值
ULL hash1 = get_hash(l1, r1);
ULL hash2 = get_hash(l2, r2);
// 哈希值相同→子串相同,否则不同
if(hash1 == hash2)
cout << "Yes" << endl;
else
cout << "No" << endl;
}
return 0;
}