去做题

第三单元:均值筛选 · 求和平均与多条件选优

Unit 3: Average Filtering — Summation, Mean & Multi-Condition Selection
计算机科学 6 · 安息云峰 Cloudrip Mountain · 寻找开始 → 诱骗牦牛 → 睡眠样本

3.1 均值筛选概述

Overview of Average-Based Filtering

前两个单元你学会了协作指挥和反转操作。这一单元,捕手 Senick 要在云峰上追踪一头传说中体型无与伦比的树妖——Burleous Majorus。要抓住它,Senick 得先做一件很"科学"的事:测量一群单位的平均大小,把平均值当作基线(baseline),再从中筛选出那些"超出平均水平"的目标。这正是数据分析的思想——先算出群体的整体水平,再据此判断个体是否突出。本单元的核心,就是求平均值与多条件筛选。

本单元主线:用累加器模式遍历数组求和;用 sum / length 计算平均值并把它封装成 average 函数;用 and 把"距离更近"和"大小高于平均值"两个条件组合起来做多条件筛选;在遍历中不断更新那个"最近且满足条件"的最佳元素;最后把选出的最佳目标对象返回给调用者。

3.1.1 为什么用平均值(Why the Average)

3.1.1.1 平均值是一条"基线" The average is a baseline

Senick 不知道 Burleous Majorus 到底该有"多大"才算大。孤立地看一个数字没有意义——身高 3 米算大吗?如果周围的树妖平均身高就是 3 米,那它只是普通个头。所以正确的做法是先算出整个种群的平均大小,把它当作一条参考基线:凡是超过平均值的,才是"偏大"的候选。平均值把绝对数值变成了有意义的相对判断。

📏 平均值 = 群体的"中间水平"
平均值告诉我们"典型的一个"大概是什么样。有了它,我们就能回答"这个个体是高于还是低于群体水平"这类问题。在关卡里,Senick 正是靠"大小 > 平均值"来剔除小个子、锁定真正够大的诱饵和目标。

3.1.2 两步走思路(The Two-Step Approach)

3.1.2.1 先求均值,再做筛选 First compute the mean, then filter

本单元所有关卡都遵循同一个"两步走"结构。第一步:遍历所有单位,求出它们某个属性(如 size)的平均值。第二步:再遍历一次,用这个平均值作为筛选标准,挑出满足条件(高于平均且距离最近)的最佳目标。理解这个"先统计、后决策"的两遍遍历结构,就抓住了整个单元的骨架。

第一步:求平均值 遍历求和 → sum / length 第二步:多条件筛选 高于均值 且 距离最近

3.2 遍历数组求和

Summing an Array by Iteration

3.2.1 累加器模式(The Accumulator Pattern)

3.2.1.1 从 0 开始,逐个加进去 Start from zero, add each one in

求和是求平均的第一步,用的是编程里最经典的累加器模式(accumulator pattern):先建一个初始为 0 的变量 sum,然后遍历数组,把每个元素的目标属性一个个加到 sum 上。循环结束时,sum 里装的就是所有元素的总和。这个"变量+循环累加"的套路,是数据统计的基本功。

累加器:sum 从 0 开始不断长大 sum=0 +3→ sum=3 +5→ sum=8 +2→ sum=10 ✓ 遍历 [3, 5, 2],总和为 10
# 累加器模式:求数组中所有元素 size 的总和
def sumSize(burls):
    sum = 0                        # 1. 初始化累加器
    for i in range(len(burls)):    # 2. 遍历每个元素
        burl = burls[i]
        sum += burl.size          # 3. 把它的 size 加进去
    return sum                    # 4. 返回总和
sum 必须在循环外初始化:sum = 0 一定要写在 for 循环之前。如果放进循环里,每一轮都会把 sum 重置为 0,最后只会剩下最后一个元素的值,得不到总和。

3.2.2 用索引访问属性(Accessing Attributes by Index)

3.2.2.1 array[i].属性名 array[i].attribute

关卡里的数组元素都是单位对象,每个对象带着自己的属性(如 size、health、pos)。用 range(len(array)) 遍历时,i 是索引,array[i] 是第 i 个对象,array[i].size 才是它的大小。也可以先把对象取出到变量再访问,两种写法等价,后者更易读。

先取对象(更清晰)
for i in range(len(burls)):
    burl = burls[i]
    sum += burl.size
直接下标(更紧凑)
for i in range(len(burls)):
    sum += burls[i].size

3.3 计算平均值

Computing the Average

3.3.1 平均值 = 总和 ÷ 数量(sum / length)

3.3.1.1 一个除法就搞定 Just one division

有了总和,求平均值就非常简单——用总和除以元素的数量。数量就是数组的长度,在关卡里写作 burls.length(标准 Python 里是 len(burls))。这一个除法,就把一堆数字浓缩成了一个代表"整体水平"的值。

