去做题

第十二单元:最近距离 · 找出距离最小的一对

Unit 12: Nearest Pair — Tracking the Minimum Distance
计算机科学 5 · 安息云峰 Cloudrip Mountain · 第 19 关:提前思考

12.1 什么是最近距离

What Is the Nearest Pair

食人魔推来了一门巨型火炮 "Big Bertha"!士兵们疯狂躲避,却躲不掉——因为这门大炮的瞄准逻辑是:锁定场上距离最接近的两个单位。既然你能预知它的目标,就能抢先一步保护他们:让巫师给那两个士兵施加"石肤"技能。可问题是,怎么在一群士兵里算出哪两个人挨得最近?上一单元你学会了用嵌套循环找出"匹配的一对",这一单元更进一步——不仅要遍历所有单位对,还要比较它们的距离、记录并返回其中最小的那一对。这就是本单元的主题:最近距离查找(nearest pair)。

本单元主线:用嵌套循环遍历所有单位对(for i 套 for j,if i == j: continue 跳过自比较),用 distanceTo() 算出每对的距离,用一个 minDistance 变量打擂台记录当前最小距离、同时用 nearestPair 记住对应的那一对单位,遍历完毕就返回距离最近的两个单位。这是"遍历所有组合"和"追踪最值"两大技巧的结合。

12.1.1 从"配对"到"最优配对"(From a Pair to the Best Pair)

12.1.1.1 不是找到就行,而是找最小的 Not just any pair — the closest one

上一单元的配对查找,一旦找到匹配就 return 收工。但本单元不同:任意两个单位之间都有距离,都算"一对",我们要的是其中距离最小的那一对。这意味着不能找到一对就停——必须把所有对都比一遍,才能确定谁最近。这正是"找最值"和"找匹配"的关键差别。

扣哒世界 最近距离示意 - koudashijie.com
图1:大炮瞄准场上距离最近的两个单位——我们要遍历所有单位对,找出其中间距最小的一对

12.1.2 打擂台找最小值(The Champion Pattern)

12.1.2.1 用一个"守擂者"记录当前最优 Keep a running best

在一堆数里找最小值,有个经典套路叫"打擂台":先设一个"守擂者"(初始最优值),然后逐个拿新选手来挑战——谁更小谁就上台当新的守擂者。遍历结束,台上留着的就是最小值。本关就是让"距离"来打擂台:minDistance 是守擂者,每算出一对的距离就和它比一比,更小就更新。

🏆 打擂台三步曲
① 设擂主:minDistance = 9001(先放一个大得离谱的初值)。
② 挑战:每算出一对的距离 distance,就问"你比擂主小吗?"
③ 换人:若 distance < minDistance,新距离上台,同时记住这一对是谁。

12.2 distanceTo 测距

Measuring Distance with distanceTo

12.2.1 两单位间的距离(Distance Between Two Units)

12.2.1.1 unitA.distanceTo(unitB) 返回间距 unitA.distanceTo(unitB) returns the gap

单位A.distanceTo(单位B) 直接返回两个单位之间的直线距离(一个数字)。你不用自己去算坐标差、开平方——游戏内置的这个方法替你完成了。距离越小,两个单位挨得越近。本关比较的就是这个返回值。

# 计算第 i 个单位到第 j 个单位的距离
distance = units[i].distanceTo(units[j])
print(distance)   # 例如 12.7,越小越近
距离是对称的:A.distanceTo(B) 和 B.distanceTo(A) 结果完全相同——甲到乙的距离,等于乙到甲的距离。这也是为什么 (i, j) 和 (j, i) 其实是同一对;不过本关用完整双重循环重复算一遍也无妨,不影响找到正确的最小值。
distanceTo 返回两单位间的直线距离 A B distance

12.3 嵌套循环遍历单位对

Nested Loops Over All Pairs

12.3.1 for i / for j + continue(All Pairs, Skip Self)

12.3.1.1 沿用上一单元的配对框架 Reuse the pairing framework

遍历所有单位对的骨架和第十一单元完全一样:外层 for i 固定第一个单位,内层 for j 遍历第二个单位,if i == j: continue 跳过"自己和自己"。区别只在循环体内做的事——上一单元是判断"是否匹配",这一单元是"算距离并比大小"。

12.3.2 计算每一对的距离(Distance for Every Pair)

12.3.2.1 在内层循环里算距离 Compute the distance inside the inner loop

跳过自比较后,就对当前这一对 units[i] 和 units[j] 调用 distanceTo 得到它们的间距。这个 distance 是接下来"打擂台"的挑战者。

