题目来源:改编自洛谷 P4305(JLOI2011)不重复数字。本 Lab 使用课程自定义输入输出协议和独立测试,不复制来源站点的代码或测试。
学习目标
前置知识与环境
先阅读第 9.2 节 散列表的“从关键字到地址”与“链地址法”两节,掌握散列函数、冲突处理与装填因子的基本口径;实现线性探测版本时可对照“开放定址法”一节。
在 Lab 目录执行 make doctor 检查环境;未安装 Make 时可在仓库根使用 pnpm 兜底命令(见下文“运行”)。
任务
对 T 组数据:每组给定 n 个整数,按输入顺序输出其中每个值第一次出现的形态(重复出现的值只保留第一个),同一组内数值之间用一个空格分隔,每组独占一行。
输入格式
- 第一行:一个整数
T,表示数据组数; - 接下来
T组数据,每组两行:- 第一行一个整数
n; - 第二行
n个整数,依次为 a₁ … aₙ。
- 第一行一个整数
输出格式
共 T 行:第 i 行为第 i 组数据“去重保首次”后的序列,数值之间用一个空格分隔;若整组数值全部相同,则该行只有一个值。
数据范围与限制
| 项目 | 范围 |
|---|---|
数据组数 T | 1 ≤ T ≤ 50 |
每组元素个数 n | 1 ≤ n ≤ 5×10⁴,且 Σn ≤ 5×10⁵ |
| 元素取值 | 0 ≤ a ≤ 10⁵ |
| 时间复杂度要求 | 每组期望 |
| 输出上限 | 8 MB(判题器放宽了默认 1 MB 输出上限;Σn 较大且基本互不相同时单行输出可超过 1 MB) |
正常、边界与错误情况
- 正常:重复值只保留首次出现,其余数值按输入顺序原样保留;
- 边界:整组数值全部相同(只输出一个值);整组互不相同(输出与输入一致);首元素与末元素相同;
- 多组数据:上一组留下的“已出现”状态必须清空,否则下一组会漏输出首次出现的值。
样例
样例 1
3
6
1 2 2 3 1 3
5
5 4 3 2 1
7
7 7 7 7 7 7 71 2 3
5 4 3 2 1
7第一组里 2 与 3 的第二次出现被丢弃,只保留首次出现;第二组互不相同,原样输出;第三组全部相同,只输出一个 7。
样例 2
2
4
0 100000 0 100000
3
9 8 90 100000
9 80 与 100000 取到值域两端;第二组首尾都是 9,只保留第一个。注意 0 也是合法元素,判重结构不能把“存着 0”和“空槽”混为一谈。
实现提示
值域只有 0…10⁵ 时,开一个 bool seen[100001] 直接判重确实可行——这本质上是散列函数 H(k)=k 的直接定址,是散列的特例。但请把本 Lab 当作手写散列表的练习:取素数表长(如 200003)做除留余数法 H(k) = k mod p,冲突时线性探测(开放定址)或拉同义词链(链地址法)。它与直接定址功能等价,但套路可以原样迁移到键值域很大的场景。
两个易错点:每组数据开始前要把散列表整体复位;若用开放定址,空槽哨兵要能与合法键 0 区分开。
运行
make run
# 免 Make 兜底:在仓库根执行
pnpm lab:run -- labs/chapter-09/exercise/E-09-02-hash-dedup-keep-first需要单用例或严格评分时使用 make run CASE=005-stress-sum5e5 与 make score;也可用 pnpm lab:score -- labs/chapter-09/exercise/E-09-02-hash-dedup-keep-first 兜底。
完成清单
思考与复盘
- 若把表长换成合数(如 200000),H(k)=k mod 200000 对什么样的输入会退化?结合教材“从关键字到地址”一节说明除留余数法为什么偏好素数。
- 线性探测在装填因子接近 1 时会发生什么?结合教材“堆积如何影响性能”一节估算平均探测次数。
- 如果元素范围改成 −10⁹ ≤ a ≤ 10⁹,直接定址数组为什么不可行?你的散列表需要改动哪几行?