平均值 = 总和 ÷ 数量 sum / len(array) 所有大小加起来 一共几个 → average
# 平均值的最小例子
sum = 1 + 2 + 5 + 4   # 总和 = 12
average = sum / 4          # 4 个元素 → 平均值 = 3

3.3.2 封装成 average 函数(Encapsulate an average Function)

3.3.2.1 一个函数完成"求和 + 相除" One function to sum and divide

把求和与相除合并进一个函数,就得到了通用的求平均值工具。它接收一个数组,内部用累加器求和,最后返回 sum / len(array)。这样主程序只要写一句 averageSize(yaks) 就能拿到平均大小,干净利落。这正是"睡眠样本"关卡要求你从头独立实现的函数。

# 通用的求平均大小函数(求和与相除合二为一)
def averageSize(units):
    sum = 0                        # 初始化累加器
    for i in range(len(units)):
        unit = units[i]
        sum += unit.size          # 累加每个单位的大小
    return sum / len(units)      # 总和 ÷ 数量 = 平均值
四步口诀(来自关卡提示):写 average 函数只需记住四句话——初始化 sum 变量;遍历每个元素;把元素的 size 加到 sum;返回 sum 除以数组长度。任何"求某属性平均值"的需求,都套这四步。
当心空数组:如果传进来的数组是空的,len(units) 为 0,除法会出错(除以零)。稳妥的做法是在相除前先判断 if len(units) > 0,否则返回 0 或跳过,避免程序崩溃。

3.4 多条件筛选

Multi-Condition Filtering

3.4.1 用 and 组合条件(Combining with and)

3.4.1.1 两个条件必须同时成立 Both conditions must hold at once

"诱骗牦牛"关卡的挑战在于:好诱饵既要够大、又要够近。只看距离不行——可能选到一头很近的小牦牛,骗不到大树妖;只看大小也不行——最大的那头可能远在牛群深处,跑过去会激怒整群牦牛。所以要用逻辑运算符 and 把两个条件串成一个:只有同时满足"大小 > 平均值"且"距离 < 当前最近"的牦牛,才算合格候选。

两个条件用 and 连接,缺一不可 条件① 大小 size > avgSize and 条件② 距离 distance < closestDist 两者都为 True,这头牦牛才是新的最佳候选

3.4.2 距离近且高于均值(Closer AND Above Average)

3.4.2.1 把两个判断写进一个 if Put both tests in one if

在遍历牦牛的循环里,对每头牦牛先算出它的距离 yakDistance 和大小 yakSize,然后用一个 if 同时检查两个条件。只有当这头牦牛比目前记录的最佳更近,并且大小超过平均值时,才认为它是"更好的选择",进而更新记录。

# 用 and 同时检查"高于均值"和"更近"两个条件
yakDistance = hero.distanceTo(yak)
yakSize = yak.size
if yakSize > avgSize and yakDistance < closestDist:
    # 这头牦牛又大又近,是更好的候选
    ...
条件顺序与含义别混:务必是 yakSize > avgSize(大小要大于平均)而不是小于,否则会选到小牦牛。同样,距离要 < closestDist(越来越近)。把大于小于写反,是本关最常见的失误。

3.5 更新最佳目标

Updating the Best Target

3.5.1 条件擂台算法(The Conditional Tournament)

3.5.1.1 带筛选条件的"打擂台" A tournament with a filter

这其实是第一单元"打擂台求最值"的升级版。区别在于:现在擂主的更换不只看一个指标,还要先通过筛选条件。做法是设两个记录变量——bestYak = None(当前最佳)和 closestDist = Infinity(当前最近距离,初始设为无穷大)。遍历每头牦牛,若它同时满足两个条件,就让它当新擂主:更新 bestYak 和 closestDist。遍历结束,擂台上就是那头"高于平均、且在合格者中最近"的最佳牦牛。

# 条件擂台:找出高于均值且最近的最佳牦牛
bestYak = None
closestDist = 9999          # 初始设成很大的距离
for i in range(len(yaks)):
    yak = yaks[i]
    yakDistance = hero.distanceTo(yak)
    yakSize = yak.size
    if yakSize > avgSize and yakDistance < closestDist:
        bestYak = yak            # 更新最佳目标
        closestDist = yakDistance  # 更新最近记录
为什么 closestDist 初始设很大:因为我们求的是"最近"(最小距离)。初始 closestDist 必须比任何真实距离都大(如 9999 或 Infinity),这样第一头合格的牦牛就一定能"打赢"它、成功成为擂主。这和第二单元求最小血量设 99999 是同一个道理。
两个变量要一起更新:发现更好的牦牛时,bestYak 和 closestDist 必须同时更新。只更新 bestYak 却忘了更新 closestDist,下一次比较用的还是旧距离,擂台机制就失灵了。

