01 / 本课学习路线
本课学习路线
阅读与推演约 126 分钟,练习约 60 分钟,进阶练习另需约 70 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
先核对并列优先级与更新动作,再用下面的自测检查元组比较、前缀和、二分与堆的基础。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 算出 KV 缓存把 100 步生成的 K/V 投影从 5050 份降到 100 份 | 第 04 节 | 自查第 2 条 |
| 说出 AI007 的三层并列规则与更新动作 | 第 05 节 | 自查第 3 条、练习 1 |
| 解释 AI008 为什么可以二分(单调性从哪来) | 第 06 节 | 自查第 4 条、练习 2 |
| 不查看笔记写出 RoPE 的配对、频率、旋转三步公式 | 第 08 节 | 自查第 5 条、练习 4 |
| 通过 AI007 前缀缓存调度器、AI008 显存预算与批处理 | 第 05、06、09 节 | 自查第 1 条 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成模块 2 的「堆与 Top-K 问题」和模块 5 的「二分答案与贪心判定」。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | min([(-2, 3, 0), (-2, 1, 1), (0, 0, 2)]) 是哪个元组?元组按什么顺序比较? | 复合排序与并列规则 |
| 自测 2 | bisect.bisect_right([50, 85, 170, 305], 100) 是多少?bisect_left 在 budget = 85 时和它差在哪? | 二分查找与边界 |
| 自测 3 | 长度 [10, 20, 30] 的前缀和是什么?「最短的 2 个」的总长度怎么从前缀和读出? | 前缀和与差分 |
| 自测 4 | heapq.heappush(h, (3, 0, 10)) 后 heapq.heappop(h) 弹出什么?堆里存元组时先比谁? | 堆与 Top-K 问题 |
| 自测 5 | if tid in cache: 对字典是 O(1) 还是 O(n)?对列表呢? | 哈希表:计数、去重与查询 |
展开先修自测答案
自测 1:(-2, 1, 1)——先比第 1 项(−2 并列),再比第 2 项(1 < 3)。AI007 的三层规则就是这样一个元组。
自测 2:2(100 插在 85 之后),减 1 得能放 1 个。budget = 85 时 bisect_right 给 2、bisect_left 给 1——恰好等于花费也算放得下,所以用 bisect_right。
自测 3:[0, 10, 30, 60];最短 2 个的总长度是前缀和第 2 项 30。
自测 4:(3, 0, 10);元组先比第 1 项,再比第 2 项——AI043 的堆键 (得分, 加入序号) 正是「得分最低、并列最早加入」。
自测 5:字典 O(1),列表 O(n)。AI043 的 id 到 10⁹、q 到 2×10⁵,必须用字典。
03 / 概念与术语
K/V 投影、槽位、公共前缀、负载、显存公式、惰性删除、束
分清槽位、公共前缀、负载与淘汰顺序,再对照程序中的比较键和更新语句。
| 术语 | 含义 | 代码里的位置 |
|---|---|---|
| K/V 投影份数 | 第 t 步要用到前 t 个词元的 K、V;不缓存每步重算 t 份,缓存后每步只算新增 1 份 | 本课只算数,不写代码 |
| 槽位与公共前缀 | 每个槽位存一段序列和负载;请求与槽位序列从头逐个比较,相同的个数就是公共前缀长 | common_prefix(seqs[i], req) |
| 三层并列(AI007) | 前缀长者优先 → 负载小者优先 → 编号小者优先 | key = (-k, loads[i], i) 取最小 |
| 更新动作(AI007) | 负载加未命中数(请求长 − 前缀长);序列换成本次请求 | loads[best] += length - matched、seqs[best] = req |
| 显存公式(AI008) | base + c×fixed + c×maxLen×kv + sumLen×attn;取最短的 c 个时 maxLen 是第 c 短、sumLen 是前缀和 | need.append(...) |
| 惰性删除(AI043) | 更新得分时不删旧堆项,弹出时核对是否是最新值,过期就丢弃再弹 | if ent is not None and ent[0] == sc and ent[1] == ar |
| 束(AI028) | 每一步保留的候选序列集合;展开全部 V 个后按 (总分降序, 字典序升序) 取前 B 个 | cands.sort(key=lambda c: (-c[0], c[1]))、cands[:B] |
04 / 缓存与显存
缓存计算量与显存占用的计算方法
生成过程是逐个词元产生的,计算量要按步数累计。先看 AI008 样例的显存占用计算(数值是题目定义的抽象单位):
| 批次 c | 花费展开 | 合计 | 对照预算 |
|---|---|---|---|
| 1 | 50 + 5 + 1×10×2 + 10×1 | 85 | 预算 100:可行 |
| 2 | 50 + 10 + 2×20×2 + 30×1 | 170 | 预算 200:可行 |
| 3 | 50 + 15 + 3×30×2 + 60×1 | 305 | 预算 1000:可行 |
取最短的 c 个请求:maxLen 是第 c 短、sumLen 用前缀和。c 越大花费只增不减——单调,所以能二分(模块 5 · 第 3 课 的判定思路原样复用)。
三层并列规则:使用元组统一比较
Python# AI007:前缀长 -> 负载小 -> 编号小,三层规则组成一个可比较元组
best = max(
range(S),
key=lambda i: (prefix_len(cache[i], req), -load[i], -i),
)
# 选中后:load[best] += len(req) - prefix_len(...)
# cache[best] = req将需要降序比较的值保留原符号、需要升序比较的值取负后组成元组,再选择字典序最大的元组即可完成三层比较。淘汰类(AI043)同理:小根堆的键是 (得分, 进入时刻),弹出前校验是否为该 id 的最新得分(惰性删除)。
并列规则是第一项检查
并列规则会直接影响输出,应分别核对三道题的规定:AI007 是「负载小、再编号小」,AI043 是「淘汰进入最早者(不是 id 最小)」,AI028 是题目明确规定的候选排序。写代码前先把并列规则原文整理成注释,写完对着注释检查一遍。
补充学习(选学)KV 缓存、显存预算与采样的关系约 4 分钟本课三道题对应的三个推理概念
本课的题对应 LLM 推理中的三个概念:KV 缓存减少了什么——把 100 步生成的 K/V 投影份数从 5050 降到 100(每步的注意力打分仍要做);单个加速设备在给定显存预算下可支持的最大批次——AI008 的显存公式加二分;前缀缓存如何提高命中——AI007 的最长公共前缀与并列规则。可用 100 步生成的计算量示例说明缓存减少了哪些重复计算。
再补采样的两个参数:temperature(温度,T>0)先给打分做除法——T>1 会缩小候选概率之间的差异,0<T<1 会扩大这些差异;Top-K 采样只保留前 k 个候选再抽样。用的正是模块 2 · 第 2 课 的 Top-K。
05 / AI007 手动计算
题面示例:两个请求的逐槽位表
AI007「前缀缓存调度器」:第一行 S Q;S 行「负载 长度 + 序列」;Q 行「长度 + 序列」。每个请求选公共前缀最长的槽位,并列选负载小的,再并列选编号小的;输出「槽位编号(从 1 起) 前缀长」;处理后该槽位负载加未命中数、序列换成本次请求。题面示例:3 2 / 3 3 1 2 3 / 1 3 1 2 4 / 0 1 9 / 3 1 2 5 / 2 9 8 → 2 2 / 3 1。
| 槽位 | 负载 | 序列 | 公共前缀 | 比较元组 (−前缀, 负载, 编号) |
|---|---|---|---|---|
| 1 | 3 | 1 2 3 | 2(1、2 相同,3 ≠ 5) | (−2, 3, 0) |
| 2 | 1 | 1 2 4 | 2 | (−2, 1, 1) ← 最小 |
| 3 | 0 | 9 | 0 | (0, 0, 2) |
输出 2 2。前缀并列时看负载:1 < 3 选槽位 2。更新:槽位 2 负载 1 + (3 − 2) = 2,序列变成 1 2 5。
| 槽位 | 负载 | 序列 | 公共前缀 | 比较元组 |
|---|---|---|---|---|
| 1 | 3 | 1 2 3 | 0 | (0, 3, 0) |
| 2 | 2 | 1 2 5 | 0 | (0, 2, 1) |
| 3 | 0 | 9 | 1 | (−1, 0, 2) ← 最小 |
输出 3 1。更新:槽位 3 负载 0 + (2 − 1) = 1,序列变成 9 8。若第一个请求后没有把槽位 2 的序列换掉,题库用例 2 2 / 0 1 1 / 10 1 2 / 3 1 2 3 / 3 1 2 4 的第二行会从 1 2 变成 1 1。
06 / AI008 手动计算
花费随 c 单调,预算上二分
AI008「显存预算与批处理」:第一行 n q;第二行 base fixed kv attn;第三行 n 个请求长度;q 行预算。选 c 个请求的花费 = base + c×fixed + c×maxLen×kv + sumLen×attn,对每个预算输出最多能放几个。题面示例:3 3 / 50 5 2 1 / 10 20 30 / 100 / 200 / 1000 → 1 / 2 / 3。
为什么取最短的 c 个:同样放 c 个,换进一个更长的请求只会让 maxLen 不减、sumLen 增大,花费不会变小。为什么单调:need[c+1] 比 need[c] 多了一个请求的 fixed、多了 maxLen 不减的 kv 项、多了不为负的 attn 项(题目允许 attn 为 0),所以 need[c] 随 c 不减——「能放 c 个」对 c 是单调的,可以二分(第 04 节的花费表就是 need[1..3])。
| 预算 | need = [50, 85, 170, 305] 中不超过预算的最大下标 | 输出 |
|---|---|---|
| 100 | 85 ≤ 100 < 170 → 下标 1 | 1 |
| 200 | 170 ≤ 200 < 305 → 下标 2 | 2 |
| 1000 | 305 ≤ 1000 → 下标 3 | 3 |
bisect_right(need, budget) - 1 直接给出下标;预算低于 base(题面示例 2:预算 40、50 对 need[0] = 50)时下标是 −1 或 0,要用 max(0, ·) 压到 0。预算最大 9×10¹⁵、花费最大约 7×10⁴ × 10⁶ × 10⁶——全程整数运算,不要转浮点。
07 / AI043 与 AI028 手动计算
淘汰规则的逐条状态表,以及束搜索的逐步表
AI043「KV 缓存淘汰」(进阶):容量 C;每条记录「标识 id 得分」。已在缓存:只改得分、加入顺序不变;不在缓存:满了先淘汰得分最低者(并列淘汰最早加入者),再插入。最后按加入顺序输出;q=0 输出 empty。
| 记录 | 动作 | 处理后的缓存(按加入顺序) |
|---|---|---|
| 1 5 | 插入 | 1:5(0) |
| 2 4 | 插入 | 1:5(0)、2:4(1) |
| 1 3 | 已在缓存:得分 5 → 3,顺序不变 | 1:3(0)、2:4(1) |
| 3 6 | 满:淘汰得分最低的 1(3),插入 | 2:4(1)、3:6(2) |
| 2 10 | 已在缓存:得分 4 → 10 | 2:10(1)、3:6(2) |
| 4 1 | 满:淘汰 3(6),插入 | 2:10(1)、4:1(3) |
| 5 2 | 满:淘汰 4(1),插入 | 2:10(1)、5:2(4) |
| 4 8 | 满:淘汰 5(2),插入(4 被淘汰过,按新记录) | 2:10(1)、4:8(5) |
输出 2 10 / 4 8。更新得分后堆里还留着旧得分的过期项:第 3 条把标识 1 改成 3 后,(5, 0) 仍在堆里;第 5 条把标识 2 改成 10 后,(4, 1) 仍在堆里。弹出时必须核对「得分与加入序号都是最新值」:第 4 条时新项 (3, 0) 排在 (5, 0) 之前,不核对也碰巧淘汰了正确的标识 1;到第 6 条,过期项 (4, 1) 先于 (6, 2) 弹出,不核对就会按过期得分 4 错误淘汰得分已是 10 的标识 2,最终输出 3 6 / 4 8。并列时淘汰最早加入者而不是标识小者:2 3 / 100 7 / 50 7 / 30 7 正确输出 50 7 / 30 7,按标识淘汰会输出 100 7 / 30 7。
AI028「束搜索解码」(进阶):第一行 T V B;第二行第 1 步各词元得分;随后 T−1 个 V×V 矩阵,第 i 行第 j 列是上一词元 i 时选 j 的得分。每步把束里每个序列扩展 V 个候选,按 (总分降序, 字典序升序) 只留前 B 个;T 步后束首即答案,输出总分与序列。
| 步 | 候选(总分, 序列) | B=1 保留 | B=2 保留 |
|---|---|---|---|
| 1 | (3, 0)、(5, 1) | (5, 1) | (5, 1)、(3, 0) |
| 2 | B=1:从 (5, 1) 扩展 (5, 1 0)、(6, 1 1);B=2 另有从 (3, 0) 扩展的 (12, 0 0)、(3, 0 1) | (6, 1 1) | (12, 0 0)、(6, 1 1) |
B=1 输出 6 / 1 1,B=2 输出 12 / 0 0(题库用例 beam-rescue)。全局最优序列 0 0 的总分 12 在 B=1 时第一步就被丢弃——答案由束搜索过程定义,不是全局最优。同分并列按序列字典序小:1 3 2 / 7 7 3 输出 7 / 0。
08 / 注意力中的位置编码
RoPE:配对、频率、旋转
模块 7 · 第 1 课 算出的打分表只看内容不看位置——把两个词元的位置互换,它们之间的打分不变。RoPE 在注意力层里给每个位置的向量做一次「按位置定角度」的旋转,位置信息就进入了点积。三步公式都是定义,按定义计算即可:
AI039 样例 1 手算(d=4、base=10000、p=3)
x = [1.00, 0.50, -2.00, 0.25],维度两两配对:第 0 对 (0,1)、第 1 对 (2,3) θ_0 = 10000^(-0/4) = 1 α_0 = 3×1 = 3.0000 θ_1 = 10000^(-2/4) = 0.01 α_1 = 3×0.01 = 0.0300 y[0] = 1×cos3.0 − 0.5×sin3.0 = -1.0606 y[1] = 1×sin3.0 + 0.5×cos3.0 = -0.3539 y[2] = -2×cos0.03 − 0.25×sin0.03 = -2.0066 y[3] = -2×sin0.03 + 0.25×cos0.03 = 0.1899 θ 的指数是 −2i/d(i 是「第几对」,不是维度下标);p=0 时旋转角全 0,原样输出
三个常见错误
① 配对方向写反——「偶数下标在前」:y[2i] 用余弦(cos)减正弦(sin),y[2i+1] 用 sin 加 cos;② θ 的指数写成 −i/d 或 −2i/(d/2);③ 舍入到 0 的负数直接打印成 -0.0000。样例里第 0 对旋转角大、第 1 对旋转角很小——「不同的对旋转速度不同」就是这个意思:编号大的对旋转慢,位置信息主要写在编号小的对上。
补充学习(选学)RoPE 三步公式总结约 4 分钟配对 → 频率 → 旋转;出错时按步骤逐一检查
第一步配对:维度 (2i, 2i+1) 是第 i 对,i 从 0 数起,共 d/2 对。第二步频率:θ_i = base^(−2i/d),i 越大 θ 越小。第三步旋转:角 α_i = p·θ_i,y[2i] = x[2i]cos α − x[2i+1]sin α,y[2i+1] = x[2i]sin α + x[2i+1]cos α。它和模块 7 · 第 1 课 补充学习里的绝对位置嵌入是两种不同的位置编码:绝对位置在进入第一层前加到输入上,RoPE 在每个注意力层里旋转 Q 和 K——旋转不改变向量长度;在 Q、K 内容固定时,两个位置的 Q、K 点积只随它们的相对距离变化,这是它被广泛采用的原因。
09 / 从规则到程序
参考实现与四份完整程序,每一步落在哪几行
四道题都是「读入 → 按规则逐条处理 → 输出」;差别只在并列元组与更新动作。
| 步骤 | AI007 | AI008 | AI043 | AI028 |
|---|---|---|---|---|
| 读入 | S 个槽位、Q 个请求(按长度读) | 排序长度、前缀和、need 数组 | C q、q 条记录 | 首步得分 + T−1 个矩阵 |
| 核心 | 逐槽位算前缀,元组取最小 | bisect_right(need, budget) - 1 | 字典 + 小根堆 + 惰性删除 | 展开、排序、截断 |
| 并列 | (−前缀, 负载, 编号) | 无 | (得分, 加入序号) | (−总分, 序列) |
| 更新 | 负载 += 未命中;序列换新 | 无 | 更新只改得分;淘汰后插入新序号 | 束换成前 B 个 |
| 输出 | 槽位编号+1 前缀长 | 每预算一行 | 按加入序号排序;空则 empty | 总分一行、序列一行 |
展开参考实现:RoPE 旋转函数 rope(自带断言;先自己写完再对照)
rope 的参考实现(自带断言)
Pythonimport math
def rope(x, p, base=10000):
d = len(x)
y = [0.0] * d
for i in range(d // 2): # 维度两两配对 (2i, 2i+1)
theta = base ** (-2 * i / d) # 第 i 对的角频率
a = p * theta # 位置 p 的旋转角
c, s = math.cos(a), math.sin(a)
y[2 * i] = x[2 * i] * c - x[2 * i + 1] * s
y[2 * i + 1] = x[2 * i] * s + x[2 * i + 1] * c
return y
# AI039 样例 1:d=4、p=3
y = rope([1.00, 0.50, -2.00, 0.25], 3)
assert [round(v, 4) for v in y] == [-1.0606, -0.3539, -2.0066, 0.1899]
# p=0:旋转角全 0,原样输出
assert [round(v, 4) for v in rope([1.00, 0.50, -2.00, 0.25], 0)] == [1.0, 0.5, -2.0, 0.25]
# 旋转不改变每一对的长度:|y[2i], y[2i+1]| == |x[2i], x[2i+1]|
assert abs(math.hypot(y[0], y[1]) - math.hypot(1.0, 0.5)) < 1e-9断言覆盖 AI039 样例 1、p=0 原样输出,以及「旋转不改变每一对的长度」。θ 用浮点幂 base ** (-2 * i / d)。
展开完整参考程序 1:AI007 前缀缓存调度器
完整程序:AI007(标准输入 → 标准输出)
Pythonimport sys
def common_prefix(a, b):
k = 0
while k < len(a) and k < len(b) and a[k] == b[k]:
k += 1
return k
def main():
data = list(map(int, sys.stdin.read().split()))
if not data:
return
s, q = data[0], data[1]
pos = 2
loads, seqs = [], []
for _ in range(s):
load, length = data[pos], data[pos + 1]
seqs.append(data[pos + 2:pos + 2 + length])
loads.append(load)
pos += 2 + length
out = []
for _ in range(q):
length = data[pos]
req = data[pos + 1:pos + 1 + length]
pos += 1 + length
best = -1
best_key = None
for i in range(s):
k = common_prefix(seqs[i], req)
key = (-k, loads[i], i) # 前缀长者优先 → 负载小者优先 → 编号小者优先
if best_key is None or key < best_key:
best, best_key = i, key
matched = -best_key[0]
out.append(f"{best + 1} {matched}") # 槽位编号从 1 起
loads[best] += length - matched # 负载加未命中的 token 数
seqs[best] = req # 缓存序列换成本次请求
print("\n".join(out))
main()每个请求逐槽位算前缀 O(S·len),S=Q=500、len=60 时最多 500 × 500 × 60 = 1.5×10⁷ 次词元比较。用题面示例核对:输出 2 2 / 3 1;提交前另在目标语言中检查最大规模耗时。
展开完整参考程序 2:AI008 显存预算与批处理
完整程序:AI008(标准输入 → 标准输出)
Pythonimport bisect
import sys
def main():
data = list(map(int, sys.stdin.read().split()))
if not data:
return
n, q = data[0], data[1]
base, fixed, kv, attn = data[2:6]
lengths = sorted(data[6:6 + n]) # 先短后长:同样的 c,最短的 c 个显存最小
pos = 6 + n
need = [base] # need[c] = 放最短 c 个请求所需显存
total = 0
for c in range(1, n + 1):
total += lengths[c - 1]
need.append(base + fixed * c + kv * c * lengths[c - 1] + attn * total)
out = []
for _ in range(q):
budget = data[pos]
pos += 1
c = bisect.bisect_right(need, budget) - 1 # need 单调不减,二分找最大的 c
out.append(str(max(0, c)))
print("\n".join(out))
main()排序和 need 数组各计算一次,每个预算只做一次二分。用题面示例核对:输出 1 / 2 / 3。
展开完整参考程序 3:AI043 KV 缓存淘汰(进阶练习)
完整程序:AI043(标准输入 → 标准输出)
Pythonimport heapq
import sys
def main():
data = sys.stdin.read().split()
if not data:
return
C, q = int(data[0]), int(data[1])
cache = {} # id -> [score, 加入序号]
heap = [] # (score, 加入序号, id),惰性删除
order = 0
for k in range(q):
tid, score = int(data[2 + 2 * k]), int(data[3 + 2 * k])
if tid in cache: # 已在缓存:只改得分,加入顺序不变
cache[tid][0] = score
heapq.heappush(heap, (score, cache[tid][1], tid))
continue
if len(cache) == C: # 已满:淘汰得分最低、并列最早加入者
while True:
sc, ar, vid = heapq.heappop(heap)
ent = cache.get(vid)
if ent is not None and ent[0] == sc and ent[1] == ar: # 跳过过期堆项
del cache[vid]
break
cache[tid] = [score, order]
heapq.heappush(heap, (score, order, tid))
order += 1
if not cache:
print("empty")
return
rows = sorted(cache.items(), key=lambda kv: kv[1][1])
print("\n".join(f"{tid} {ent[0]}" for tid, ent in rows))
main()堆里的过期项在弹出时丢弃,每条记录至多入堆一次、每个堆项至多弹出一次,总量 O(q log q),不再逐次扫描整个缓存。用第 07 节的八条记录核对:输出 2 10 / 4 8。
展开完整参考程序 4:AI028 束搜索解码(进阶练习)
完整程序:AI028(标准输入 → 标准输出)
Pythonimport sys
def main():
data = list(map(int, sys.stdin.read().split()))
if not data:
return
T, V, B = data[0], data[1], data[2]
pos = 3
first = data[pos:pos + V]
pos += V
mats = []
for _ in range(T - 1):
mats.append([data[pos + i * V:pos + (i + 1) * V] for i in range(V)])
pos += V * V
beam = [(0, ())] # (总分, 序列)
for t in range(T):
cands = []
for score, seq in beam:
row = first if t == 0 else mats[t - 1][seq[-1]]
for j in range(V):
cands.append((score + row[j], seq + (j,)))
cands.sort(key=lambda c: (-c[0], c[1])) # 总分降序,同分序列字典序升序
beam = cands[:B]
score, seq = beam[0]
print(score)
print(" ".join(map(str, seq)))
main()每步最多 B×V = 400 个候选,排序后截断。用题面示例核对:输出 6 / 1 1。
10 / 边界、反例与复杂度
错误做法在题库用例上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| AI007 并列先比编号再比负载 | 题面示例 | 1 2 / 3 1 | 2 2 / 3 1 | 答案错误(WA) |
| AI007 处理后不换缓存序列 | 2 2 / 0 1 1 / 10 1 2 / 3 1 2 3 / 3 1 2 4 | 1 1 / 1 1 | 1 1 / 1 2 | 答案错误(WA) |
| AI007 负载加整段请求长度 | 3 5 / 1 2 1 2 / 2 2 1 3 / 3 1 4 / …(题库 many-requests) | 末行 2 0 | 末行 1 0 | 答案错误(WA) |
| AI007 前缀循环只按请求长度走 | 2 1 / 4 2 1 2 / 5 4 1 2 3 4 / 3 1 2 9 | 下标越界的运行错误 | 1 2 | 运行错误(RE) |
| AI007 槽位编号从 0 起 | 题面示例 | 1 2 / 2 1 | 2 2 / 3 1 | 答案错误(WA) |
| AI008 不排序 | 4 2 / 20 3 2 1 / 100 1 50 2 / 200 / 500 | 2 / 4 | 2 / 3 | 答案错误(WA) |
AI008 用 bisect_left(恰好等于预算算放不下) | 3 2 / 50 5 1 1 / 5 10 15 / 75 / 140 | 1 / 2 | 1 / 3 | 答案错误(WA) |
AI008 预算低于 base 不压到 0 | 题面示例 2 | -1 / 0 | 0 / 0 | 答案错误(WA) |
| AI043 并列淘汰标识小者 | 2 3 / 100 7 / 50 7 / 30 7 | 100 7 / 30 7 | 50 7 / 30 7 | 答案错误(WA) |
| AI043 更新得分时删掉再插入(重置加入顺序) | 2 3 / 10 1 / 20 2 / 10 99 | 20 2 / 10 99 | 10 99 / 20 2 | 答案错误(WA) |
| AI043 弹堆时不核对过期项 | 第 07 节八条记录 | 3 6 / 4 8 | 2 10 / 4 8 | 答案错误(WA) |
AI043 q=0 不输出 empty | 3 0 | 空输出 | empty | 格式错误(PE) |
| AI028 同分取字典序大 | 1 3 2 / 7 7 3;2 2 4 / 5 5 / 1 0 / 1 0 | 7 / 1;6 / 1 0 | 7 / 0;6 / 0 0 | 答案错误(WA) |
| AI028 每步只留 1 个(忽略 B) | 2 2 2 / 3 5 / 9 0 / 0 1 | 6 / 1 1 | 12 / 0 0 | 答案错误(WA) |
| AI028 矩阵行列读反(用第 j 行第 i 列) | 2 2 2 / -3 -1 / -5 -2 / -4 -9 | -3 / 1 0 | -5 / 0 1 | 答案错误(WA) |
把参考程序改成对应写法运行,就能得到表里的错误输出。AI028 矩阵读反在题面示例上碰巧输出相同(示例矩阵对称),表里换用一组矩阵不对称的输入。
| 做法 | 时间 | 本课规模下 |
|---|---|---|
| AI007 逐槽位算前缀 | O(Q·S·len) | 最多约 1.5×10⁷ 次词元比较 |
| AI008 need 数组 + 每预算二分 | O(n log n + q log n) | 排序一次、预处理一次,每个预算二分 |
| AI008 每预算从 c=0 线性试 | O(q·n) | 最坏 7×10⁷ 次判定;用二分减少逐个试探 |
| AI043 字典 + 惰性堆 | O(q log q) | 最多 2×10⁵ 次入堆,每个堆项至多弹出一次 |
| AI043 每次淘汰线性扫缓存 | O(q·C) | 2×10¹⁰,必然超时 |
| AI028 每步展开排序 | 最坏 O(T²·B·V·log(B·V)),计入序列复制与字典序比较 | 每步最多 400 个候选;候选序列长不超过 T=20 |
11 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 05 节表格的格式,处理题库用例 2 1 / 1 2 1 2 / 1 2 1 3 / 2 1 9:两个槽位负载都是 1,请求 1 9。
展开练习 1 答案
槽位 1:前缀 1(1 相同,2 ≠ 9),元组 (−1, 1, 0);槽位 2:前缀 1,元组 (−1, 1, 1)。前缀、负载都并列,编号小的槽位 1 胜出:输出 1 1;更新后槽位 1 负载 2、序列 1 9。
练习 2(改一个条件):题面示例的预算改成 170 和 169,输出各是多少?为什么差 1 就不同?
展开练习 2 答案
170 → 2(need[2] = 170 ≤ 170,恰好放得下);169 → 1。这正是 bisect_right 与 bisect_left 的差别:等于预算算放得下。
练习 3(改一个条件):AI043 容量改成 1,处理 1 9 / 2 8 / 3 10 / 4 10,最终缓存是什么?每一步淘汰了谁?
展开练习 3 答案
容量 1 时每条新记录都先淘汰当前唯一一条:1(9) → 淘汰 1 插 2(8) → 淘汰 2 插 3(10) → 淘汰 3 插 4(10)。输出 4 10(题面示例 2)。新记录得分 8 低于被淘汰的 9 也照样接收——题面写明「新记录一定会被接收」。
练习 4(独立实现):完成「进阶练习 3」的 rope,再加一条断言:旋转前后第 0 对 (y[0], y[1]) 的长度等于 (x[0], x[1]) 的长度。
展开练习 4 答案
rope 的参考实现(自带断言)
Pythonimport math
def rope(x, p, base=10000):
d = len(x)
y = [0.0] * d
for i in range(d // 2): # 维度两两配对 (2i, 2i+1)
theta = base ** (-2 * i / d) # 第 i 对的角频率
a = p * theta # 位置 p 的旋转角
c, s = math.cos(a), math.sin(a)
y[2 * i] = x[2 * i] * c - x[2 * i + 1] * s
y[2 * i + 1] = x[2 * i] * s + x[2 * i + 1] * c
return y
# AI039 样例 1:d=4、p=3
y = rope([1.00, 0.50, -2.00, 0.25], 3)
assert [round(v, 4) for v in y] == [-1.0606, -0.3539, -2.0066, 0.1899]
# p=0:旋转角全 0,原样输出
assert [round(v, 4) for v in rope([1.00, 0.50, -2.00, 0.25], 0)] == [1.0, 0.5, -2.0, 0.25]
# 旋转不改变每一对的长度:|y[2i], y[2i+1]| == |x[2i], x[2i+1]|
assert abs(math.hypot(y[0], y[1]) - math.hypot(1.0, 0.5)) < 1e-9见第 09 节展开区(同一份代码,最后一条断言就是本题)。
练习 5(迁移):题库用例 2 2 2 / -3 -1 / -5 -2 / -4 -9(B=2)手推两步,给出输出。
展开练习 5 答案
第 1 步:(−3, 0)、(−1, 1) → 排序后 (−1, 1)、(−3, 0) 都保留。第 2 步:从 (−1, 1) 扩展 M[1] = [−4, −9] → (−5, 1 0)、(−10, 1 1);从 (−3, 0) 扩展 M[0] = [−5, −2] → (−8, 0 0)、(−5, 0 1)。总分 −5 并列两个:序列 0 1 字典序小于 1 0,束首是 (−5, 0 1)。输出 -5 / 0 1。把矩阵行列读反会得到 -3 / 1 0。
12 / 读题要求与复习自评
四道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | AI007 | AI008 | AI043 | AI028 |
|---|---|---|---|---|
| 输入 | S Q;S 行「负载 长度 序列」;Q 行「长度 序列」 | n q;四个系数;n 个长度;q 个预算 | C q;q 行「标识 得分」 | T V B;首步得分;T−1 个 V×V 矩阵 |
| 并列 | 前缀长 → 负载小 → 编号小 | 无 | 得分低 → 加入早 | 总分高 → 字典序小 |
| 更新 | 负载 += 请求长 − 前缀长;序列换新 | 无 | 已有只改得分;淘汰后插入 | 束换成前 B 个 |
| 输出 | Q 行「编号(从 1 起) 前缀长」 | q 行整数 | 按加入顺序每行「标识 得分」;空则 empty | 总分一行 + 序列一行 |
| 规模 | S、Q ≤ 500,len ≤ 60 | n ≤ 7×10⁴,预算 ≤ 9×10¹⁵(整数) | C ≤ 10⁵,q ≤ 2×10⁵ | T、V、B ≤ 20 |
参考程序:需要对照解法时,展开第 09 节的四份完整程序。复习与自评:「学习完成检查」六条是自评,勾选不改变题目的通过(AC)状态。本课记为完成的条件:必做题 AI007、AI008 都通过,并勾选全部六条学习完成检查;进阶练习 AI043、AI028 与复习题不计入完成状态。复习时用三个问题自测:① 不看正文,写出 AI007 的三层元组与两个更新动作;② 不看表格,从 need = [50, 85, 170, 305] 说出预算 200 的答案和为什么能二分;③ 说出 AI043 的堆键与弹出时要核对什么。答不出哪一条,就回到对应的节重读,再做第 11 节对应的练习。
13 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/2 道
AI007 · 前缀缓存调度器
必做任务 1练习重点:逐槽位算最长公共前缀,按前缀长 → 负载小 → 编号小三层并列选择槽位;预计用时:30 分钟
完成标准:能说出处理完一个请求后,槽位的负载和缓存序列各自怎么更新
需要时查看提示
将三层排序关键字组成元组 (前缀长, -负载, -编号) 并按字典序取最大;选中后负载加的是未命中数 = 请求长 - 前缀长,缓存序列整个换成本次请求。第 05 节有题面示例两个请求的逐槽位表。
AI008 · 显存预算与批处理
必做任务 2练习重点:请求按长度升序排好,对「放几个」二分;预计用时:30 分钟
完成标准:能解释为什么取最短的 c 个最优,以及花费为什么随 c 单调
需要时查看提示
排序后取前 c 个:maxLen 是第 c 短、sumLen 用前缀和。预算最大到 9×10¹⁵,全程整数运算不要转浮点;对每个预算二分最大可行 c:若 c 可行,则将二分下界更新为 c;否则将上界更新为 c−1。第 06 节有 need 数组与三个预算的表。
AI043 · KV 缓存淘汰
进阶练习 1进阶练习练习重点:得分最低先淘汰、并列淘汰进入最早者;更新得分不改进入顺序;预计用时:30 分钟
完成标准:能解释为什么满规模下要用惰性删除的堆而不是线性扫描
需要时查看提示
哈希表存每个 id 的最新得分与进入时刻;小根堆键 (得分, 进入时刻),弹出时校验是不是最新值,过期就丢弃再弹。已有 id 只改得分不算新进入;被淘汰过的 id 再来按新词元处理。C=10⁵、q=2×10⁵ 下 O(q·C) 扫描会超时。第 07 节有八条记录的逐条状态表。
AI028 · 束搜索生成
进阶练习 2进阶练习练习重点:每步展开全部候选,按题目的并列规则取前 B 个;预计用时:25 分钟
完成标准:能说出束搜索与逐步贪心的差别(B=1 时就是贪心)
需要时查看提示
展开后统一排序再截断,不要边生成边淘汰;分数并列按题目明确规定的次序处理。B=1 的用例正是「贪心会错过整体更优序列」的反例来源。第 07 节有 B=1 与 B=2 的逐步对照。
RoPE 旋转函数(rope):配对、频率、旋转
进阶练习 3进阶练习自主练习练习重点:θ_i=base^(−2i/d)、α_i=p·θ_i、二维旋转公式;p=0 原样输出;预计用时:15 分钟
完成标准:AI039 样例 1 的四个分量均满足断言,能说出 θ 指数里的 i 是「第几对」
需要时查看提示
遍历 for i in range(d//2):a = p * base**(-2*i/d),c, s = cos(a), sin(a),y[2i] = x[2i]*c - x[2i+1]*s,y[2i+1] = x[2i]*s + x[2i+1]*c。θ 用浮点幂运算,不要用整数幂累乘。参考实现在第 09 节。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
import math
def rope(x, p, base=10000):
# 待完成:维度两两配对 (2i, 2i+1),theta_i = base**(-2i/d),alpha_i = p*theta_i,
# y[2i] = x[2i]cos(a) - x[2i+1]sin(a),y[2i+1] = x[2i]sin(a) + x[2i+1]cos(a)
...
# AI039 样例 1:d=4、p=3
y = rope([1.00, 0.50, -2.00, 0.25], 3)
assert [round(v, 4) for v in y] == [-1.0606, -0.3539, -2.0066, 0.1899]
# p=0:旋转角全 0,原样输出
assert [round(v, 4) for v in rope([1.00, 0.50, -2.00, 0.25], 0)] == [1.0, 0.5, -2.0, 0.25]提交结果
提交结果说明与处理方法
- WA
答案错误
多数出在并列规则:AI007 第二层是负载不是编号;AI043 并列淘汰「最早进入」不是「id 最小」;先对样例逐条复算。第 10 节的表给出了每种错误在题库用例上的输出
- PE
格式错误
AI007 每行两个数(槽位编号从 1 起、前缀长);AI043 q=0 时输出
empty- RE
运行错误
AI007 的请求行是「长度(
len)+ len 个词元」,读入个数按 len 数,前缀循环要同时受两边长度限制;AI043 的 id 可到 10⁹,用字典不要开数组- TLE
超时
AI008 对每个预算线性试 c 逼近时限;AI043 线性扫描淘汰会超——一个用二分、一个用惰性堆
- AC
通过
复述一遍 AI007 的三层并列与 AI043 的淘汰键,并比较 AI007 的选择规则与 AI043 的淘汰规则为何采用相反的排序方向
14 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。