# 遍历所有单位对,逐对算距离
for i in range(len(units)):
    for j in range(len(units)):
        if i == j:
            continue                       # 跳过自比较
        distance = units[i].distanceTo(units[j])
        # 下一步:拿 distance 打擂台

12.4 追踪最小值

Tracking the Minimum

12.4.1 minDistance 初始值(The Initial Value)

12.4.1.1 为什么初值要设得很大 Why start with a huge number

找最小值时,守擂者的初值要设成一个足够大的数,大到"任何真实距离都会比它小",这样第一对算出来的距离就一定能挑战成功、顺利上台。关卡用了 minDistance = 9001(一个有梗的大数)。如果反过来初值设成 0 或很小的数,那就永远没有距离能比它更小,最终什么都找不到。

找最小值(本关)
minDistance = 9001
# 初值要大
if distance < minDistance:
    ...
找最大值(概览对照)
maxDistance = 0
# 初值要小
if distance > maxDistance:
    ...
找最小用大初值,找最大用小初值:关卡概览特意给了"找最远的一对"作对照——它用 maxDistance = 0 且比较 >。本关要找最近,所以反过来:minDistance = 9001 且比较 <。方向搞反,结果就完全错了。

12.4.2 双变量同步更新(Update Two Variables Together)

12.4.2.1 记距离的同时也要记"是谁" Record both the distance and the pair

这是本关最核心的一步。当发现一对更近的单位时,要同时更新两个变量:minDistance 记住新的最小距离,nearestPair 记住这对是谁。二者必须成对更新、缺一不可——只更新距离却忘了记单位,最后就不知道最近的是谁;只记单位却不更新距离,下次比较的基准就错了。

# 发现更近的一对,两个变量一起更新
if distance < minDistance:
    minDistance = distance                # ① 更新最小距离
    nearestPair = [units[i], units[j]]  # ② 记住是这一对

下面用 4 个单位、几对距离演示 minDistance 和 nearestPair 如何一步步被刷新(挑战成功才更新,失败就保持):

检查的一对distance< minDistance?minDistancenearestPair
初始——9001["Nobody","Nobody"]
A–B30是30[A, B]
A–C45否30[A, B]
B–C18是18[B, C]
C–D25否18[B, C]
读表小结:擂主从 9001 一路被刷新到 30、再到 18;只有距离更小时(30、18)两个变量才一起更新,距离更大时(45、25)保持不动。遍历完,nearestPair = [B, C] 就是最近的一对——正是我们要的答案。

12.5 返回最近的一对

Returning the Nearest Pair

12.5.1 返回并使用结果(Return & Use the Result)

12.5.1.1 循环全跑完,再 return Return after the loops finish

和上一单元"找到就 return"截然不同:找最小值必须等双重循环全部跑完才能确定答案,所以 return nearestPair 要写在循环外面。如果在循环内提前 return,就等于只比了一部分对就下结论,很可能错过真正最近的那一对。

return 的位置决定成败:找匹配可以"边找边返回",找最值绝不能。return nearestPair 必须在两层 for 都结束之后。放错位置(缩进进了循环里)是本关最隐蔽的 bug——它可能"碰巧"在简单情形下对,却在复杂情形下悄悄出错。

调用处每帧判断大炮何时发射,到点就调用函数拿到最近的一对,说出他们的名字让巫师保护:

while True:
    soldiers = hero.findByType("soldier")
    # 已知大炮的发射时刻
    if hero.time % 8 == 5:
        pair = findNearestPair(soldiers)
        hero.say(pair[0].id + " " + pair[1].id)

12.6 实战关卡精讲

Level Strategy & Analysis

这一节聚焦关卡的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。

第 19 关 · 提前思考

🎯 提前思考 —— 算出大炮会瞄准的那一对

扣哒世界 提前思考关卡 - koudashijie.com
图2:提前思考——预判巨炮 Big Bertha 锁定的最近两名士兵,抢先让巫师为他们施加石肤

关卡背景与目标

食人魔的巨炮 "Big Bertha" 每隔一段时间开火一次,每次都锁定场上距离最近的两个单位。你要做的是提前算出这两名士兵是谁,在大炮发射前说出他们的名字——巫师听到后会给他们施加石肤(stoneskin)保护。目标是撑住阵地、分散大炮注意力,直到特种小队赶到。

策略:写好"找最近一对"的函数,卡准发射时刻调用

策略分两层。算法层:补全 findNearestPair(units),用嵌套循环遍历所有士兵对,用 distanceTo 算距离,用 minDistance 打擂台找出最近的一对并返回。时机层:主循环用 if hero.time % 8 == 5 判断大炮即将发射的时刻,只在这一刻调用函数、说出名字——既省算力,又确保保护恰好赶在开火前生效。

思路拆解