3.5.2 返回目标对象(Returning the Target Object)

3.5.2.1 返回的是对象,不是数字 Return the object, not a number

筛选的最终产物是一个单位对象(那头最佳牦牛),而不仅仅是它的大小或距离。把整个 bestYak 对象保存下来并返回,调用方才能对它下命令——比如 hero.say(bestYak) 标出它,或指挥单位移向 bestYak.pos。记住:我们要的是"哪一个",所以保存和返回的应当是元素本身。

🎯 数字 vs. 对象
closestDist 是一个数字,只用来比较;bestYak 是一个对象,是我们真正想要的结果。很多新手只记住了最小的距离值,却忘了同时记住"是哪头牦牛",导致循环结束后手里只有一个数字、找不到目标本体。

3.6 实战关卡精讲

Level Strategy & Analysis

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

关卡 · 寻找开始

🌲 寻找开始 —— 求树妖的平均大小

扣哒世界 寻找开始关卡 - koudashijie.com
图2:寻找开始——趁树妖迁徙时测量种群平均大小,作为日后识别传说树妖 Burleous Majorus 的基线

策略:先建立"平均大小"这条基线

Senick 要找传说中体型无与伦比的 Burleous Majorus,可他不知道"多大才算大"。本关的任务是趁树妖迁徙,测出这群树妖的平均大小并说出来,作为将来识别巨型树妖的参考基线。这是整个单元的起点——一切筛选都建立在"平均值"之上。

思路拆解:拆成两个函数

关卡把求平均值拆成了两个协作的函数。sumSize 负责用累加器模式遍历所有树妖、把它们的 size 累加成总和;averageSize 则调用 sumSize 拿到总和,再除以数量得到平均值。这种"求和归求和、相除归相除"的分工,体现了函数职责单一的思想。

# 关键片段:sumSize 求总和,averageSize 求平均
def sumSize(burls):
    sum = 0
    for i in range(len(burls)):
        sum += burls[i].size   # 累加每个树妖的大小
    return sum

def averageSize(burls):
    sum = sumSize(burls)       # 借助 sumSize 拿到总和
    return sum / len(burls)   # 总和 ÷ 数量
主循环怎么用:在 while True 里先用 hero.findEnemies() 拿到树妖数组,调用 averageSize 得到平均值,再用 hero.say(avgSize) 说出来。这样每帧都会报告当前视野内树妖的平均大小。
pass 只是占位:默认代码里 sumSize 函数体是一个 pass,那只是"什么都不做"的占位符,提醒你这里需要自己填写累加逻辑。真正实现时要把 pass 替换成累加器代码。
关卡 · 诱骗牦牛

🐂 诱骗牦牛 —— 挑一头又大又近的诱饵

扣哒世界 诱骗牦牛关卡 - koudashijie.com
图3:诱骗牦牛——为陷阱挑选一头大小高于平均值、且距离最近的牦牛作诱饵,切勿深入牛群激怒整群

策略:大陷阱要配大诱饵,但别太深入

要抓大树妖 Majorus,需要一头够分量的诱饵牦牛。本关的两难在于:诱饵必须大小高于平均值(够大才骗得到大树妖),但又不能选牛群深处那一头(跑太远会激怒整群牦牛)。所以最佳策略是——在所有"高于平均大小"的牦牛里,挑距离英雄最近的那一头。这就把"大小"和"距离"两个属性结合了起来。

思路拆解:均值 + 条件擂台

算法分两步。第一步补全 averageSize,求出牦牛的平均大小 avgSize。第二步做条件擂台:设 bestYak = None、closestDist = Infinity,遍历每头牦牛,用 if yakSize > avgSize and yakDistance < closestDist 同时检查"够大"和"更近",满足就更新 bestYak 与 closestDist。最后 hero.say(bestYak) 标出这头最佳诱饵。

# 关键片段:在高于均值的牦牛中找最近的一头
avgSize = averageSize(yaks)
bestYak = None
closestDist = 9999
for i in range(len(yaks)):
    yak = yaks[i]
    yakDistance = hero.distanceTo(yak)
    if yak.size > avgSize and yakDistance < closestDist:
        bestYak = yak
        closestDist = yakDistance
hero.say(bestYak)
别只按距离选:本关的核心提醒是——不要把"最近"当作唯一标准。如果只写 if yakDistance < closestDist,很可能选中一头很近的小牦牛,Senick 就抓不到大树妖了。size > avgSize 这个条件绝不能省。
为什么"最近"能防激怒:牛群深处的牦牛跑过去要穿过整群,容易惊动其他牦牛。在合格(够大)的牦牛中选最近的,就能尽量在牛群边缘取饵,降低激怒风险。这是"距离条件"背后的实战考量。
关卡 · 睡眠样本

