去做题

第五单元:最值算法 · 从数组中找出"之最"

Unit 5: The Max Algorithm — Finding the Best in an Array
计算机科学 5 · 安息云峰 Cloudrip Mountain · 戒指运送人 → 图书馆谋士

5.1 什么是最值算法

What Is the Max/Min Algorithm

前面几个单元里,你学会了封装函数、分支决策、用取余轮换。这一单元你要掌握一个在编程中出现频率极高的经典算法——最值算法(find the max/min):从一堆数据里挑出"最大的""最强的""最优的"那一个。在战场上,它意味着让弓箭手集火"生命值最高的大食人魔",先啃下最硬的骨头。

本单元主线:你将深入理解 return 如何让函数交出一个结果、如何用变量接住返回值(archerTarget = findStrongestTarget()),亲手写出遍历数组找最大值的算法(找生命值最高的敌人),学会把返回值用于条件判断(目标是否还存活、是否需要重新寻找),并认识用三角函数把角度换算成坐标偏移,让士兵们围成一个保护圈。

5.1.1 从数组里挑"之最"(Picking the Best from a List)

5.1.1.1 一堆里面挑一个 Choosing one out of many

findNearest 帮你找"最近的",但战术上,"最近"未必是"最该打的"。图书馆谋士这一关要求集火生命值最高的敌人——因为大食人魔血厚威胁大,先集中火力除掉它最划算。而"最高生命值"这种条件,没有现成函数可用,得你自己写算法从敌人数组里一个个比出来。这就是最值算法的用武之地。

5.1.2 擂台赛的比喻(The Champion Analogy)

5.1.2.1 让选手轮番上台打擂 Contestants challenge the champion one by one

🏆 找最大值就像"打擂台"
先随便请一位选手站上擂台当擂主(或者干脆让擂台空着)。然后让其余选手一个一个上来挑战:谁比当前擂主强,谁就取而代之当新擂主;打不过就下台。
所有人都比试完后,站在台上的那位,一定是全场最强的。这正是"遍历数组找最大值"的核心思路。

5.2 return 返回值

Returning a Value

5.2.1 函数交出一个结果(A Function Hands Back a Result)

5.2.1.1 调用它,就能得到一个值 Calling it gives you a value

第三单元你已经见过 return 返回字符串。这一单元的 return 更进一步:它能返回任何东西——一个数字、一个敌人单位、甚至一个 {x, y} 坐标对象。关卡注释说得很生动:"该函数返回一些东西!当你调用函数时,你会得到一些值。"findStrongestTarget() 返回的就是一个"敌人单位"对象。

5.2.2 返回值赋给变量(Assigning the Return Value)

5.2.2.1 用变量接住函数的成果 Catch the result in a variable

函数把值 return 出来后,你要用一个变量接住它,之后才能反复使用。写法是 变量 = 函数():等号右边的函数先跑完、算出返回值,再把这个值装进等号左边的变量里。此后这个变量就"记住"了那个成果。

用变量接住函数的返回值 archerTarget = findStrongestTarget() 接住结果的变量 先跑完,算出返回值 右边先执行 → 得到最强敌人 → 装进左边变量
为什么要用变量记住它?因为找最强目标要遍历所有敌人、逐个比较,是一件"费劲"的事。用变量把结果存下来,就能在后续多帧里持续命令弓箭手攻击同一个目标,而不必每帧都重新算一遍——既省力,又能保证火力集中在同一个敌人身上。

5.3 遍历找最大值

Finding the Max by Iteration

5.3.1 擂主算法三步走(The Three Steps)

5.3.1.1 初始化、遍历比较、更新擂主 Initialize, compare, update

遍历找最大值有一个固定套路,牢牢记住这三步就能应对绝大多数"找之最"问题:

找最大值算法:三步走 ① 初始化擂台 mostHealth = 0 bestTarget = None 先假设还没有擂主 ② 逐个挑战 for 每个敌人: if 它血量 > mostHealth 比当前擂主强吗? ③ 更新擂主 bestTarget = 它 mostHealth = 它血量 更强者上台

5.3.2 找最高生命值敌人(Finding the Highest-Health Enemy)

5.3.2.1 把三步写成代码 Turning the three steps into code

"图书馆谋士"的 findStrongestTarget 就是这套算法的标准实现。先把擂主 bestTarget 设为 None、把当前最高血量 mostHealth 设为 0;再遍历所有敌人,谁的 health 比 mostHealth 大,就让它当新擂主,并同时更新 mostHealth 为它的血量。遍历完,bestTarget 就是血量最高的那个。

