01 / 本课学习路线
本课学习路线
阅读与推演约 105 分钟,练习约 75 分钟,进阶练习另需约 30 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
求 Top-K 时,先定堆的方向,再定并列时的元组符号;每来一个新候选,判断是否要替换堆顶。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 一句话说出「Top-K 大用小顶堆」的理由,并按题目改成求最小 K 个时把方向反过来 | 第 03、04 节 | 自查第 2 条、练习 3 |
| 写对「堆满 K 后比堆顶好才 heapreplace」的淘汰规则,并把并列规则写进元组符号 | 第 04 节 | 自查第 3 条、练习 4 |
| 用点积打分 + Top-K 淘汰 + 按题意重排,通过(AC)AI023 | 第 05、07 节 | 必做任务 2 |
| 用取负实现大顶堆做回合模拟,输出前还原负号,通过 P2523 | 第 06、07 节 | 自查第 4 条、必做任务 3 |
| 说出堆内顺序为什么不是答案顺序、输出前为什么要再排一次 | 第 03、08 节 | 自查第 5 条 |
下面 5 题先试着写答案,再展开对照。元组排序键与取负是上一课用过的基础;堆操作和点积现在不会也可以继续,分别学完第 03、05 节后再回答相关问题。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | import heapq; h = []; heapq.heappush(h, 5); heapq.heappush(h, 2); heapq.heappush(h, 9),此时 h[0] 是多少?heapq.heappop(h) 返回什么? | 常用内置函数与模块与 import |
| 自测 2 | (6, -2) < (6, -3) 是真还是假?(6, -3) > (3, -1) 呢? | 元组(上一课自测 1 也考过) |
| 自测 3 | 把 [8, 7, 4] 每个取负放进小顶堆,堆顶是多少?它对应原来的哪个数? | 数字与运算符 |
| 自测 4 | 向量 (1, 2) 与 (2, 2) 的点积是多少?与 (0, 3) 呢? | 本课第 05 节开头(点积 = 对应分量相乘再相加;初次接触可学完再答) |
| 自测 5 | sorted([(6, -3), (6, -2), (3, -1)], key=lambda t: (-t[0], -t[1])) 得到什么? | 列表常用操作 |
展开先修自测答案
自测 1:h[0] 是 2(堆顶永远是最小值);heappop 返回 2,之后堆顶变成 5。Python 的 heapq 只提供小顶堆。
自测 2:假(第 0 个分量相等,比第 1 个:−2 > −3);真(6 > 3)。元组比较先比第 0 个分量——这就是 (得分, −编号) 能同时表达「得分低先淘汰、同分编号大先淘汰」的原因。
自测 3:堆顶是 −8,对应原来的 8——取负后最大的数变成最小的,所以在小顶堆的堆顶。输出前要把它变回 8。
自测 4:1×2 + 2×2 = 6;1×0 + 2×3 = 6。两个候选并列,AI023 规定编号小的优先。
自测 5:[(6, -2), (6, -3), (3, -1)]——得分降序,同分按 −编号降序即编号升序:编号 2 在 3 前。这就是输出前的重排规则。
03 / 概念与术语
堆、堆顶、方向、淘汰、候选集合
堆解决的问题只有一个:在不断进来的元素里,随时拿到「当前最小(或最大)」的那个,代价是每次 O(log 元素数)。Top-K 用它维护「目前最好的 K 个」。
| 术语 | 含义 | 代码写法(heapq) |
|---|---|---|
| 堆 | 一种能 O(log n) 插入、O(1) 看最小值、O(log n) 取走最小值的容器 | import heapq;堆就是一个普通列表 |
| 小顶堆 / 大顶堆 | 堆顶是最小值 / 最大值;Python 只有小顶堆 | 大顶堆用「取负入堆、取出再取负」实现 |
| 堆顶 | 列表下标 0 的元素 | heap[0](只看不取) |
| 入堆 / 出堆 | 插入一个元素 / 取走堆顶 | heapq.heappush(heap, x) / heapq.heappop(heap) |
| 替换堆顶 | 取走堆顶并放入新元素,一次完成 | heapq.heapreplace(heap, x) |
| 建堆 | 把已有列表整理成堆 | heapq.heapify(lst)(O(n)) |
| Top-K | 只要最好的 K 个,不要全部排序 | 大小为 K 的堆 + 淘汰规则 |
| 淘汰 | 堆满 K 个后,新元素比堆顶好才替换堆顶 | if entry > heap[0]: heapreplace |
| 候选集合 | 堆里的 K 个元素只是「留下来的」,内部顺序不是排好序的 | 输出前 sorted(heap, key=…) |
堆里是候选集合,不是答案顺序
堆模块(heapq)的底层数组只保证堆序,不是有序序列。把堆直接打印出来顺序是乱的——收集完 K 个候选后,一定按题目的输出规则再排一次。第 08 节的错误表给出了直接打印堆的具体错误输出。
04 / 方向与淘汰
堆里存什么,堆顶是什么:先定方向,再定并列符号
求最大的 K 个 → 小顶堆;求最小的 K 个 → 大顶堆。理由只有一句:堆顶必须是「当前 K 个候选里最先该被淘汰的」,这样新元素只需与堆顶比较一次。
| 题目要求 | 堆的方向 | 元组写法 | 堆顶是谁 | 新元素替换条件 |
|---|---|---|---|---|
| 最大的 K 个,同分保编号小 | 小顶堆 | (得分, −编号) | 得分最低、同分编号最大 | entry > heap[0] |
| 最大的 K 个,同分保编号大 | 小顶堆 | (得分, 编号) | 得分最低、同分编号最小 | entry > heap[0] |
| 最小的 K 个,同分保编号小 | 大顶堆(取负) | (−得分, −编号) | 得分最高、同分编号最大 | entry > heap[0] |
| 最小的 K 个,同分保编号大 | 大顶堆(取负) | (−得分, 编号) | 得分最高、同分编号最小 | entry > heap[0] |
四种情况的替换条件都是「新元组比堆顶大」;变的只是元组里每个分量的符号。写代码前先把「留谁、淘汰谁」写成一行注释,再决定符号。
大小为 K 的小顶堆:淘汰规则(可直接运行)
Pythonimport heapq
# 可直接运行的最小例子:items = [(得分, 编号)],选得分最高的 k 个;同分保留编号小的
items = [(9, 1), (5, 2), (9, 3), (7, 4)]
k = 2
heap = [] # 小顶堆:堆顶 = K 个候选里最先被淘汰的
for score, idx in items:
entry = (score, -idx) # 同分时编号大的更靠堆顶(先被淘汰)
if len(heap) < k:
heapq.heappush(heap, entry)
elif entry > heap[0]:
heapq.heapreplace(heap, entry) # 移除堆顶并插入新元素,由 heapreplace 一次完成
# 堆里是候选集合,不是答案顺序——输出前按题目要求再排一次(这里:得分降序、同分编号升序)
kept = sorted(((s, -neg) for s, neg in heap), key=lambda t: (-t[0], t[1]))
print([idx for _, idx in kept]) # → [1, 3]复制即可运行,输出 [1, 3]。heapq 是小顶堆,堆顶是元组最小者。(得分, -编号) 让同分时编号大的先被淘汰、留下编号小的——并列方向以题目要求为准,这里演示的是「同分保编号小」。
手推:得分 (9,1)(5,2)(9,3)(7,4)、K = 2
入 (9,-1) 堆 = [(9,-1)]
入 (5,-2) 堆 = [(5,-2),(9,-1)] 堆顶 (5,-2) 是当前 K 个候选中优先级最低的元素
(9,-3) > (5,-2) heapreplace → 堆 = [(9,-3),(9,-1)]
(7,-4) < (9,-3) 比堆顶差,丢弃
留下编号 {1, 3} 同分 9 的两条都保住,5 和 7 被淘汰为什么不「先全部入堆最后截断」:那样堆里有 n 个元素,每次操作 O(log n),总量退回 O(n log n),放弃了 O(n log K) 的优势;n = 10⁵、K = 10 时前者约 166 万次比较、后者约 33 万次(第 08 节的运算量表)。
05 / 完整手算例:AI023
点积打分 → 入堆 / 淘汰 → 按优先顺序输出
AI023「向量 Top-K 检索」:n 个候选向量(编号从 1 起)和一个查询向量,维度 d;相似度 = 点积(对应分量相乘再相加);选相似度最高的 k 个,相似度相同编号小的优先;一行输出 k 个编号,按优先顺序、单个空格分隔。
| 候选编号 | 向量 | 点积 | 元组 (得分, −编号) | 入堆 / 淘汰 | 堆的内容 |
|---|---|---|---|---|---|
| 1 | (1, 1) | 1×1 + 2×1 = 3 | (3, −1) | 未满 2 个,入堆 | [(3,−1)] |
| 2 | (2, 2) | 1×2 + 2×2 = 6 | (6, −2) | 未满 2 个,入堆 | [(3,−1), (6,−2)] |
| 3 | (0, 3) | 1×0 + 2×3 = 6 | (6, −3) | (6,−3) > 堆顶 (3,−1) → 替换 | [(6,−3), (6,−2)] |
候选集合 {2, 3},两者相似度都是 6。按优先顺序重排(相似度降序、编号升序)→ 输出 2 3,与题面示例一致。注意堆里的内部顺序是 (6,−3) 在前——直接打印会得到 3 2。
| 候选编号 | 向量 | 点积 | 元组 | 结果 |
|---|---|---|---|---|
| 1 | (5, 5, 5) | 0 | (0, −1) | 入堆 |
| 2 | (−1, −1, −1) | 0 | (0, −2) | 入堆(k=2 未满) |
全部相似度为 0 时按编号升序:输出 1 2。零查询向量不是特例,同一套规则自然得到答案。
数据范围的含义:n ≤ 10⁵、d ≤ 16、分量绝对值 ≤ 1000,点积最大 16×10⁶ = 1.6×10⁷,Python 整数不会溢出(C++/Java 用 64 位更稳)。时间为 O(n·d) 的打分(1.6×10⁶ 次乘加)加上 O(n log₂ k) 的堆操作,按量级估算在 3 秒时限内;实际耗时以判题结果为准。
06 / 堆模拟:P2523
反复取最重的三块,按四种情形算剩块放回
P2523「回收银饰」:n 块银饰(重量正整数),每回合取三块最重的 x ≤ y ≤ z 熔掉:三者相等全熔;x=y≠z 剩 z−y;x≠y=z 剩 y−x;三者互不相等剩 |(z−y)−(y−x)|。不足三块时停止:剩两块输出较大者,剩一块输出它,没有剩输出 0。
| 情形 | 条件 | 剩块 | 例子 |
|---|---|---|---|
| ① | x = y = z | 0(全熔) | 5,5,5 → 0 |
| ② | x = y,y ≠ z | z − y | 1,1,4 → 3 |
| ③ | x ≠ y,y = z | y − x | 1,4,4 → 3 |
| ④ | 互不相等 | |(z−y) − (y−x)| | 4,7,8 → |1 − 3| = 2;6,8,10 → |2 − 2| = 0 |
情形④也可能算出 0(6, 8, 10),此时和情形①一样不放回。只有剩块大于 0 才放回堆。
手算:{8, 7, 4, 2, 1, 1}
回合1: 取 8,7,4 (x=4,y=7,z=8) → 情形④ → |(8-7)-(7-4)| = |1-3| = 2 → 放回
堆: {2,2,1,1}
回合2: 取 2,2,1 (x=1,y=2,z=2) → 情形③ (y=z) → 剩 y-x = 1 → 放回
堆: {1,1}
不足三块,剩两块 → 输出较大者 1手算:{2, 4, 6, 8, 10}(剩块为 0 的回合)
回合1: 取 10,8,6 (x=6,y=8,z=10) → 情形④ → |(10-8)-(8-6)| = 0 → 不放回
堆: {2,4}
不足三块,剩两块 → 输出较大者 4取负入堆实现大顶堆:把每块重量取负放进小顶堆,堆顶就是「最重的」;弹出三次依次得到 −z、−y、−x(弹出顺序是从大到小),算出剩块后取负放回;输出时把堆顶取负还原。题目页参考题解用每回合整体排序再弹末尾三个,n ≤ 40 时同样够快,两种写法结果相同。
07 / 从步骤到程序
两道必做题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。
| 步骤 | 代码 | 说明 |
|---|---|---|
| 读 n d k 与查询向量 | data = sys.stdin.read().split(),切前 3 + d 个记号 | 一次读完按位置切 |
| 逐候选打分 | sum(int(a) * b for a, b in zip(v, q)) | 点积;编号从 1 起 |
| 入堆 / 淘汰 | (score, -idx);未满 heappush,否则 entry > heap[0] 时 heapreplace | 第 04 节的四行 |
| 按优先顺序输出 | sorted(heap, key=lambda t: (-t[0], -t[1])) | 相似度降序、编号升序;不能直接打印堆 |
展开完整参考程序 1:AI023 向量 Top-K 检索(先自己写完并提交一次,再展开对照)
完整程序:AI023(标准输入 → 标准输出)
Pythonimport sys
import heapq
data = sys.stdin.read().split()
n, d, k = int(data[0]), int(data[1]), int(data[2])
q = [int(x) for x in data[3:3 + d]]
heap = [] # 小顶堆:堆顶 = 当前 k 个候选里最先被淘汰的
pos = 3 + d
for idx in range(1, n + 1): # 候选编号从 1 起
v = data[pos:pos + d]
pos += d
score = sum(int(a) * b for a, b in zip(v, q)) # 点积;Python 整数不会溢出
entry = (score, -idx) # 同分时编号大的更靠堆顶(先被淘汰),留下编号小的
if len(heap) < k:
heapq.heappush(heap, entry)
elif entry > heap[0]:
heapq.heapreplace(heap, entry)
# 堆里只是候选集合;按题目要求的优先顺序(相似度降序、编号升序)再排一次
kept = sorted(heap, key=lambda t: (-t[0], -t[1]))
print(" ".join(str(-neg) for _, neg in kept))自测用例:题面两个示例、练习 1 的 k=n 输入、第 08 节错误表的 k=1 同分输入(含零查询向量、负分量)。
展开完整参考程序 2:P2523 回收银饰
完整程序:P2523(标准输入 → 标准输出)
Pythonimport sys
import heapq
data = sys.stdin.read().split()
n = int(data[0])
heap = [-int(x) for x in data[1:1 + n]] # 取负入堆:小顶堆变成「最重的在堆顶」
heapq.heapify(heap)
while len(heap) >= 3: # 至少三块才开始一回合
z = -heapq.heappop(heap) # 弹出顺序:最重、次重、第三重
y = -heapq.heappop(heap)
x = -heapq.heappop(heap) # 于是 x <= y <= z
if x == y == z:
rest = 0
elif x == y:
rest = z - y
elif y == z:
rest = y - x
else:
rest = abs((z - y) - (y - x))
if rest > 0:
heapq.heappush(heap, -rest) # 非零才放回(记得取负)
print(-heap[0] if heap else 0) # 剩两块输出较大者(堆顶);剩一块输出它;空堆输出 0自测用例:第 06 节两组手算例,以及 1 块、2 块、正好三块全熔(3 / 1 2 3)、4 / 5 5 5 1 四个边界(后两个在练习 5)。循环条件是堆大小 ≥ 3;rest > 0 才放回;输出前取负还原。
08 / 边界、反例与复杂度
错误做法在具体输入上各输出什么,以及三种选法的运算量
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| AI023 方向选反:用大顶堆(元组取负)但沿用同一替换规则 | 题面示例 1 | 留下的是得分 3 和 6 的编号 1、3 → 3 1 | 2 3 | 答案错误(WA) |
| AI023 并列符号写反:元组 (得分, 编号) | k=1 的输入:查询 (3),候选 −2、9、9、1、0 | 同分 27 时留下编号大的 → 3 | 2 | 答案错误(WA) |
| AI023 直接打印堆内顺序、不重排 | 题面示例 1 | 3 2 | 2 3 | 答案错误(WA) |
| AI023 先全部入堆再取前 k | n = 10⁵ | 结果正确,但 O(n log n) 且内存 O(n) | 同左 | 按量级估算仍在时限内;写法不可迁移到更大规模 |
| P2523 输出前忘记还原负号 | 5 / 2 4 6 8 10 | -4 | 4 | 答案错误(WA) |
| P2523 循环条件写成堆非空 | 2 / 3 9 | 弹第三块时堆已空,抛出 IndexError | 9 | 运行错误(RE) |
| P2523 剩块为 0 也放回 | 5 / 2 4 6 8 10 | 放回 0 后堆 {4,2,0} 再熔一回合:|(4−2)−(2−0)| = 0 又放回 → 堆 {0} → 输出 0 | 4 | 答案错误(WA) |
第一行的错误输出计算:大顶堆下「新元组 > 堆顶」留下的是得分较小的候选 (3,1) 和 (6,3),按得分降序输出 3 1。第二行五个候选的得分:−6、27、27、3、0。
| 选法 | 运算量 | K = 10 时 | 适用 |
|---|---|---|---|
| 全部排序取前 K | m log₂ m | ≈ 166 万次比较 | K 接近 m,或代码最简单优先 |
| 大小为 K 的堆 | m log₂ K + K log₂ K | ≈ 33 万 + 33 次 | K 远小于 m(本课) |
| 按得分开桶 | O(m + 值域) | ≈ 10⁵ + 值域 | 得分是有限范围的整数 |
本课的题用排序或堆按量级估算都在时限内;桶留作知识储备。K 越小堆省得越多,K 接近 m 时两者持平、直接排序更简单。
补充学习(选学)排序、堆、桶:三种选法的运算量约 5 分钟m=10⁵ 代入的运算量对比与选择标准
m = 100 000 个候选、K = 10:全部排序 m log₂ m ≈ 166 万次比较;大小为 K 的堆 m log₂ K ≈ 33 万次,再加整理结果的 K log₂ K ≈ 33 次。K 越小堆省得越多;K 接近 m 时两者持平,直接排序更简单。
得分范围有限时还有第三条路:按得分开桶,从高分桶往低分桶收集,收满 K 个就停,O(n + 值域)。本课的题用排序或堆都在时限内很从容,桶留作知识储备。
补充学习(选学)Top-K 在采样与向量检索中的应用约 4 分钟大模型 top-k 采样与向量检索两个场景
启用 top-k 采样时,大模型每生成一个词元(token)要从大量候选打分里挑出前 K 个再采样。当候选数远大于 K 时,可用 n·log₂K 与 n·log₂n 粗略比较两种方法的运算量,而且每个词元都要做一次。实际由 GPU 优化的 top-k 算子完成,但复杂度的计算方式相同。
向量检索「找最相近的 K 条」方向要反过来:留 K 个最小距离 → 大顶堆,元组 (-距离, -编号),新点更近才换堆顶。和「留最大 K 个」正好镜像;并列细节以题目约定为准。AI023 的点积打分 + Top-K 就是这个场景对应的练习题。
09 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):AI023 输入「n=4,d=2,k=4;查询 (2, 1);候选 (1,0)、(0,1)、(−3,4)、(2,2)」,按第 05 节表的格式算出每个点积和最终输出。
展开练习 1 答案
点积:2、1、−2、6。k = n,四个都入堆、没有淘汰;按相似度降序输出编号 4 1 2 3。做错最常见的原因:k = n 时仍以为要淘汰,或忘了负分也是合法得分。
练习 2(改一个条件):题目改成「相似度相同时编号大的优先」,元组怎么改?用 k=1 的输入(查询 (3);候选 −2、9、9、1、0)写出输出。这是变式,只在本地验证;提交原题仍要用原规则「同分保编号小」。
展开练习 2 答案
元组改成 (得分, 编号):同分时编号小的更靠堆顶、先被淘汰。得分 −6、27、27、3、0;k=1 时 (27,2) 与 (27,3) 之间留下 (27,3) → 输出 3(原规则输出 2)。输出重排的键也要同步改成 (-得分, -编号)。
练习 3(改一个条件):改为保留得分最低的 k 个、同分仍保编号小——完成「进阶练习 1」的 bottomk_ids 并让断言通过;说出与正向版本相比改了哪三处。
展开练习 3 答案
bottomk_ids 的参考实现(自带断言)
Pythonimport heapq
def bottomk_ids(scores, k):
heap = []
for score, idx in scores:
entry = (-score, -idx) # 方向整体反过来:大顶堆;同分仍保编号小
if len(heap) < k:
heapq.heappush(heap, entry)
elif entry > heap[0]:
heapq.heapreplace(heap, entry)
return sorted(-neg for _, neg in heap) # 本自测要求编号升序输出
assert bottomk_ids([(9, 1), (5, 2), (9, 3), (7, 4)], 2) == [2, 4]
assert bottomk_ids([(5, 1), (5, 3)], 1) == [1]三处改动:① 得分取负(大顶堆,堆顶是当前 K 个里得分最高的);② 同分规则跟着取负仍是 −编号;③ 输出规则按自测要求改为编号升序。替换条件 entry > heap[0] 不变。
练习 4(独立实现):完成「代码自测」任务的 topk_ids 让两条断言通过,再加一条断言:AI023 题面示例 1 的得分 [(3,1),(6,2),(6,3)]、k=2 应得到 [2, 3]。
展开练习 4 答案
topk_ids 的参考实现(自带断言)
Pythonimport heapq
def topk_ids(scores, k):
heap = []
for score, idx in scores:
entry = (score, -idx) # 同分时编号大的更靠堆顶,先被淘汰
if len(heap) < k:
heapq.heappush(heap, entry)
elif entry > heap[0]:
heapq.heapreplace(heap, entry)
return [-neg for _, neg in sorted(heap, key=lambda t: (-t[0], -t[1]))] # 得分降序、同分编号升序
assert sorted(topk_ids([(9, 1), (5, 2), (9, 3), (7, 4)], 2)) == [1, 3]
assert topk_ids([(9, 1), (9, 3)], 1) == [1]
assert topk_ids([(3, 1), (6, 2), (6, 3)], 2) == [2, 3] # AI023 题面示例 1 的得分返回前按 (−得分, −(−编号)) 即得分降序、编号升序重排;第三条断言就是题面示例 1。
练习 5(迁移):不运行程序,按第 06 节的格式手算 P2523 输入 4 / 5 5 5 1 和 3 / 1 2 3 的输出。
展开练习 5 答案
5 5 5 1:取 5,5,5 → 情形① 全熔,不放回 → 堆 {1} → 剩一块 → 输出 1。1 2 3:取 3,2,1 → 情形④ → |(3−2)−(2−1)| = 0 → 不放回 → 堆空 → 输出 0。做错最常见的原因:情形④算出 0 时仍放回,导致多熔一回合。
10 / 读题要求与复习自评
两道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | AI023 向量 Top-K 检索 | P2523 回收银饰 |
|---|---|---|
| 输入 | n d k;d 个整数(查询向量);n 行各 d 个整数 | n;n 个正整数重量 |
| 得分 / 规则 | 点积;相似度降序,同分编号小优先 | 每回合取最重三块,四种情形算剩块,非零放回 |
| 堆的方向 | 小顶堆,元组 (得分, −编号) | 大顶堆(取负) |
| 输出 | 一行 k 个编号,单个空格分隔,按优先顺序 | 一个整数:剩两块取较大、剩一块取它、空则 0 |
| 数据范围 | n ≤ 10⁵,d ≤ 16,|分量| ≤ 1000,1 ≤ k ≤ n | 1 ≤ n ≤ 40,重量 ∈ [1, 2000] |
| 样例 | 3 2 2 / 1 2 / 1 1 / 2 2 / 0 3 → 2 3 | 题目页为准(自拟 8 7 4 2 1 1 → 1) |
题解入口:需要对照解法时,先展开本课第 07 节的两份完整参考程序;两道题的题目页另有思路与参考代码,可在题目页查看。复习与自评:本课算完成 = 两道必做题 AI023、P2523 都通过判题,并勾选全部六条「学习完成检查」;进阶练习与复习题不影响完成状态。六条检查是自评,不改变题目的通过(AC)状态。复习时用三个问题自测:① 不看正文,写出第 04 节决策表的四行;② 不看表格,重算题面示例 1 的三个点积并说出为什么输出 2 3 而不是 3 2;③ 说出 P2523 情形④算出 0 时该怎么处理。答不出哪一条,就回到对应的节重读,再做第 09 节对应的练习。
基础加练(选做)
同一主题的 1 道基础题
这组题目采用标准输入输出(ACM 模式):程序从标准输入读取数据、把结果写到标准输出。它们难度低于必做题,适合在必做题之前热身,或在未通过时回来巩固;不计入本课完成标准。每道题给出练习重点、完成标准与提示;已在本站通过的题目会直接显示为已通过。
11 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道
实现前 K 个最大值的编号筛选并验证并列规则(topk_ids)
必做任务 1:代码自测自主练习练习重点:小顶堆保 K 大;同分保编号小的元组设计;预计用时:15 分钟
完成标准:能不看讲解说出堆顶为什么是「得分最低、同分编号最大」
需要时查看提示
元组 (score, -idx):得分(score)小的先到堆顶;同分时编号取负(-idx)更小、即编号(idx)更大的先到堆顶,先被淘汰。跑通两个断言再提交判题。参考实现在第 09 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
import heapq
def topk_ids(scores, k):
# scores = [(得分, 编号)];返回得分最高的 k 个编号
# 并列约定(本自测):同分保留编号更小的
# 请在这里实现:小顶堆 + (得分, -编号) 元组
...
assert sorted(topk_ids([(9, 1), (5, 2), (9, 3), (7, 4)], 2)) == [1, 3]
# 同分边界:K=1 时 (9,1) 和 (9,3) 只留编号小的 1
assert topk_ids([(9, 1), (9, 3)], 1) == [1]AI023 · 向量 Top-K
必做任务 2练习重点:点积算相似得分,再按 Top-K 淘汰规则选出结果;预计用时:35 分钟
完成标准:能整段说出「打分 → 入堆 → 淘汰 → 按题意排序输出」四步
需要时查看提示
得分是整数点积,累加用 Python 原生 int 不会溢出。并列规则按题目说明中的那一句写比较元组——先读清同分时保编号小还是大,再定 -idx 的正负。输出顺序按题目要求再排一次,不要直接输出堆的内部顺序。第 05 节把两个题面示例逐候选列出。
P2523 · 回收银饰
必做任务 3练习重点:大顶堆反复取三块最重的,按规则算剩块放回;预计用时:25 分钟
完成标准:能按 x≤y≤z 四种情形口述剩块规则,并解释取负入堆
需要时查看提示
取负入堆实现大顶堆;每回合弹三次得到 -z,-y,-x(注意弹出顺序是从大到小),按题目说明的四种情形算剩块,非零才放回。循环条件是堆大小 ≥ 3;结束后剩两块输出较大者、剩一块输出它、空堆输出 0。第 06 节有两组逐回合手算。
改为保留 K 个最小值(bottomk_ids)
进阶练习 1:代码自测进阶练习自主练习练习重点:把方向整体反过来:大顶堆、元组 (-得分, -编号),同分仍保编号小;预计用时:30 分钟
完成标准:能说出为什么 (-得分, -编号) 恰好让「得分最高、同分编号最大」的候选先被淘汰
需要时查看提示
留 K 个最小 → 堆顶要放「当前 K 个里最大的」→ 大顶堆 → 取负。同分规则也要跟着取负:(-得分, -编号) 里 -编号 更小(编号更大)的先被淘汰。两个断言过了,再回头对比正向版本,把改动的三处标出来。参考实现在第 09 节练习 3 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
import heapq
def bottomk_ids(scores, k):
# scores = [(得分, 编号)];返回得分最低的 k 个编号(升序)
# 并列约定(本自测):同分保留编号更小的
# 请在这里实现:方向反过来——大顶堆,元组 (-得分, -编号)
...
assert bottomk_ids([(9, 1), (5, 2), (9, 3), (7, 4)], 2) == [2, 4]
# 同分边界:K=1 时 (5,1) 和 (5,3) 只留编号小的 1
assert bottomk_ids([(5, 1), (5, 3)], 1) == [1]提交结果
提交结果说明与处理方法
- WA
答案错误
三个常见错误:堆方向选反(求最大的 K 个却建了大顶堆)、同分并列的 -idx 正负写反、输出直接按堆内顺序没按题意重排——第 08 节的表给出了每种错误的具体输出
- RE
运行错误
堆空时取堆顶会报错:回收银饰的循环条件先判断堆的长度(len(heap) >= 3);元素总数不足 K 也要单独处理
- TLE
超时
先全量入堆再截断、或每轮重新排序整个列表——都退化回 O(n log n),按淘汰规则重写
- PE
格式错误
输出 K 个编号的分隔符与顺序对样例逐字符核对
- AC
通过
说一遍:题目改成求最小的 K 个时,堆的方向和比较元组分别要怎么改。
12 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。