💤 睡眠样本 —— 从头独立写 average 函数

扣哒世界 睡眠样本关卡 - koudashijie.com
图4:睡眠样本——计划的最后一步,独立从零实现求平均值的函数,测出雪人的平均尺寸

策略:计划收官,独立实现均值函数

这是 Senick 大计划的最后一步。前两关你分别学会了"求和"和"求和+相除",本关不再给你任何脚手架——要求你从零开始、独立地写出完整的 average 函数,测出雪人的平均尺寸。这是对本单元核心技能的一次综合检验。

思路拆解:先按类型找,再套四步模板

算法很清晰。第一步用 hero.findByType("yeti") 精确找出所有雪人(而不是所有敌人),存进 yetis。第二步套用求平均值的四步模板:初始化 sum = 0、遍历累加每个雪人的 size、返回 sum / len(yetis)。第三步用 hero.say(...) 说出平均尺寸。

# 关键片段:findByType 精确取雪人,再求平均
yetis = hero.findByType("yeti")   # 只要雪人

def averageSize(yetis):
    sum = 0
    for i in range(len(yetis)):
        sum += yetis[i].size
    return sum / len(yetis)

hero.say(averageSize(yetis))   # 说出雪人平均尺寸
findByType 比 findEnemies 更精准:本关要的是"雪人"这一特定类型,用 findByType("yeti") 能只挑出雪人,避免把其他单位也算进平均值里。当你只关心某一类单位时,findByType 往往比笼统的 findEnemies 更合适。
这是一次"默写":睡眠样本没有默认代码框架,正是要检验你能否凭记忆完整写出求平均值的四步。如果卡住了,回想那句口诀:初始化 sum、遍历、累加 size、返回 sum 除以长度。

3.7 常见错误

Common Mistakes

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

3.7.1.1 均值筛选的典型问题 Typical issues with average filtering

扣哒世界 均值筛选常见错误速查 - koudashijie.com
图5:sum 位置错、忘记相除、条件写反、只更新一个变量,是本单元最常见的失误
易错点清单:
  • sum 在循环内初始化——sum = 0 必须放在 for 循环之前,放里面每轮重置会只剩最后一个元素的值。
  • 求和后忘记相除——average 函数要 return sum / len(array),只 return 总和就成了"求和"而非"求平均"。
  • 大小条件写反——诱饵要 size > avgSize(大于平均),写成小于会选到小牦牛,骗不到大树妖。
  • 只按距离选、漏掉大小条件——必须用 and 同时判断大小与距离,只看距离会选错目标。
  • closestDist 初始值太小——求最近距离时初值要设很大(9999 或 Infinity),设小了第一个候选也刷不进去。
  • 两个记录变量没同步更新——发现更优目标时 bestYak 和 closestDist 要一起更新,只更新一个会让擂台失灵。
  • 只记住了数字、忘了记对象——要保存 bestYak 这个对象本身,光有 closestDist 数字找不到目标。
  • 除以零——空数组时 len 为 0,相除会报错,必要时先判断数组非空。
  • 用 findEnemies 代替 findByType——只需某类单位(如雪人)时,笼统的 findEnemies 会混入其他单位污染平均值。

本单元小结 ✨

这一单元你和捕手 Senick 一起,把"数据分析"的思想带进了云峰狩猎!你掌握了用累加器模式遍历数组求和——从 0 开始、逐个把属性加进去;学会了用 sum / length 计算平均值,并把它封装成通用的 average 函数;懂得了用 and 把"大小高于平均"和"距离更近"两个条件组合起来做多条件筛选;通过条件擂台不断更新那个最近又合格的最佳目标,并把整个目标对象返回给调用者。在"寻找开始""诱骗牦牛""睡眠样本"三关里,你从测基线、到按均值筛诱饵、再到独立默写均值函数,一步步走完了 Senick 的完整计划。来回顾一下核心概念:

核心概念关键点对应关卡
遍历数组求和sum=0 起步,循环累加属性寻找开始
函数分工sumSize 求和,averageSize 相除寻找开始
计算平均值sum / len(array)三关
多条件筛选size > avg and dist < closest诱骗牦牛
条件擂台closestDist 初值设大,逐步刷新诱骗牦牛
更新最佳目标bestYak 与 closestDist 同步更新诱骗牦牛
返回对象保存元素本身,而非只存数字诱骗牦牛
独立写均值函数findByType + 四步模板睡眠样本

懂得先测出群体基线、再用多个条件权衡取舍、最后精准锁定最佳目标的英雄,才是既有数据头脑、又有决断力的真正猎手。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️📊🐂

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

扣哒世界教研团队