01 / 本课学习路线
本课学习路线
阅读与推演约 128 分钟,练习约 50 分钟,进阶练习另需约 50 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
本课只有一个算法(Dijkstra)和一条性质(非负权时出堆即确定);分层状态只是把「会变化的量」写进节点。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 解释「出堆即确定」为什么成立、负权时为什么失效 | 第 04、05 节 | 自查第 2 条 |
| 写出懒删除:过期条目的判断与跳过 | 第 05 节 | 自查第 3 条、练习 4 |
| 先建距离表再在少量关键点上枚举顺序,通过(AC)P4600 | 第 06、09 节 | 自查第 4 条、必做任务 1 |
| 画出分层状态图:k 次加速 = k+1 层,答案在终点各层取最小 | 第 07 节 | 自查第 5 条、进阶练习 1 |
| 用并查集或 DFS 数连通分量 | 第 08 节 | 进阶练习 2 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成模块 2 的「堆与 Top-K」与上一课「拓扑排序与依赖关系」。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | h = []; heapq.heappush(h, (4, 1)); heapq.heappush(h, (1, 2)),heapq.heappop(h) 返回什么? | 堆与 Top-K 问题 |
| 自测 2 | (3, 1) < (4, 0) 与 (3, 2) < (3, 1) 各是真还是假? | 元组 |
| 自测 3 | float("inf") 与任何整数比较谁大?min(float("inf"), 20) 是多少? | 数字与运算符 |
| 自测 4 | 边 (u, v, w) 表示 u→v 权 w,邻接表 adj[u] 里该存什么? | 上一课第 03 节 |
| 自测 5 | 建一个 (n+1)×(k+1)、全为无穷大的二维列表。 | 列表 |
展开先修自测答案
自测 1:(1, 2)——小顶堆按元组第 0 个分量(距离)取最小。
自测 2:真(3 < 4);假(第 0 个分量相等,比第 1 个:2 > 1)。堆里放 (距离, 节点) 就是靠这条规则先比距离。
自测 3:无穷大更大;min 得 20。距离表初值设无穷大,任何真实距离都能把它松弛掉。
自测 4:adj[u].append((v, w))——邻居与边权成对存。
自测 5:[[float("inf")] * (k + 1) for _ in range(n + 1)],不能用 [[…] * (k+1)] * (n+1)(各行会是同一个列表)。
03 / 概念与术语
带权边、松弛、出堆即确定、过期条目、分层状态
先把上一模块 lower_bound 之外的第二个「一个模板」记牢:Dijkstra 的主循环只有五行——出堆、跳过过期、遍历邻居、松弛、压堆。
| 术语 | 含义 | 代码写法 |
|---|---|---|
| 带权边 | 走这条边要付出的代价(时延、距离),非负 | adj[u].append((v, w)) |
| 距离表 dist | 起点到每个节点目前已知的最短距离,初值无穷大 | dist = [inf] * n; dist[src] = 0 |
| 松弛 | 经由 u 到 v 更短就更新 dist[v],并把 (新距离, v) 压堆 | if d + w < dist[v]: dist[v] = d + w; heappush |
| 出堆即确定 | 边权非负时,节点第一次出堆的距离就是最终答案 | Dijkstra 正确性的依据 |
| 过期条目 | 同一节点更早、更大的距离还留在堆里 | if d > dist[u]: continue(懒删除) |
| 分层状态 | 节点 = (位置, 会变化的量),每层一份 dist | dist[v][used];堆里放三元组 |
| 距离表(两阶段) | 先对每个关键点跑一次 Dijkstra 得到关键点两两距离,再在小表上枚举 | P4600 |
| 题目特征 | 选什么 | 本课 |
|---|---|---|
| 每步代价相同(最少几步、第几天) | BFS(上上课) | — |
| 边权非负且不同 | Dijkstra | P4600、AI027 |
| 移动之外还有一个会变的量(次数、体力) | 分层 Dijkstra | AI027 |
| 只问连不连通 | DFS / BFS / 并查集 | P3504 |
| 有负权边 | 换别的最短路算法(本课程不展开) | 第 05 节反例 |
04 / 出堆即确定
3 节点逐步出堆:第一次松弛得到的 4 不是最终答案
3 个节点:0→1 权 4,0→2 权 1,2→1 权 2。从 0 出发:
| 步骤 | 出堆 | 动作 | dist [0,1,2] | 堆(距离, 节点) |
|---|---|---|---|---|
| 初始 | — | 起点入堆 | [0, ∞, ∞] | [(0,0)] |
| 1 | (0, 0) | 松弛 1:0+4 = 4;松弛 2:0+1 = 1 | [0, 4, 1] | [(1,2), (4,1)] |
| 2 | (1, 2) | 松弛 1:1+2 = 3 < 4 → 更新并压堆 | [0, 3, 1] | [(3,1), (4,1)] |
| 3 | (3, 1) | 节点 1 的最短距离确定为 3;无出边 | [0, 3, 1] | [(4,1)] |
| 4 | (4, 1) | 4 > dist[1] = 3 → 过期条目,跳过 | [0, 3, 1] | [] |
答案 [0, 3, 1]。若在步骤 1 就把 dist[1] = 4 当答案,就漏掉了 0→2→1 = 3。出堆即确定的理由:出堆的是全堆最小,其它任何路径到它都要先经过一个距离不小于它的点,边权非负不可能更短。
Dijkstra 练习模板
Pythonimport heapq
def dijkstra(n, adj, src):
dist = [float("inf")] * n
dist[src] = 0
heap = [(0, src)]
while heap:
d, u = heapq.heappop(heap)
# 待完成 1:过期条目的判断——d 和 dist[u] 不相等时直接 continue
...
for v, w in adj[u]:
nd = d + w
# 待完成 2:松弛条件;更新后把 (nd, v) 压回堆
...
return dist二叉堆 + 懒删除:O(E·logV)。分层最短路的节点数变成 V×(k+1),复杂度 O(E·(k+1)·log(V·(k+1)))。先建距离表再枚举:Dijkstra 若干次之后,在 m 个关键点上枚举顺序是 O(m!),只适合关键点数量很小的情况,要结合 m 的上限和时间限制估算。
05 / 过期条目与负权
懒删除:堆里记的距离比表里大就跳过;负权边让「出堆即确定」失效
同一节点可能带着不同的距离多次进堆(步骤 1 与 2 里节点 1 进了两次)。不去改堆里的旧条目,而是出堆时比较:堆里的 d 大于 dist[u] 就是过期,直接跳过。
有负权边时不要用 Dijkstra
0→1 权 5、0→2 权 6、2→1 权 −4、1→3 权 1:节点 1 以 dist=5 先出堆,按「出堆即确定」它的距离不再更新;可随后节点 2 出堆时发现 6+(−4)=2 < 5。带已确定集合(settled)的写法不会再处理节点 1,于是 dist[3] 停在 6(真正的最短是 2+1=3);本页模板的懒删除写法会把节点 1 再压回堆、再出一次——这个例子的答案恰好正确,但「第一次出堆就是答案」已经不成立,存在负环时算法还可能无法终止。遇到负权边,换用适用的最短路算法。
不写懒删除会怎样:过期条目出堆后再次遍历它的全部邻居,每个邻居的松弛都失败但工作量白做;节点被多次压堆的图上,总时间从 O(E log V) 退化到接近 O(E·V)。结果仍然正确,所以这是超时类错误而不是答案错误。
06 / 两阶段:P4600
先对每个关键点跑 Dijkstra 建距离表,再在小表上枚举送货顺序
P4600「快递员的烦恼」:第一行 n m;n 行「客户 id 快递站到该客户的距离」;m 行「客户 id1 客户 id2 距离」(客户之间未必有直接路线,可多次经过任何点);送完所有客户回到快递站,输出最短总路程;无法完成输出 -1。题面示例 1:2 1 / 1 1000 / 2 1200 / 1 2 300 → 2500。
| 0 | 客户 1 | 客户 2 | |
|---|---|---|---|
| 0 | 0 | 1000 | 1200 |
| 客户 1 | 1000 | 0 | 300 |
| 客户 2 | 1200 | 300 | 0 |
客户 1 到客户 2 直接 300,绕快递站是 2200,Dijkstra 自然给出 300。关键点只有 n+1 = 3 个,表是 3×3。
| 顺序 | 路程 | 说明 |
|---|---|---|
| 0 → 1 → 2 → 0 | 1000 + 300 + 1200 = 2500 | 最短 |
| 0 → 2 → 1 → 0 | 1200 + 300 + 1000 = 2500 | 并列 |
输出 2500,与题面一致(题面解释里的「先送 1 回站再送 2」= 4400 不是候选:距离表里 1→2 已经是 300,回溯枚举的是访问顺序而不是每一步的走法)。为什么两阶段:n 个客户的顺序有 n! 种,n 很小时可枚举;路网点可能很多,但只需对 n+1 个关键点各跑一次 Dijkstra。题面保证快递站到每个客户都有距离,所以整张图连通,-1 实际不会出现。
第二个题面示例(5 个客户、1 条客户间路线)输出 9200:五个客户的 5! = 120 种顺序在距离表上枚举即可;程序在展开区。
07 / 分层状态:AI027
状态 = (节点, 已用加速次数):层内普通边,跨层「时延减半」边
AI027「推理流水线最短时延」:n 个算子(编号 1..n,各有处理时延 p),m 条有向通道(u v t);从 1 到 n 的路径总时延 = 路径上所有算子的处理时延 + 所经通道的传输时延;至多 k 次加速,每次把路径上一条通道的传输时延变为 ⌊t/2⌋(可不用完);不可达输出 -1。题面示例 1(k=0)→ 24;示例 2(同图 k=1)→ 20。
加速是「减半向下取整」,不是零费用
本题容易被误读成「加速把边按零费用通过」。题目规则是:加速后的通道时延是 ⌊t/2⌋,且处理时延不受影响。第 10 节错误表给出按零费用写会得到的错误输出。
| 出堆状态 (节点, 已用) | 距离 | 层内松弛(不加速:+t+p) | 跨层松弛(加速:+⌊t/2⌋+p) |
|---|---|---|---|
| (1, 0) | 5 | (2,0)=5+6+2=13;(3,0)=5+2+3=10 | (2,1)=5+3+2=10;(3,1)=5+1+3=9 |
| (3, 1) | 9 | (4,1)=9+20+4=33 | 已用满,不能再跨层 |
| (2, 1) | 10 | (4,1)=10+7+4=21 < 33 → 更新 | — |
| (3, 0) | 10 | (4,0)=10+20+4=34 | (4,1)=10+10+4=24,不比 21 小 |
| (2, 0) | 13 | (4,0)=13+7+4=24 < 34 → 更新 | (4,1)=13+3+4=20 < 21 → 更新 |
| (4, 1) | 20 | 终点:dist[4] = [24, 20] | — |
答案 min(dist[4]) = 20:路径 1→2→4,把 2→4 的 7 减半成 3(5+6+2+3+4)。示例 1(k=0)只有第 0 层:1→2→4 = 24、1→3→4 = 34 → 24。起点的处理时延 p₁ 也计入(n=1 时答案就是 p₁)。
遗漏状态维度的后果:两层共用一个 dist,同一位置较早记录的距离会把另一层的合法状态当成过期条目丢掉——示例 2 会输出 21(第 10 节有逐步轨迹)。自查方法:口述 dist 下标有几维,再核对它与「会变化的量」的数量是否一致。答案要在终点的所有层里取最小:加速可以不用完。
补充学习(选学)分层状态:将已用次数加入节点状态约 6 分钟AI027 的建模写法,和遗漏状态维度导致的答案错误(WA)
「最多可用 k 次加速把一条通道的时延减半」这类题,同一个位置在「已用 0 次」和「已用 1 次」时的处境完全不同——它们就该是两个节点。把状态写成 (节点, 已用次数),dist 变成二维;普通边在层内走,加速边从第 i 层跨到第 i+1 层,之后照常 Dijkstra。
遗漏状态维度的后果:两层共用一个 dist,同一位置较早记录的距离会阻止另一个合法状态继续松弛——这是分层题典型的答案错误。自查方法:口述 dist 下标有几维,再核对它与「会变化的量」的数量是否一致。
08 / 连通分量:P3504
广播服务器数 = 连通分量个数
P3504「广播服务器」:N 行 N 列的 0/1 邻接矩阵(对角线为 1);直接或间接相连都能收到广播;输出至少要向几台服务器发广播。答案就是连通分量的个数:每个分量里发一台。
| 起点 | BFS 走到的服务器 | 分量数 |
|---|---|---|
| 0 | 0、1(第 0 行第 1 列是 1) | 1 |
| 1 | 已访问,跳过 | 1 |
| 2 | 只有它自己 | 2 |
输出 2。邻接矩阵的邻居是「扫一行」:第 u 行里为 1 且未访问的列。全 1 矩阵输出 1,单位矩阵输出 N。
并查集写法:逐对 matrix[i][j] == 1 就合并 i、j,最后数根的个数——边逐条到来、只问「是否同一组」时比 BFS 更方便;本题两种写法都行,参考程序用 BFS,练习 5 给并查集版。
补充学习(选学)并查集入门(进阶练习)约 6 分钟P3504 用连通分量数回答「至少广播几台」
广播服务器问的其实是连通分量个数:每个分量里广播一台,全分量都能收到。并查集用「代表元」维护连通性:查找根(find)时带路径压缩、合并(union)两棵树;也可以用深度优先搜索(DFS,模块 3 · 第 3 课)数分量——两种写法都练一遍,感受并查集在「边逐条到来」场景下的优势。
09 / 从步骤到程序
三道题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。
| 题 | 步骤 | 代码 | 说明 |
|---|---|---|---|
| P4600 | 客户 id 映射到 1..n;双向建边;每个关键点各跑一次 Dijkstra;小表上回溯 | table = [dijkstra(s) for s in range(n + 1)];go(nxt, count + 1, total + table[cur][nxt]) | 回程 table[cur][0] |
| AI027 | 二维 dist;堆放 (距离, 节点, 已用);层内 +t+proc[v]、跨层 +t//2+proc[v] | if used < k: … dist[v][used + 1] | 答案 min(dist[n]) |
| P3504 | 逐台未访问的服务器 BFS,扫一行找邻居 | if matrix[u][v] == 1 and not seen[v] | 分量数 = 答案 |
展开完整参考程序 1:P4600 快递员的烦恼(先自己写完并提交一次,再展开对照)
完整程序:P4600(标准输入 → 标准输出)
Pythonimport sys
import heapq
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
ids = {} # 客户编号 → 1..n(快递站是 0)
adj = [[] for _ in range(n + 1)]
pos = 2
for i in range(1, n + 1):
cid, d = int(data[pos]), int(data[pos + 1])
pos += 2
ids[cid] = i
adj[0].append((i, d)) # 快递站 ↔ 客户,双向
adj[i].append((0, d))
for _ in range(m):
a, b, d = int(data[pos]), int(data[pos + 1]), int(data[pos + 2])
pos += 3
i, j = ids[a], ids[b]
adj[i].append((j, d)) # 客户 ↔ 客户,双向
adj[j].append((i, d))
def dijkstra(src): # 从 src 到每个关键点的最短距离
dist = [float("inf")] * (n + 1)
dist[src] = 0
heap = [(0, src)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]:
continue # 过期条目
for v, w in adj[u]:
if d + w < dist[v]:
dist[v] = d + w
heapq.heappush(heap, (d + w, v))
return dist
table = [dijkstra(s) for s in range(n + 1)] # (n+1)×(n+1) 距离表:任意两个关键点之间的最短路
best = float("inf")
used = [False] * (n + 1)
def go(cur, count, total): # 在小表上回溯枚举送货顺序
global best
if total >= best:
return
if count == n: # 送完所有客户 → 回快递站
best = min(best, total + table[cur][0])
return
for nxt in range(1, n + 1):
if not used[nxt]:
used[nxt] = True
go(nxt, count + 1, total + table[cur][nxt])
used[nxt] = False
go(0, 0, 0)
print(best if best != float("inf") else -1)自测建议:题面两组示例(2500 / 9200)、第 10 节「客户 3 只能经站」的例子(10210)、单个客户往返(1 0 / 7 500 → 1000)。
展开完整参考程序 2:AI027 推理流水线最短时延(进阶)
完整程序:AI027(标准输入 → 标准输出)
Pythonimport sys
import heapq
data = sys.stdin.read().split()
n, m, k = int(data[0]), int(data[1]), int(data[2])
proc = [0] + [int(x) for x in data[3:3 + n]] # 算子处理时延,编号 1..n
adj = [[] for _ in range(n + 1)]
pos = 3 + n
for _ in range(m):
u, v, t = int(data[pos]), int(data[pos + 1]), int(data[pos + 2])
pos += 3
adj[u].append((v, t)) # 有向通道 u → v,传输时延 t
INF = float("inf")
dist = [[INF] * (k + 1) for _ in range(n + 1)] # dist[节点][已用加速次数]
dist[1][0] = proc[1] # 起点的处理时延也计入
heap = [(proc[1], 1, 0)]
while heap:
d, u, used = heapq.heappop(heap)
if d > dist[u][used]: # 过期条目
continue
for v, t in adj[u]:
nd = d + t + proc[v] # 不加速:层内走
if nd < dist[v][used]:
dist[v][used] = nd
heapq.heappush(heap, (nd, v, used))
if used < k: # 加速:跨到下一层,时延减半向下取整
nd2 = d + t // 2 + proc[v]
if nd2 < dist[v][used + 1]:
dist[v][used + 1] = nd2
heapq.heappush(heap, (nd2, v, used + 1))
best = min(dist[n]) # 终点的所有层里取最小(加速可以不用完)
print(-1 if best == INF else best)自测建议:题面示例(k=0 → 24、k=1 → 20)、k=2 → 17、不可达(3 1 2 / 1 1 1 / 2 3 5 → -1)、加速用不满(2 1 3 / 1 1 / 1 2 4 → 4);再自拟含重边、自环的输入。
展开完整参考程序 3:P3504 广播服务器(进阶)
完整程序:P3504(标准输入 → 标准输出)
Pythonimport sys
from collections import deque
rows = [line.split() for line in sys.stdin.read().split("\n") if line.strip()]
n = len(rows)
matrix = [[int(x) for x in row] for row in rows]
seen = [False] * n
groups = 0
for start in range(n):
if seen[start]:
continue
groups += 1 # 一个新的连通分量:广播一台
seen[start] = True
q = deque([start])
while q:
u = q.popleft()
for v in range(n): # 邻接矩阵:扫一行找直接相连的服务器
if matrix[u][v] == 1 and not seen[v]:
seen[v] = True
q.append(v)
print(groups)自测建议:第 08 节手算例(2)、全 1 矩阵(1)、单位矩阵(N)。行数由输入行数决定,不必先读 N。
10 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| 把第一次松弛当最终答案 | 第 04 节 3 节点 | dist = [0, 4, 1] | [0, 3, 1] | 答案错误(WA) |
| AI027 两层共用一份 dist | 题面示例 2 | 21 | 20 | 答案错误(WA) |
| AI027 把加速当成零费用(+0 而不是 +⌊t/2⌋) | 题面示例 2 | 14 | 20 | 答案错误(WA) |
| AI027 漏掉起点的处理时延 | 题面示例 2 | 15 | 20 | 答案错误(WA) |
| AI027 只取终点最后一层 dist[n][k] | 2 1 3 / 1 1 / 1 2 4 | -1(只有一条边,用不满 3 次) | 4 | 答案错误(WA) |
| P4600 忘记回程 | 题面示例 1 | 1300 | 2500 | 答案错误(WA) |
| P4600 只用直接边当距离表(不跑 Dijkstra) | 3 1 / 1 100 / 2 100 / 3 5000 / 1 2 10 | -1(客户 3 与其它客户无直接路线) | 10210 | 答案错误(WA) |
| P3504 用「N − 边数」当答案 | 全 1 的 3×3 | 0 | 1 | 答案错误(WA) |
| 不写懒删除 | 节点被多次压堆的图 | 结果正确但反复松弛 | 同左 | 超时(TLE)风险 |
第二行的 21:共用 dist 时,(3,1)=9 与 (2,1)=10 先出堆,把终点更新到 33、再到 21;随后 (3,0)=10、(2,0)=13 出堆时堆里的距离大于 dist[3]=9、dist[2]=10,被当成过期条目跳过——「还没用加速」这一层再也走不到终点,1→2→4 只加速 2→4 得到的 20 就丢了。第七行的 −1:距离表里客户 3 到客户 1、2 是无穷大,回溯找不到完整顺序。
| 做法 | 时间 | 本课的规模 |
|---|---|---|
| Dijkstra(二叉堆 + 懒删除) | O(E log V) | AI027:n ≤ 5000、m ≤ 20000 |
| 分层 Dijkstra | O(E·(k+1)·log(V·(k+1))) | k ≤ 10 → 状态 ≤ 5.5×10⁴ |
| 两阶段(n+1 次 Dijkstra + n! 枚举) | O((n+1)·E log V + n!) | P4600:n ≤ 10 → 10! ≈ 3.6×10⁶ |
| 直接在原图上枚举所有走法 | 指数级 | 超时 |
11 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 04 节表的格式,对 4 节点、边 0→1 (1)、0→2 (5)、1→2 (1)、2→3 (1) 逐步写出出堆顺序与 dist。
展开练习 1 答案
出 (0,0):dist [0,1,5,∞],堆 [(1,1),(5,2)];出 (1,1):松弛 2:1+1=2 < 5 → dist [0,1,2,∞],堆 [(2,2),(5,2)];出 (2,2):松弛 3:3 → dist [0,1,2,3];出 (3,3):无出边;出 (5,2):过期跳过。答案 [0, 1, 2, 3](练习 4 断言的第四条)。
练习 2(改一个条件):AI027 题面示例的图,k=2 时答案是多少?哪两条通道被加速?
展开练习 2 答案
路径 1→2→4 两条通道都减半:6→3、7→3,总时延 5+3+2+3+4 = 17;路径 1→3→4:2→1、20→10,5+1+3+10+4 = 23。答案 17,加速 1→2 与 2→4。k 再大也不会更小(每条通道至多加速一次)。
练习 3(改一个条件):把「最短」改成「最长时延」,Dijkstra 还能用吗?为什么?
展开练习 3 答案
不能。「出堆即确定」依赖「再走只会更远」;求最长时,一个点出堆后完全可能经更长的绕路得到更大的值,而且有环时最长路无界。有向无环图上的最长路要用上一课的拓扑序做动态规划(模块 4)。
练习 4(独立实现):完成「代码自测」的 dijkstra,再加两条断言:单点图应为 [0];练习 1 的图应为 [0, 1, 2, 3]。
展开练习 4 答案
dijkstra 的参考实现(自带断言)
Pythonimport heapq
def dijkstra(n, adj, src):
dist = [float("inf")] * n
dist[src] = 0
heap = [(0, src)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]: # 过期条目:堆里记的距离比表里的大 → 跳过
continue
for v, w in adj[u]:
nd = d + w
if nd < dist[v]: # 松弛:更短就更新并压回堆
dist[v] = nd
heapq.heappush(heap, (nd, v))
return dist
# 手推过的 3 节点:0→1 权 4,0→2 权 1,2→1 权 2
adj = [[(1, 4), (2, 1)], [], [(1, 2)]]
assert dijkstra(3, adj, 0) == [0, 3, 1] # 0→2→1 = 3,不是直连的 4
assert dijkstra(2, [[], []], 0)[1] == float("inf") # 不可达
assert dijkstra(1, [[]], 0) == [0] # 单点
adj2 = [[(1, 1), (2, 5)], [(2, 1)], [(3, 1)], []]
assert dijkstra(4, adj2, 0) == [0, 1, 2, 3] # 绕行 0→1→2 = 2 优于直连 5四条断言覆盖:绕行更短、不可达、单点、练习 1。删掉 if d > dist[u]: continue 结果仍对但会重复松弛。
练习 5(迁移):不运行程序,写出 P4600 题面示例 2 距离表里客户 5 与客户 9 之间的距离(输入 5 1000 / 9 1200 / 5 9 400);再给 P3504 写一个并查集版本并用第 08 节的 3×3 矩阵验证。
展开练习 5 答案
客户 5 到客户 9:直接 400,绕快递站 1000 + 1200 = 2200,取 400。并查集:parent = list(range(n)),find 带路径压缩,对每个 matrix[i][j] == 1 执行 parent[find(i)] = find(j),最后 len({find(i) for i in range(n)});3×3 例子里 0 与 1 合并、2 单独 → 2。
12 / 读题要求与复习自评
三道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | P4600 | AI027 | P3504 |
|---|---|---|---|
| 输入 | n m;n 行客户 id 距离;m 行 id1 id2 距离 | n m k;n 个处理时延;m 行 u v t | N 行 N 列 0/1 |
| 输出 | 最短总路程;不可达 -1 | 最小总时延;不可达 -1 | 连通分量个数 |
| 状态 | 关键点(n+1 个) | (节点, 已用加速次数) | 服务器 |
| 数据范围 | 题目页为准(客户数很小) | n ≤ 5000,m ≤ 20000,k ≤ 10 | N ≤ 40 |
| 样例 | 题面 2 1 / 1 1000 / 2 1200 / 1 2 300 → 2500 | 题库示例 k=0 → 24、k=1 → 20 | 自拟 3×3 → 2 |
需要对照解法时,展开本课第 09 节的完整参考程序;P4600 与 P3504 的题目页另有思路与参考代码,可在题目页查看。完成条件:本课算完成 = P4600 通过判题,并勾选全部六条「学习完成检查」;六条检查是自评,勾选不改变题目的通过(AC)状态;AI027、P3504 是进阶练习,与复习题一样单独统计,不影响完成状态。复习时用三个问题自测:① 不看正文,说出「出堆即确定」成立的前提;② 不看表格,重推第 04 节 3 节点的出堆顺序;③ 说出 AI027 的 dist 有几维、加速边怎么跨层。答不出哪一条,就回到对应的节重读,再做第 11 节对应的练习。
13 / 练习
按顺序完成本课的任务
必做题已通过 0/1 道;进阶练习已通过 0/2 道
出堆即确定与懒删除(dijkstra)
代码自测自主练习练习重点:距离表、最小堆、过期条目跳过、松弛入堆;预计用时:15 分钟
完成标准:能指出「d != dist[u] 就跳过(continue)」防的是什么
需要时查看提示
断言 1 的答案是 [0, 3, 1] 而不是 [0, 4, 1]——如果输出是 4,说明把第一次松弛当成了最终答案。参考实现在第 11 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
import heapq
def dijkstra(n, adj, src):
# 你来写:adj[u] = [(v, w), ...],返回 src 到各点最短距离表
...
# 手推过的 3 节点:0→1 权 4,0→2 权 1,2→1 权 2
adj = [[(1, 4), (2, 1)], [], [(1, 2)]]
assert dijkstra(3, adj, 0) == [0, 3, 1] # 0→2→1 = 3,不是直连的 4
# 不可达
assert dijkstra(2, [[], []], 0)[1] == float("inf")P4600 · 快递员的烦恼
必做任务 1练习重点:多次 Dijkstra 得到关键点距离表 + 小规模枚举访问顺序;预计用时:35 分钟
完成标准:能说出「先建距离表再枚举」比直接在原图上枚举省在哪
需要时查看提示
客户数很小:对快递站和每个客户各跑一次 Dijkstra,得到 (n+1)×(n+1) 距离表;再在表上回溯枚举送货顺序(含回程)取最小。客户间无直接路线没关系——Dijkstra 给的本来就是全图最短。第 06 节把题面示例的距离表与两条顺序列出。
AI027 · 推理流水线最短时延(分层)
进阶练习 1进阶练习练习重点:状态 (节点, 已用加速次数),层内普通边、跨层「时延减半」边;预计用时:30 分钟
完成标准:能画出 k=1 时的两层图,并说出遗漏状态维度会导致什么错误
需要时查看提示
dist 开成 [n+1][k+1],起点 dist[1][0] = p₁;堆里放 (距离, 节点, 已用次数) 三元组;跨层用 ⌊t/2⌋ 而不是 0。答案在终点的所有层里取最小——加速可以不用完。第 07 节把题面示例 2 逐状态列出。
P3504 · 广播服务器
进阶练习 2进阶练习练习重点:邻接矩阵下数连通分量:并查集或 BFS 二选一;预计用时:20 分钟
完成标准:能解释答案为什么恰好等于连通分量个数
需要时查看提示
邻接矩阵(matrix)中 matrix[i][j]=1 表示直接相连;间接相连由传递性保证。BFS 写法:逐台未访问的服务器出发,扫一行找邻居;并查集写法见第 11 节练习 5。
提交结果
提交结果说明与处理方法
- WA
答案错误
优先检查三处:未出堆就当最终答案、分层题遗漏状态维度或把加速当零费用、P4600 忘了回程——第 10 节的表给出了每种错误的具体输出
- RE
运行错误
邻接表按最大编号开数组但节点编号不连续(P4600 客户 id 要先映射);堆里元组顺序写反导致按节点比较
- TLE
超时
不跳过过期条目,同一节点反复松弛全部邻居;P4600 在原图上直接枚举所有走法
- AC
通过
再测起点即终点、不可达、单点图三个边界;口述一遍「出堆即确定」成立的前提
14 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。