def findStrongestTarget():
    mostHealth = 0          # ① 擂台上还没人,纪录为 0
    bestTarget = None       # ① 还没有擂主
    enemies = hero.findEnemies()
    # ② 让每个敌人上来挑战
    for i in range(len(enemies)):
        enemy = enemies[i]
        if enemy.health > mostHealth:   # 比擂主强?
            bestTarget = enemy               # ③ 换新擂主
            mostHealth = enemy.health       # ③ 刷新纪录
    # ...返回结果(见下)...
别忘了同步更新 mostHealth:找到更强者时,两个变量都要改:bestTarget = enemy 记住是谁,mostHealth = enemy.health 记住它多强。如果只改 bestTarget 却忘了刷新 mostHealth,比较基准就永远停在旧值,后面的比较会全部乱套,找出来的不是真正的最大值。

5.3.3 找不到的兜底(Handling "No Target")

5.3.3.1 只集火"够格"的目标 Only fire at a target worth it

关卡有个战术要求:"如果有大食人魔的话,只集中弓箭手的火力。"翻译成代码就是:遍历完后,只有当擂主确实存在、且血量超过某个门槛(如 > 15)时,才把它当作值得集火的强敌返回;否则返回 None,表示"没有值得特意集火的大目标"。return 一个有意义的"空结果",是很重要的编程习惯。

    # 只有存在且血量够高(是大食人魔)才值得集火
    if bestTarget and bestTarget.health > 15:
        return bestTarget
    else:
        return None   # 没有大目标,返回空

5.4 返回值用于判断

Using the Return Value in a Condition

5.4.1 目标是否存活(Is the Target Still Alive)

5.4.1.1 用返回值当判断条件 The return value drives the decision

函数返回的值,除了赋给变量,还能直接拿来做条件判断。图书馆谋士的主循环里有一句关键判断:"如果 archerTarget 被击败或不存在,那就找一个新的。"它检查两件事:目标不存在(not archerTarget,比如上一次返回了 None),或者目标已阵亡(archerTarget.health <= 0)。满足任一条件,就重新调用 findStrongestTarget() 找新目标。

while True:
    # 目标不存在,或血量已清零 → 需要重新寻找
    if not archerTarget or archerTarget.health <= 0:
        archerTarget = findStrongestTarget()   # 接住新目标
    # ...用 archerTarget 指挥弓箭手...
not 与 or 的配合:not archerTarget 在目标是 None(空)时为真;or 表示"两个条件满足任意一个即可"。合起来读作"如果没有目标,或者目标已经死了"。把两种"该换目标"的情况用 or 并在一起,逻辑清晰又周全。

