去做题

第四单元:模运算 · 让索引优雅地循环

Unit 4: The Modulo Operator — Wrapping Indices Elegantly
计算机科学 5 · 安息云峰 Cloudrip Mountain · 联合作战 → 钢爪间隙

4.1 什么是模运算

What Is the Modulo Operation

在前几个单元里,你学会了封装函数、用分支做决策。这一单元你要认识一个看起来不起眼、却能优雅解决"轮流"和"循环"问题的运算——模运算(modulo),也就是求余数。它能让你用极短的一行代码,实现"士兵、弓箭手轮着召唤""把部队均匀分配到几个防御点""索引无论多大都不会越界"这些实用效果。

本单元主线:你将掌握模运算符 %(求两数相除的余数)、用 len() 获取数组长度,并把二者结合成一个万能公式:索引 % 数组长度。这个公式能让索引在数组范围内"周而复始地折返",从而实现兵种轮换(len(hero.built) % len(summonTypes))、单位均分到多个防御点(i % len(defendPoints)),并从根本上杜绝数组越界。

4.1.1 余数的日常直觉(The Everyday Intuition of Remainders)

4.1.1.1 从"分东西"到"看钟表" From sharing items to reading a clock

余数其实你早就会了。把 12 颗糖平均分给 5 个小朋友,每人 2 颗,还剩 2 颗——这个"剩下的 2"就是余数,写作 12 % 5 == 2。再想想钟表:现在 10 点,过 5 小时是几点?(10 + 5) % 12 == 3,是 3 点——时针走到 12 就自动"折返"回头。模运算的精髓就是这种转圈、折返、周而复始的感觉。

🕐 模运算就像"钟表"
钟面只有 12 个数,但时间可以无限增加。无论过了多少小时,时针总会落在 1~12 之间——因为它每到 12 就绕回起点。
数组也只有有限个格子,但索引可以无限增加。用 % 长度,就能让索引像时针一样,永远落在有效范围内。

4.1.2 为什么需要它(Why We Need It)

4.1.2.1 用一个不断变大的数,反复取用有限的选项 Reusing a small set with an ever-growing counter

游戏里你会不停召唤部队,第 1 个、第 2 个、第 100 个……这个"已造数量"只增不减、越来越大。但你想召唤的兵种只有那么几种(比如士兵、弓箭手两种)。怎么让一个不断变大的数字,反复地、循环地对应到这少数几个选项上?答案就是模运算。它把"无限增长的计数"折叠回"有限的选项范围",让轮换自然发生。

不断变大的计数,经过 %,折回有限选项 0 1 2 3 4 5 6 7 … 已造数量(越来越大) % 2 0 1 0 1 0 1 … 只在 0、1 之间打转 → 士兵、弓箭手、士兵、弓箭手…轮流召唤

4.2 模运算符 %

The Modulo Operator %

4.2.1 a % b 的含义(What a % b Means)

4.2.1.1 相除,取余 Divide, then take the remainder

关卡把它说得很清楚:"a % b 返回 a 除以 b 的余数(作为一个整数)。所以 12 % 5 == 2。"符号 % 在这里不是百分号,而是求余运算符。它的运算方法是:a 里能塞进多少个完整的 b,剩下不够再凑一个 b 的部分,就是余数。

12 % 5 == 2 12 % 5 = 2 被除数 取余 除数 余数 12 里有 2 个 5(=10),剩下 2

4.2.2 余数的循环规律(The Cyclical Pattern of Remainders)

4.2.2.1 余数永远在 0 到 b-1 之间转圈 Remainders always cycle from 0 to b-1

这是模运算最重要的性质:无论被除数多大,a % b 的结果永远落在 0 到 b-1 之间,而且随着 a 递增,余数会 0, 1, 2, …, b-1, 0, 1, 2, … 这样周而复始地循环。关卡用一个数组的例子把这个规律列得明明白白:

# 数组有 4 个元素,用 i % 4 环绕它
summonTypes = ["soldier", "archer", "peasant", "paladin"]