函数遵循 12.4 的打擂台模板:先 minDistance = 9001、nearestPair = ["Nobody", "Nobody"];外层 for i、内层 for j 遍历所有对,if i == j: continue 跳过自比较;distance = units[i].distanceTo(units[j]) 算距离;if distance < minDistance 就把 minDistance 更新为新距离、把 nearestPair 更新为 [units[i], units[j]];最后在循环外 return nearestPair。调用处用 pair[0].id 和 pair[1].id 拼出名字说出来。

# 关键片段:打擂台找最近一对(核心逻辑)
def findNearestPair(units):
    minDistance = 9001
    nearestPair = ["Nobody", "Nobody"]
    for i in range(len(units)):
        for j in range(len(units)):
            if i == j:
                continue
            distance = units[i].distanceTo(units[j])
            if distance < minDistance:
                minDistance = distance
                nearestPair = [units[i], units[j]]
    return nearestPair          # ← 在循环外返回
hero.time % 8 == 5 是什么意思?取余运算 % 让条件周期性成立:每 8 秒一个周期,在余数为 5 的那一刻触发。这正好对应大炮的发射节奏——不用每帧都算最近对,只在关键时刻算一次即可,既高效又精准。
return 千万别写进循环里:本关最容易错的就是把 return nearestPair 误缩进到 for 循环内部。那样函数会在第一对(甚至第一次比较)就返回,根本没比完所有对,找到的"最近"往往是错的。记住:找最值要遍历到底,return 在循环之外。
初值 9001 的作用:把 minDistance 设成远大于任何可能距离的数,保证第一对一定能刷新它。若不慎设成 0,条件 distance < 0 永远不成立,nearestPair 会一直停留在 ["Nobody","Nobody"],说出来的名字自然是错的。

12.7 常见错误

Common Mistakes

12.7.1 新手最容易踩的坑(Pitfalls for Beginners)

12.7.1.1 最近距离查找的典型问题 Typical issues in nearest-pair search

扣哒世界 最近距离常见错误速查 - koudashijie.com
图3:初值设反、return 放进循环、只更新一个变量,是本单元最常见的失误
易错点清单:
  • 初值设反——找最小却把 minDistance 设成 0 或很小,导致永远刷新不了,结果一直是初始的占位值。
  • 比较符号用反——找最近应是 distance < minDistance;写成 > 会变成找最远,方向全错。
  • return 放进循环里——必须在两层 for 都结束后再 return nearestPair,否则没比完就下结论。
  • 只更新一个变量——更新了 minDistance 却忘了 nearestPair(或反之),最后拿不到正确的一对。
  • 忘跳过自比较——漏了 if i == j: continue,同一单位到自己的距离为 0,会永远"最近"却毫无意义。
  • distanceTo 用法错——应是 units[i].distanceTo(units[j]),别写成对 pos 或数组调用。
  • 取结果下标错——返回的是一对,用 pair[0]、pair[1] 取;说名字要取 .id 并用空格连接。
  • 发射时机判断错——hero.time % 8 == 5 的取余条件写错,会导致保护没卡在大炮开火前。

本单元小结 ✨

恭喜你识破了巨炮 Big Bertha 的瞄准逻辑!这一单元你把"配对查找"升级成了"最优配对查找"——不只是找到一对,而是找出距离最小的那一对。你复用了嵌套循环 + 跳过自比较的框架遍历所有单位对,学会了用 distanceTo() 测量两单位间距,掌握了最关键的"打擂台"追踪最值技巧:把 minDistance 初值设得足够大、每遇到更近的一对就同步更新 minDistance 和 nearestPair 两个变量,并牢记找最值必须遍历到底、return 要写在循环之外。配合 hero.time % 8 == 5 卡准发射时机,你成功预判并保护了危险中的士兵。来回顾一下核心概念:

核心概念关键点对应内容
最优配对找距离最小的一对12.1
打擂台模式守擂者 + 逐个挑战12.1 · 12.4
distanceTo 测距A.distanceTo(B)12.2
嵌套循环遍历对for i / for j + continue12.3
逐对算距离内层循环里 distanceTo12.3
minDistance 初值找最小设大值(9001)12.4
双变量同步更新距离与配对一起更新12.4 · 提前思考
循环外 return遍历到底再返回12.5 · 提前思考
取余控制时机hero.time % 8 == 5提前思考
最小 vs 最大初值与比较符号相反12.4

懂得遍历万千组合、用一个擂台变量牢牢守住最优解、并算准时机出手的英雄,
才是云峰之上真正料敌先机的程序员。继续向雪山之巅挺进吧,勇敢的程序员!🏔️🎯🛡️

扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十二单元

扣哒世界教研团队