题目来源:改编自洛谷 P3370【模板】字符串哈希。本 Lab 使用课程自定义输入输出协议和独立测试,不复制来源站点的代码或测试。
学习目标
前置知识与环境
先阅读第 9.2 节 散列表的“从关键字到地址”与“装填因子”两节:本 Lab 的关键字是任意字符串,散列函数要先把串压缩成一个整数,再对表长取模落到桶下标;表长与装填因子直接决定冲突概率。
在 Lab 目录执行 make doctor 检查环境;未安装 Make 时可在仓库根使用 pnpm 兜底命令(见下文“运行”)。
任务
给定 n 个字符串(大小写敏感),输出其中不同字符串的个数。
输入格式
- 第一行:一个整数
n,表示字符串个数; - 接下来
n行:每行一个字符串,由大小写字母与数字组成。
输出格式
一行一个整数:不同字符串的个数。
数据范围与限制
| 项目 | 范围 |
|---|---|
字符串个数 n | 1 ≤ n ≤ 5×10⁴ |
| 字符串长度 | 1 ~ 60 |
| 字符集 | 大小写字母与数字,大小写敏感(Ab 与 ab 是两个串) |
| 时间复杂度要求 | 期望 |
正常、边界与错误情况
- 正常:完全相同的串只计一次;
- 边界:全部相同(答案为 1);全部不同(答案为 n);长度为 1 的单字符串;
- 大小写:
Ab、ab、AB互不相同,任何“先统一大小写”的归一化都会算错; - 同义词:两个不同串的散列值可能相同(冲突),此时仍要比较原串,不能漏计。
样例
样例 1
5
aaaa
abc
abc
abcc
123454abc 出现两次只计一次,不同的是 aaaa、abc、abcc、12345 共 4 个(即原题样例口径)。
样例 2
6
Ab
ab
AB
Ab
a
15大小写敏感:Ab、ab、AB 是三个不同的串;Ab 的第二次出现被丢弃;单字符 a 与 1 各算一个,共 5 个。
实现提示
散列函数用 131 进制多项式散列(BKDR 风格):h = h * 131 + c,用 unsigned long long 存放,溢出自然丢弃——这等价于对 2⁶⁴ 取模。桶下标再对素数表长(如 131071)取模:散列值域是 2⁶⁴,远大于桶数,必须再压缩一次。
冲突处理用链地址法:节点上同时存散列值和原串,插入时先比较散列值(整数不等可直接排除),相等再比较原串。“只比散列值不比原串”在理论上不安全——两个不同串完全可能撞出同一个散列值。整表统计也可以“读入后排序去重”完成(
运行
make run
# 免 Make 兜底:在仓库根执行
pnpm lab:run -- labs/chapter-09/exercise/E-09-07-string-hash-count需要单用例或严格评分时使用 make run CASE=006-stress-n5e4 与 make score;也可用 pnpm lab:score -- labs/chapter-09/exercise/E-09-07-string-hash-count 兜底。
完成清单
思考与复盘
- 131 进制下,两个不同字符串的散列值何时相同?“只比散列值不比原串”在理论上不安全、在实践中却几乎不出错,这组矛盾怎么权衡?
- 自然溢出已经等价于 mod 2⁶⁴,为什么桶下标还要对 131071 再取模?若把表长直接换成 2¹⁷(合数),分布会发生什么变化?
- 若字符串长度上限放大到 10⁴,整串散列的成本如何变化?“只散列前若干位 + 长度”这类折中会牺牲什么?