0 % 4 == 0   # → "soldier"
1 % 4 == 1   # → "archer"
2 % 4 == 2   # → "peasant"
3 % 4 == 3   # → "paladin"
4 % 4 == 0   # → "soldier"  绕回开头!
5 % 4 == 1   # → "archer"   ……如此循环
i % 4 永远在 0→1→2→3→0 之间转圈 0 1 2 3 soldier archer peasant paladin

4.3 len 与数组长度

len() and Array Length

4.3.1 len 获取长度(Getting the Length with len)

4.3.1.1 数一数数组里有几个元素 Counting the elements

len(数组) 返回数组里元素的个数。比如 summonTypes = ["soldier", "archer"],那么 len(summonTypes) 就是 2。len(hero.built) 则是你到目前为止造过的部队总数。把 len() 用作 % 的除数,是本单元最核心的组合——因为数组长度正是"选项的总数",用它取余,索引就永远不会跑到数组外面去。

用 len 而非写死数字:写 % len(summonTypes) 比写 % 2 更好。将来你往 summonTypes 里多加一个兵种,len() 会自动变成 3,代码无需任何改动就能正确轮换三种兵。让代码自己去数长度,别把长度写死。

4.3.2 索引与越界(Indexing and Out-of-Range)

4.3.2.1 索引从 0 开始,最大是"长度减一" Indices start at 0, max is length minus one

数组的索引从 0 开始数,一个有 n 个元素的数组,合法索引是 0 到 n-1。一旦你用一个 ≥ n 的索引去访问,就会越界(out of range),程序报错。而不断增长的计数器(如已造部队数)迟早会超过数组长度——这正是模运算大显身手的地方:索引 % 长度 保证结果永远在合法范围内。

❌ 直接用大索引 → 越界
# summonTypes 只有 2 个元素
i = 5
type = summonTypes[i]
# ✗ 索引 5 越界,报错!
✅ 先取余 → 永不越界
i = 5
idx = i % len(summonTypes)
# 5 % 2 == 1
type = summonTypes[idx]  # ✓ 安全

4.4 取余实现循环折返

Wrapping Indices with Modulo

4.4.1 索引 % 长度(index % length)

4.4.1.1 本单元的万能公式 The one formula that does it all

把前面所有零件拼起来,就得到本单元反复出现的黄金公式:

数组[ 一个越来越大的数 % 数组长度 ] summonTypes [ len(hero.built) % len(summonTypes) ] 已造数量不断变大,%长度后折回 0~长度-1,正好当索引

它的读法是:"用已经造过的部队数量对兵种数组的长度取余,得到一个永远合法的索引,再拿它去数组里取出这一次该召唤的兵种"。每召唤一个,len(hero.built) 就加一,取余结果就往后挪一格,从而实现自动轮换。

4.4.2 兵种轮换(Rotating Unit Types)

4.4.2.1 按数组顺序,一种接一种地造 Summon types in round-robin order

"联合作战"关卡就是练这个。你先在 summonTypes 里编排好想要的兵种和顺序,比如 ["soldier", "archer"],然后用公式挑出本次该召唤的类型。已造 0 个时造士兵,造完变 1 个时造弓箭手,变 2 个时 2 % 2 == 0 又回到士兵……士兵、弓箭手就这样 1:1 地交替建造。

# 编排兵种与顺序
summonTypes = ["soldier", "archer"]

def summonTroops():
    # 用取余在数组里不断折返,挑出本次兵种
    type = summonTypes[len(hero.built) % len(summonTypes)]
    if hero.costOf(type) <= hero.gold:
        hero.summon(type)
想改配比?改数组就行:如果想让士兵更多,把数组写成 ["soldier", "soldier", "archer"],轮换就变成士兵、士兵、弓箭手,即 2:1 的配比。数组的内容和重复次数直接决定了兵种比例,非常灵活——这正是"数据驱动"的思想:改数据,不改逻辑。

4.4.3 防止越界(Preventing Out-of-Range Errors)

4.4.3.1 取余是天然的"安全护栏" Modulo is a natural safety guard

关卡默认代码的注释一语道破:"这可以用在索引可能大于长度时,能够绕回数组的开头。"len(hero.built) 会一直增长到几十、上百,但只要套上 % len(summonTypes),结果永远被"关"在 0 ~ 长度-1 之内,绝不会越界。你不必写任何 if 去检查索引超没超——取余本身就是护栏。

