洛谷P3370字符串哈希(集合:Hash表)
P3370 【模板】字符串哈希
题目描述
如题,给定 NNN 个字符串(第 iii 个字符串长度为 MiM_iMi,字符串内包含数字、大小写字母,大小写敏感),请求出 NNN 个字符串中共有多少个不同的字符串。
友情提醒:如果真的想好好练习哈希的话,请自觉。
输入格式
第一行包含一个整数 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≤15M_{\max}\leq 15Mmax≤15。
对于 70%70\%70% 的数据:N≤1000N\leq 1000N≤1000,Mi≈100M_i≈100Mi≈100,Mmax≤150M_{\max}\leq 150Mmax≤150。
对于 100%100\%100% 的数据:N≤10000N\leq 10000N≤10000,Mi≈1000M_i≈1000Mi≈1000,Mmax≤1500M_{\max}\leq 1500Mmax≤1500。
样例说明
样例中第一个字符串 abc\tt{abc}abc 和第三个字符串 abc\tt{abc}abc 是一样的,所以所提供字符串的集合为 {aaaa,abc,abcc,12345}\{\tt{aaaa},\tt{abc},\tt{abcc},\tt{12345}\}{aaaa,abc,abcc,12345},故共计 444 个不同的字符串。
题解
#include<iostream>
#include<string>
#include<vector>
#define Max 1050 //字符串的最大长度
#define base 257 //哈希计算的基数
#define mod 23333 // 哈希表的大小(取模值)
using namespace std;int n, ans; //n:输入字符串的数量;ans:不同字符串的数量
string s; //输入的字符串
vector<string> linker[mod + 2]; //哈希表主体,每个位置是一个字符串向量inline void insert() {int hash = 1; //初始化哈希值//计算字符串的哈希值for (int i = 0; s[i]; i++) {hash = (hash * 111 * base) % mod;}string t = s;//检查当前哈希值对应的数组中是否已有该字符串(处理哈希冲突)for (int i = 0; i < linker[hash].size(); i++) {if (linker[hash][i] == t)return; //已存在则直接返回,不重复计数}//不存在则插入哈希表并计数+1linker[hash].push_back(t);ans++;
}int main() {cin >> n;for (int i = 1; i <= n; i++) {cin >> s;insert();}cout << ans;return 0;
}
补充说明:
在哈希函数里,像代码中 hash = (hash * 111 * base + s[i]) % mod
这样选取 111 和 base 这类乘数,主要是为了让哈希值尽可能随机、均匀分布,以此降低哈希冲突概率,它们的选取一般要遵循这些原则:
1. 与字符集、模数适配
和字符的数值范围匹配:要是处理的是 ASCII 字符(范围 0 - 127 左右 )或者扩展 ASCII 字符(0 - 255 ),乘数选成能让字符数值参与运算后,哈希值变化更丰富的数。比如 base 选 261(大于常见字符集大小 ),和字符数值结合运算时,能让不同字符对哈希值的贡献更有区分度,避免 “字符数值小,乘以小乘数后变化微弱,容易撞哈希” 的情况。
和模数 mod 协同:尽量让乘数和 mod 互质(或者至少不是倍数关系 ),这样每一步哈希计算时,hash * 乘数 + 字符 的结果再取模,能让哈希值在 mod 范围内更均匀分散。像 mod 是 23333(质数 ),111 和 261 跟它互质的话,能减少哈希值扎堆,降低冲突概率。
2. 让哈希计算更 “发散”
增大哈希值差异:乘数要选成能放大不同字符、不同字符串位置差异的数。比如 111 是一个相对适中的数,每一轮哈希计算用它去乘之前的哈希值,再结合新字符,能让字符串里哪怕微小的差异(像不同位置换个字符 ),经过多轮运算后,哈希值差异被放大,让最终哈希结果区别更明显,不容易撞哈希。
避免 “乘法陷阱”:别选太小的乘数(比如 1、2 这类 ),否则不同字符或者不同字符串,哈希计算后结果容易趋同。也别选极端大的数(导致中间结果溢出、计算变慢 ,不过代码里用了取模,一定程度能缓解,但数值太大会让计算规律变弱,也可能有问题 ),要在 “区分度” 和 “计算可行性” 之间找平衡,111、261 就属于能较好制造差异,又不会让计算太离谱的数。
3. 适配实际场景与经验
经验性选取:很多时候,这类乘数是靠经验、或者测试调整出来的。比如在处理字符串哈希时,大家试过不同的基数(像 113、257、261 等 ),发现用这些数时,哈希冲突概率相对低,就会保留下来复用。111 可能也是经过实践,或者结合题目数据特点选的,用它能让代码在处理给定输入(比如题目里的字符串数据 )时,哈希表现更稳定。
适配数据特点:要是知道要处理的字符串有特定规律(比如全是小写字母、长度都很短等 ),可以针对性选乘数。比如全是小写字母时,乘数选成和 26(字母数量 )、256(ASCII 范围 )等有关联、又能制造差异的数,让哈希更适配数据,减少冲突。