食人魔推来了一门巨型火炮 "Big Bertha"!士兵们疯狂躲避,却躲不掉——因为这门大炮的瞄准逻辑是:锁定场上距离最接近的两个单位。既然你能预知它的目标,就能抢先一步保护他们:让巫师给那两个士兵施加"石肤"技能。可问题是,怎么在一群士兵里算出哪两个人挨得最近?上一单元你学会了用嵌套循环找出"匹配的一对",这一单元更进一步——不仅要遍历所有单位对,还要比较它们的距离、记录并返回其中最小的那一对。这就是本单元的主题:最近距离查找(nearest pair)。
for i 套 for j,if i == j: continue 跳过自比较),用 distanceTo() 算出每对的距离,用一个 minDistance 变量打擂台记录当前最小距离、同时用 nearestPair 记住对应的那一对单位,遍历完毕就返回距离最近的两个单位。这是"遍历所有组合"和"追踪最值"两大技巧的结合。
上一单元的配对查找,一旦找到匹配就 return 收工。但本单元不同:任意两个单位之间都有距离,都算"一对",我们要的是其中距离最小的那一对。这意味着不能找到一对就停——必须把所有对都比一遍,才能确定谁最近。这正是"找最值"和"找匹配"的关键差别。
在一堆数里找最小值,有个经典套路叫"打擂台":先设一个"守擂者"(初始最优值),然后逐个拿新选手来挑战——谁更小谁就上台当新的守擂者。遍历结束,台上留着的就是最小值。本关就是让"距离"来打擂台:minDistance 是守擂者,每算出一对的距离就和它比一比,更小就更新。
minDistance = 9001(先放一个大得离谱的初值)。distance,就问"你比擂主小吗?"distance < minDistance,新距离上台,同时记住这一对是谁。
单位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) 其实是同一对;不过本关用完整双重循环重复算一遍也无妨,不影响找到正确的最小值。
遍历所有单位对的骨架和第十一单元完全一样:外层 for i 固定第一个单位,内层 for j 遍历第二个单位,if i == j: continue 跳过"自己和自己"。区别只在循环体内做的事——上一单元是判断"是否匹配",这一单元是"算距离并比大小"。
跳过自比较后,就对当前这一对 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 打擂台
找最小值时,守擂者的初值要设成一个足够大的数,大到"任何真实距离都会比它小",这样第一对算出来的距离就一定能挑战成功、顺利上台。关卡用了 minDistance = 9001(一个有梗的大数)。如果反过来初值设成 0 或很小的数,那就永远没有距离能比它更小,最终什么都找不到。
minDistance = 9001 # 初值要大 if distance < minDistance: ...
maxDistance = 0 # 初值要小 if distance > maxDistance: ...
maxDistance = 0 且比较 >。本关要找最近,所以反过来:minDistance = 9001 且比较 <。方向搞反,结果就完全错了。
这是本关最核心的一步。当发现一对更近的单位时,要同时更新两个变量:minDistance 记住新的最小距离,nearestPair 记住这对是谁。二者必须成对更新、缺一不可——只更新距离却忘了记单位,最后就不知道最近的是谁;只记单位却不更新距离,下次比较的基准就错了。
# 发现更近的一对,两个变量一起更新 if distance < minDistance: minDistance = distance # ① 更新最小距离 nearestPair = [units[i], units[j]] # ② 记住是这一对
下面用 4 个单位、几对距离演示 minDistance 和 nearestPair 如何一步步被刷新(挑战成功才更新,失败就保持):
| 检查的一对 | distance | < minDistance? | minDistance | nearestPair |
|---|---|---|---|---|
| 初始 | — | — | 9001 | ["Nobody","Nobody"] |
| A–B | 30 | 是 | 30 | [A, B] |
| A–C | 45 | 否 | 30 | [A, B] |
| B–C | 18 | 是 | 18 | [B, C] |
| C–D | 25 | 否 | 18 | [B, C] |
nearestPair = [B, C] 就是最近的一对——正是我们要的答案。
和上一单元"找到就 return"截然不同:找最小值必须等双重循环全部跑完才能确定答案,所以 return nearestPair 要写在循环外面。如果在循环内提前 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)
这一节聚焦关卡的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
食人魔的巨炮 "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 # ← 在循环外返回
% 让条件周期性成立:每 8 秒一个周期,在余数为 5 的那一刻触发。这正好对应大炮的发射节奏——不用每帧都算最近对,只在关键时刻算一次即可,既高效又精准。
return nearestPair 误缩进到 for 循环内部。那样函数会在第一对(甚至第一次比较)就返回,根本没比完所有对,找到的"最近"往往是错的。记住:找最值要遍历到底,return 在循环之外。
minDistance 设成远大于任何可能距离的数,保证第一对一定能刷新它。若不慎设成 0,条件 distance < 0 永远不成立,nearestPair 会一直停留在 ["Nobody","Nobody"],说出来的名字自然是错的。
minDistance 设成 0 或很小,导致永远刷新不了,结果一直是初始的占位值。distance < minDistance;写成 > 会变成找最远,方向全错。return nearestPair,否则没比完就下结论。minDistance 却忘了 nearestPair(或反之),最后拿不到正确的一对。if i == j: continue,同一单位到自己的距离为 0,会永远"最近"却毫无意义。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 + continue | 12.3 |
| 逐对算距离 | 内层循环里 distanceTo | 12.3 |
| minDistance 初值 | 找最小设大值(9001) | 12.4 |
| 双变量同步更新 | 距离与配对一起更新 | 12.4 · 提前思考 |
| 循环外 return | 遍历到底再返回 | 12.5 · 提前思考 |
| 取余控制时机 | hero.time % 8 == 5 | 提前思考 |
| 最小 vs 最大 | 初值与比较符号相反 | 12.4 |
懂得遍历万千组合、用一个擂台变量牢牢守住最优解、并算准时机出手的英雄,
才是云峰之上真正料敌先机的程序员。继续向雪山之巅挺进吧,勇敢的程序员!🏔️🎯🛡️
扣哒世界 编程知识 · 计算机科学 5(安息云峰):第十二单元
扣哒世界教研团队