4.5 均分到多个点

Distributing Units Across Points

4.5.1 单位均分防御点(Spreading Units Evenly)

4.5.1.1 用取余把每个单位分派到一个点 Assign each unit to a point by modulo

模运算的另一个妙用是均分。"钢爪间隙"要把 8 个单位平均派到 4 个防御点,每点两个。诀窍是:遍历友军时,用每个友军的下标 i 对防御点数量取余,defendPoints[i % len(defendPoints)],就能算出它该去哪个点。下标 0、1、2、3 分到 4 个不同的点,下标 4、5、6、7 又依次绕回这 4 个点——每点正好两个,自动均分。

8 个单位 × (i % 4) → 4 个点各 2 个 i=0→点0 i=1→点1 i=2→点2 i=3→点3 i=4→点0 i=5→点1 i=6→点2 i=7→点3 点0 ← 2 个 点1 ← 2 个 点2 ← 2 个 点3 ← 2 个 下标绕回,兵力天然平均分布
轮换与均分,一体两面:"兵种轮换"是让不同的计数轮流对应数组里的元素;"单位均分"是让不同的单位下标轮流对应不同的防御点。两者本质完全相同——都是 索引 % 长度 让一个递增的数在有限选项里循环。看懂一个,另一个自然就通了。

4.6 实战关卡精讲

Level Strategy & Analysis

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

关卡 · 联合作战

🤝 联合作战 —— 用取余轮换兵种

扣哒世界 联合作战关卡 - koudashijie.com
图2:联合作战——开放式防御关,找出最佳兵种组合,用取余让士兵与弓箭手交替成军,撑过食人魔攻势

策略:编排数组,找出最佳组合

这是一个开放式防御关,目标是生存 30 秒并收集 300 金钱(进阶挑战是撑到 60 秒)。关卡不限定你造什么兵,而是让你通过编排 summonTypes 数组来决定兵种的种类、顺序和配比。策略核心就是找到一套攻守兼备的组合——比如士兵扛线、弓箭手输出,让 ["soldier", "archer"] 交替成军。

思路拆解:三件事各一个函数

整体沿用前几单元"一个函数做一件事"的结构,主循环每帧调用三件事:summonTroops 用取余公式轮换召唤、gatherCoins 移动去捡最近的金币维持经济、commandTroops 遍历友军各打最近的敌人。其中最关键的是 summonTroops 里的取余选型,以及召唤前用 if hero.costOf(type) <= hero.gold 判断"买得起才召唤"。

# 关键片段:编排数组 + 取余轮换
summonTypes = ["soldier", "archer"]

def summonTroops():
    type = summonTypes[len(hero.built) % len(summonTypes)]
    if hero.costOf(type) <= hero.gold:
        hero.summon(type)
指挥时要跳过栅栏(palisade):关卡小贴士提醒,在 commandTroops 里遍历友军时,要跳过任何 type == "palisade" 的盟友。栅栏是不能移动、不能攻击的防御建筑,对它下达"攻击"命令毫无意义甚至报错。用一句 if friend.type == "palisade": continue(或反过来只对能战斗的单位下令)即可优雅避开。
开放关的调优思路:没通过?先想是"钱不够扩军"还是"兵种被克制"。可以调整数组配比(多加士兵扛线,或多加弓箭手输出)、优化捡币逻辑攒更多钱、或确保每帧都在指挥友军攻击。开放关的乐趣正在于反复试验、逼近最优解。
关卡 · 钢爪间隙

🛡️ 钢爪间隙 —— 用取余均分防御点

扣哒世界 钢爪间隙关卡 - koudashijie.com
图3:钢爪间隙——四面受敌,把 8 个单位用取余均匀派往 4 个防御点,每点两名,守住临时栅栏

策略:八个单位,四点布防

本关英雄被敌人四面包围,要靠部队守住一道临时栅栏。关卡设定得很直白:召唤 8 个单位、布防 4 个点,每点两个。这就要求把兵力均匀撒开,而不是全挤在一处——四个方向都得有人守。实现均分的钥匙,正是模运算 unitIndex % 4。

思路拆解:两个数组 + 两处取余