5.4.2 记住目标不重找(Remember, Don't Re-search)

5.4.2.1 目标还活着,就继续打它 Keep attacking a living target

这个设计很巧妙:只有当目标死了或不存在时才重新寻找,否则就一直沿用 archerTarget。这带来两个好处。其一是火力集中——所有弓箭手锁定同一个大食人魔穷追猛打,直到它倒下,而不是每帧各打各的、把伤害分散。其二是效率——不必每帧都遍历全部敌人重新计算最大值,只在真正需要时才算一次。

目标还活着就继续打,死了才重新找 目标还活着? 是 否 继续命令弓箭手打它 重新 findStrongestTarget()

5.5 三角函数算坐标

Trigonometry for Offset Coordinates

5.5.1 极坐标转直角坐标(Polar to Cartesian)

5.5.1.1 用"角度 + 半径"算出"横竖偏移" From angle + radius to x, y offset

要让士兵们均匀地站成一圈,就得算出圈上每个点相对圆心的位置。数学上,一个点可以用"离圆心多远(半径 radius)+ 朝哪个方向(角度 angle)"来描述,这叫极坐标。而游戏里移动需要的是横坐标 x 和纵坐标 y,这叫直角坐标。两者之间的桥梁,正是三角函数 cos(余弦)和 sin(正弦)。

# 关卡已提供:把 半径+角度 换算成 x、y 偏移
def radialToCartesian(radius, degrees):
    radians = Math.PI / 180 * degrees   # 角度转弧度
    xOffset = radius * Math.cos(radians)  # 横向偏移 = 半径 × cos
    yOffset = radius * Math.sin(radians)  # 纵向偏移 = 半径 × sin
    return {"x": xOffset, "y": yOffset}
不必吃透数学,理解"输入输出"即可:关卡明确说过,"除了输入和输出以外,你不必完全理解这些函数做了什么"。你只要知道:给它一个半径和一个角度,它就返回一个 {x, y} 偏移量对象。这正是"把复杂细节封装进函数、只暴露简单接口"的价值——你可以站在巨人的肩膀上直接用。

5.5.2 围成一圈的偏移(Offsets Around a Circle)

5.5.2.1 让每个士兵分到不同的角度 Give each soldier a different angle

要把士兵均匀铺满一圈,关键是让第 i 个士兵分到角度 i * 360 / 士兵总数。比如 4 个士兵,就分到 0°、90°、180°、270°,正好四个方向。这和第四单元"用下标把单位均分到防御点"异曲同工——都是让下标 i 均匀地对应到一圈/一组位置上。

# 关卡已提供:第 i 个士兵相对农民的偏移
def findSoldierOffset(soldiers, i):
    angle = i * 360 / len(soldiers)   # 均分角度
    return radialToCartesian(5, angle)   # 半径 5 的圆上
4 个士兵 = 0°/90°/180°/270°,围农民成一圈 农民 0° 90° 180° 270°

5.6 实战关卡精讲

Level Strategy & Analysis

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

关卡 · 戒指运送人

💍 戒指运送人 —— 用偏移让士兵围成保护圈

扣哒世界 戒指运送人关卡 - koudashijie.com
图2:戒指运送人——护送持戒农民逃出山脉,让士兵围成一圈随行护卫,重在逃脱而非恋战

策略:结阵护送,逃而不战

本关目标是护送持戒农民安全撤离,不是打仗——山里潜伏着更多食人魔,恋战只会招来更多敌人。策略核心是让士兵在农民周围结成一个移动的保护圈,随农民一同前进;英雄自己也要跟紧农民。这样农民被士兵团团护住,边走边挡,安全脱身。

思路拆解:遍历士兵,各就各位

关卡已提供 findSoldierOffset(soldiers, i),你只要正确调用它。算法思路是:先用 findByType("soldier") 拿到士兵数组、用 findByType("peasant")[0] 拿到农民;然后 for i in range(len(soldiers)) 遍历每个士兵,用 findSoldierOffset 求出它的偏移量,把偏移量的 x、y 加到农民当前位置上,得到该士兵应站的绝对坐标,再 command(soldier, "move", moveTo) 命令它过去。

# 关键片段:偏移量 + 农民位置 = 士兵目标位置
soldiers = hero.findByType("soldier")
while True:
    for i in range(len(soldiers)):
        offset = findSoldierOffset(soldiers, i)
        x = peasant.pos.x + offset.x     # 偏移叠加到农民身上
        y = peasant.pos.y + offset.y
        moveTo = {"x": x, "y": y}
        hero.command(soldiers[i], "move", moveTo)
    # 英雄也要跟上农民一起走
"相对偏移"为什么每帧都要重算?因为农民在不断移动,圈的圆心时刻在变。偏移量 offset 是"相对农民"的固定相对位置,但 农民位置 + 偏移 得到的绝对坐标每帧都不同。放在 while 里每帧重算并重新下令,圈才能"跟着农民走",始终罩住他。
用 "move" 而非 "attack":本关是逃脱关,命令士兵 move 到指定位置保持队形,而不是 attack 去追敌人。追敌会打乱阵型、把农民暴露出去。牢记目标是"护送脱险",让队形优先于杀敌。
关卡 · 图书馆谋士

📚 图书馆谋士 —— 写函数找出最强敌人

扣哒世界 图书馆谋士关卡 - koudashijie.com
图3:图书馆谋士——Hushbaum 遭伏击,士兵结圈护住她与弓箭手,弓箭手集火生命值最高的大食人魔

策略:士兵结圈,弓箭手集火

本关 Hushbaum 被伏击,她忙于治疗,指挥全交给你。战术分两层:士兵围成一圈防守,把 Hushbaum 和弓箭手护在圈内(一旦敌人能碰到她或弓箭手,就会招来更多援军);弓箭手集火最强敌人,优先啃掉血最厚的大食人魔。士兵用关卡提供的 commandSoldier 结阵,弓箭手的目标选择则要你亲手实现。

思路拆解:三段核心逻辑

核心是补全三处。其一,写 findStrongestTarget:用前面讲的"擂主算法三步走"遍历敌人找血量最高者,并在结尾用 if bestTarget and bestTarget.health > 15 判断是否够格返回。其二,主循环里管理目标:用 if not archerTarget or archerTarget.health <= 0 判断是否需要重找,需要就 archerTarget = findStrongestTarget() 接住新目标。其三,写 commandArcher 并遍历弓箭手调用它:有强敌就打强敌,否则打最近的敌人。

# 关键片段一:找最强目标(擂主算法)
def findStrongestTarget():
    mostHealth = 0
    bestTarget = None
    enemies = hero.findEnemies()
    for i in range(len(enemies)):
        enemy = enemies[i]
        if enemy.health > mostHealth:
            bestTarget = enemy
            mostHealth = enemy.health
    if bestTarget and bestTarget.health > 15:
        return bestTarget
    else:
        return None
# 关键片段二:主循环按需重找 + 指挥弓箭手
while True:
    if not archerTarget or archerTarget.health <= 0:
        archerTarget = findStrongestTarget()
    soldiers = hero.findByType("soldier")
    archers = hero.findByType("archer")
    for i in range(len(archers)):
        commandArcher(archers[i])
commandArcher 里的"有强敌打强敌,否则打最近":函数里先取 nearest = archer.findNearestEnemy(),再判断:若全局 archerTarget 存在就 attack 它(集火),否则退而求其次 attack 最近的敌人。这样既保证有大目标时火力集中,又保证没有大目标时弓箭手也不会闲着。
为什么把弓箭手护在圈内?关卡明确提示:敌人一旦认为能够到 Hushbaum 或弓箭手,就会增派部队。让血厚的士兵在外圈 defend 结阵,把脆弱的治疗者和远程手护在中间,等于用"阵型"控制了敌人的增援节奏——这是防守反击的关键。

5.7 常见错误

Common Mistakes

5.7.1 最值算法与返回值的坑(Pitfalls)

5.7.1.1 找最大值相关的常见问题 Typical issues with max & return

扣哒世界 最值算法常见错误速查 - koudashijie.com
图4:忘更新纪录变量、没接住返回值、忘判断目标存活,是本单元最常见的失误
易错点清单:
  • 找到更强者只改 bestTarget,忘了同步 mostHealth = enemy.health——比较基准失效,找出的不是真最大值。
  • 初始 mostHealth 设得太高——若设成一个很大的数,所有敌人都比不过,永远找不到擂主,应设 0。
  • 函数写了却不 return——只找到 bestTarget 却不 return,外面拿到的是 None,等于白找。
  • 没用变量接住返回值——只写 findStrongestTarget() 而没有 archerTarget = ...,结果被丢弃。
  • 忘判断目标是否存活——不检查 health <= 0 就一直打死目标,弓箭手会对着尸体挥箭。
  • 把 not archerTarget or ... 写成 and——逻辑相反,会导致该重找时不重找。
  • 戒指运送人里忘加农民位置——只用了 offset 没加 peasant.pos,士兵会跑到地图原点附近。
  • 逃脱关误用 attack——该 move 保持队形却去 attack 追敌,阵型散了、农民暴露。
  • 试图完全看懂三角函数才敢用——其实只需理解"输入半径角度、输出 x/y 偏移"即可直接调用。

本单元小结 ✨

恭喜你掌握了编程中最经典的算法之一——最值算法!这一单元你彻底理解了 return 如何让函数交出结果、如何用 变量 = 函数() 接住返回值;亲手写出了"初始化擂台 → 逐个挑战 → 更新擂主"的遍历找最大值算法,找出生命值最高的大食人魔;学会把返回值用于条件判断(not archerTarget or health <= 0),做到目标存活就集火、阵亡才重找;还认识了用三角函数把角度换算成坐标偏移,让士兵围成保护圈。来回顾一下核心概念:

核心概念关键点对应关卡
return 返回值函数可交出任意值:数字、单位、坐标两关
返回值赋给变量变量 = 函数() 接住成果图书馆谋士
遍历找最大值初始化→逐个比较→更新擂主图书馆谋士
同步更新纪录换擂主时 bestTarget 与 mostHealth 一起改图书馆谋士
返回值用于判断用它判断目标是否存活/是否重找图书馆谋士
集火与效率目标活着就沿用,不必每帧重算图书馆谋士
极坐标转直角cos/sin 把角度+半径变 x/y 偏移戒指运送人
偏移叠加定位农民位置 + 偏移 = 士兵目标位置戒指运送人

懂得从千军万马中一眼锁定最强之敌、又能让部队结阵护主的英雄,才是云峰之上真正决胜千里的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️💍📚

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

扣哒世界教研团队