本关有两个预置数组:defendPoints 存 4 个防御点坐标,summonTypes 存 8 个单位的兵种顺序(4 士兵 + 4 弓箭手)。取余在两处发力。其一,召唤:summonTroops 仍用 len(hero.built) % len(summonTypes) 按顺序把 8 种配置造出来。其二,布防:commandTroops 遍历友军时,用友军下标 i % len(defendPoints) 算出它该去的点,再 hero.command(friend, "defend", point)。

# 关键片段:用下标取余把每个友军派到一个防御点
def commandTroops():
    friends = hero.findFriends()
    for i in range(len(friends)):
        friend = friends[i]
        # 下标 % 防御点数量 → 绕回 defendPoints
        point = defendPoints[i % len(defendPoints)]
        hero.command(friend, "defend", point)
为什么 i % 4 就能均分?因为下标是连续的 0、1、2、3、4、5、6、7,对 4 取余后依次是 0、1、2、3、0、1、2、3——前四个各占一个点,后四个又各绕回同一个点。只要单位总数是防御点数的整数倍,每个点分到的数量就完全相等。8 个单位、4 个点,每点正好 2 个。
"defend" 与 "attack" 的区别:本关用的是 command(friend, "defend", point)——让单位驻守某个坐标点,敌人靠近才应战。这和前面关卡的 command(friend, "attack", enemy)(主动扑向某个敌人)不同。四面受敌时,"守住阵位"比"追着敌人跑"更稳,能防止阵型被敌人各个击破。

4.7 常见错误

Common Mistakes

4.7.1 取余与索引的典型坑(Pitfalls)

4.7.1.1 模运算相关的常见问题 Typical issues with modulo & indexing

扣哒世界 模运算常见错误速查 - koudashijie.com
图4:忘了取余导致越界、除数写死、把 % 当百分号,是本单元最常见的失误
易错点清单:
  • 忘了取余直接用大索引——summonTypes[len(hero.built)] 迟早越界报错,必须 % len(summonTypes)。
  • 取余和索引位置写反——应是 数组[i % len(数组)],别写成 数组[i] % len(数组)(那是先取元素再取余,逻辑全错)。
  • 把除数写死成数字——写 % 2 而非 % len(summonTypes),一旦数组增减元素就轮换错乱。
  • 把 % 当成百分号理解——它是求余运算符,12 % 5 是 2,不是"百分之十二"。
  • 用错取余的对象——兵种轮换用 len(hero.built),均分防御点用友军下标 i,两者别混用。
  • 召唤前不判断金钱——应先 if hero.costOf(type) <= hero.gold,钱不够硬召唤无效。
  • 指挥时没跳过栅栏——对 type == "palisade" 的盟友下攻击/移动命令没意义,要跳过。
  • 混淆 "defend" 与 "attack"——钢爪间隙要 defend 守点,别错用成 attack 追敌。
  • 单位数不是点数的整数倍——若总数不能被点数整除,各点会差一个单位,布防不完全均匀(本关 8÷4 正好整除)。

本单元小结 ✨

恭喜你掌握了模运算这件"以小博大"的利器!这一单元你理解了求余运算符 % 的含义(a % b 取相除的余数)与它周而复始、绝不越界的循环特性,学会了用 len() 取得数组长度,并把二者拼成本单元的黄金公式 索引 % 数组长度。你用它在"联合作战"里让士兵、弓箭手轮换成军,又在"钢爪间隙"里把 8 个单位均分到 4 个防御点,还顺带彻底解决了数组越界的隐患。来回顾一下核心概念:

核心概念关键点对应关卡
模运算符 %a % b 取余数,如 12%5==2两关
余数循环性结果永远在 0 ~ b-1 之间转圈两关
len 取长度数组元素个数,作 % 的除数两关
索引 % 长度让不断变大的索引绕回数组开头两关
兵种轮换len(hero.built) % len(summonTypes)联合作战
单位均分i % len(defendPoints) 平均布防钢爪间隙
防止越界取余是天然的安全护栏两关
数据驱动配比改数组内容即改兵种比例联合作战

懂得让一个小小的余数,指挥千军万马轮番上阵、四面布防的英雄,才是云峰之上真正举重若轻的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🤝🛡️

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

扣哒世界教研团队