去做题

第十四单元:二维数组(列表)· 网格里的世界

Unit 14: 2D Arrays (Lists) — The World in a Grid
计算机科学 5 · 安息云峰 Cloudrip Mountain · 力量的源点 → 危险谷

14.1 什么是二维数组

What Is a 2D Array

欢迎来到计算机科学 5 的第十四单元!到目前为止,你已经能熟练使用普通数组(一维列表)——把一排数据整整齐齐排成一行。但真实世界里有太多东西不是"一排"能装下的:棋盘、地图、屏幕上的像素、Excel 表格……它们都是有行有列的方格世界。本单元你将掌握描述这类结构的利器——二维数组(2D array),也叫嵌套数组(nested array)。

本单元主线:二维数组就是"数组的数组"。你将学会用 grid[i][j] 读取第 i 行第 j 列的元素,用双层 for 循环把整张网格从头扫到尾,再根据每个格子的值(正数 / 1 / 0)决定英雄该做什么,最后把二维的"行列索引"翻译成游戏世界里真实的笛卡尔 (x, y) 坐标。两关"力量的源点"与"危险谷"就是这套技能的完整演练。

14.1.1 数组的数组(An Array of Arrays)

14.1.1.1 把多个数组装进一个数组 Nesting arrays inside an array

关卡"危险谷"的概览一句话点破本质:"嵌套数组,或二维数组,是一种……包含更多数组的数组!"普通数组里装的是数字或字符串,而二维数组里装的每一个元素本身又是一个数组。看下面这个最简单的例子:

# 挤成一行时看着复杂
doubleArray = [[1, 2], [3, 4]]

# 整理换行后,结构一目了然
doubleArray = [
    [1, 2],   # ← 第 0 行,本身是一个数组
    [3, 4]    # ← 第 1 行,也是一个数组
]
换行是好习惯:写二维数组时,把每一行单独放一行,整个结构会像一张真正的表格,读起来轻松得多。[[1,2],[3,4]] 和分行写法完全等价,但后者一眼就能看出"两行两列"。
扣哒世界 数组的数组示意图 - koudashijie.com
图1:二维数组 = 一个"大数组",里面每个格子又装着一个"小数组"

14.1.2 行与列的概念(Rows and Columns)

14.1.2.1 外层是行,内层是列 Outer = rows, inner = columns

理解二维数组的关键,是把它想象成一张表格:最外层数组的每个元素代表一行(row);每一行内部的数组元素则代表这一行里的各个列(column / cell)。"力量的源点"关卡里巫师给的地图正是这样一张更大的网格:

powerMap = [
    [0, 1, 0, 0, 0],   # 第 0 行
    [2, 0, 0, 3, 0],   # 第 1 行
    [0, 0, 4, 0, 0],   # 第 2 行
    [0, 0, 0, 0, 5]    # 第 3 行
]
powerMap 长成一张 4 行 5 列的表格
0
1
0
0
0
2
0
0
3
0
0
0
4
0
0
0
0
0
0
5
绿色 = 正数(力量源点)· 灰色 = 0(不好的点)
先行后列的约定:在几乎所有编程环境里,二维数组都遵循"先行后列"的顺序,即 grid[行][列]。行的索引通常用 i,列的索引通常用 j。记住这个顺序,后面读值、遍历都不会乱。

14.2 读取二维数组元素

Reading Elements from a 2D Array

14.2.1 双重方括号 [i][j](Double Brackets)

14.2.1.1 两个索引锁定一个格子 Two indices pinpoint one cell

读普通数组用一个方括号 arr[i];读二维数组要用两个方括号:第一个选出"哪一行",第二个在这一行里选出"哪一列"。也就是 grid[i][j] = 第 i 行第 j 列的值。它的运作可以拆成两步理解。

powerMap[i][j] 的两把钥匙 powerMap [ i ] [ j ] 选第几行 选第几列 先用 [i] 拿到一整行(一个数组),再用 [j] 从这行里取出具体数字

14.2.2 分步取值 vs 叠加索引(Two Ways to Access)

14.2.2.1 先取行再取列,或一步到位 Step-by-step or chained indexing

"危险谷"关卡展示了访问元素的两种写法,效果完全相同。第一种是分步取值:先用一个索引把某一行整个取出来存进变量,再用第二个索引从这行里取具体元素——这样代码更清晰,也方便复用整行。第二种是叠加索引:把两个方括号连着写,一步到位。

分步取值(清晰)
doubleArray = [[1,2], [3,4]]

row1 = doubleArray[0]
# row1 是 [1, 2]
cell1 = row1[0]
# cell1 是 1
叠加索引(简洁)
doubleArray = [[1,2], [3,4]]

cell1 = doubleArray[0][0]
# cell1 直接就是 1
为什么 doubleArray[0][0] 是 1?先算 doubleArray[0],它是第 0 行 [1, 2];再对这个结果取 [0],得到 1。两个方括号是从左到右依次生效的,就像连续开两道门。
扣哒世界 叠加索引的两步过程 - koudashijie.com
图2:doubleArray[0][0] —— 第一步取出整行,第二步取出格子

14.3 双层循环遍历

Traversing with Nested Loops

14.3.1 外循环走行,内循环走列(Outer Rows, Inner Columns)

14.3.1.1 循环套循环,扫遍每个格子 A loop inside a loop visits every cell

要把整张网格的每个格子都访问一遍,一层循环不够——必须用双层(嵌套)循环。外层循环负责"走过每一行",内层循环负责"走过当前这一行里的每一列"。关卡的思路是:外层用 range(len(powerMap)) 遍历所有行的索引 i;对每一行,内层再用 range(len(powerMap[i])) 遍历这一行里所有列的索引 j。

# 首先,循环遍历行的索引
for i in range(len(powerMap)):
    # 每一行都是一个数组,遍历它的列索引
    for j in range(len(powerMap[i])):
        # 获取 i 行 j 列的值
        pointValue = powerMap[i][j]
        # ...根据 pointValue 决定做什么...
🔁 像"逐行逐格读书"
外层循环 = 翻到第 i 行;内层循环 = 在这一行里从左到右逐个字读到底 j。读完一行,外层循环自动翻到下一行,再从头逐格读起——直到整张网格读完。这正是计算机里最经典的"行主序(row-major order)"遍历方式。

14.3.2 行主序遍历(Row-Major Order)

14.3.2.1 用 len 让代码适配任何尺寸 len() adapts to any grid size

为什么用 range(len(powerMap)) 而不是写死 range(4)?因为 len(powerMap) 会自动算出"有几行",len(powerMap[i]) 会算出"第 i 行有几列"。这样无论地图是 4×5 还是 10×10,同一段代码都能正确扫完,不用手动改数字。这是编写健壮遍历代码的关键习惯。

行主序:从 (0,0) 一路蛇形扫到最后一格 0,0 0,1 0,2 → 行结束 1,0 1,1 1,2 → 下一行 2,0 2,1 2,2 → 全部完成 ✅ 外层 i:0 → 1 → 2 | 内层 j:每行都 0 → 1 → 2

14.4 按元素值分支

Branching on Element Values

14.4.1 正数 / 1 / 0 的含义(Values as Instructions)

14.4.1.1 格子里的数字就是"命令" The number in a cell is a command

二维数组真正的威力,在于把格子里的数值当作指令来解读。遍历到每个格子时,用 if 判断它的值,不同的值触发不同的行动。这一单元的两关正好演示了两种典型约定:

🗺️ 力量的源点:正数 vs 0

0 是"不好的点",跳过不理;任何正数都是"力量源点",要移动过去念咒 "VENI" 迎接挑战。

🔥 危险谷:1 vs 0

子数组里 0 是农民(不能伤害),1 是食人魔(要布置火焰陷阱)。只在值为 1 的格子建陷阱。

用代码表达这种"看值办事"的逻辑,核心就是在双层循环最里面加一个 if:

pointValue = powerMap[i][j]

if pointValue > 0:       # 力量的源点:正数才是好点
    # ...在这里执行"念咒 + 应战"...
    pass

# 或者,危险谷的判断:
if tile is 1:          # 值为 1 表示食人魔
    # ...在这里"建造火焰陷阱"...
    pass
is 1 与 == 1:关卡默认代码里用了 if tile is 1:。在 扣哒世界 环境中它能正常工作,但在标准 Python 里判断数字是否相等更推荐写 if tile == 1:。is 比较的是"是不是同一个对象",== 比较的是"值是否相等"——对数字判断,== 才是稳妥写法。

14.5 网格坐标转换

From Grid Indices to Cartesian Coordinates

14.5.1 行列 → 笛卡尔坐标(Row/Col to x/y)

14.5.1.1 索引不是坐标,需要换算 Indices aren't positions — convert them

这是本单元最"数学"的一步,也是最容易搞混的地方。i 和 j 只是网格里的行号、列号(从 0 开始的整数),它们不是英雄要走到的真实位置。游戏世界用的是笛卡尔坐标 (x, y)(以像素为单位)。要让英雄站到某个格子上,必须先把 (行, 列) 换算成 (x, y)。

"力量的源点"贴心地准备了一个转换函数。它的逻辑是:从一个起始偏移量开始,每往右一列 x 就增加固定间距,每往下一行 y 就增加固定间距。

# 该函数把网格的 (行, 列) 转换为 x-y 坐标
def convert(row, col):
    return {'x': 16 + col * 12,
            'y': 16 + row * 12}
x = 16 + col × 12 的三个零件 16 + col × 12 起始偏移 第几列 格子间距 列号越大 → x 越靠右 行号越大 → y 越靠下
危险谷里的坐标换算:"危险谷"没有单独的 convert 函数,而是把公式直接写进了建造语句:hero.buildXY("fire-trap", 36 + 6*j, 20 + 6*i)。同样是"起始偏移 + 索引 × 间距"的套路——j 控制横向 x,i 控制纵向 y,只是偏移量和间距的具体数字不同。理解了公式结构,换任何网格都能举一反三。
别把 i、j 直接当坐标:最常见的错误就是写 hero.moveXY(i, j)。i=1, j=3 只是"第 1 行第 3 列",直接当坐标会让英雄走到地图角落的 (1, 3) 像素处。一定要先经过转换公式,得到真正的 (x, y) 再移动。

14.6 实战关卡精讲

Level Strategy & Analysis

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

第 23 关 · 力量的源点

🗺️ 力量的源点 —— 遍历地图,找到所有正数

扣哒世界 力量的源点关卡 - koudashijie.com
图3:力量的源点——只有巫师能看见的力量源点,藏在一张二维数字地图里

策略:把"看值行动"套进双层循环

本关的通关审判是打败 3 个黑暗生物,但它们藏在力量源点上,需要你先找到这些点。巫师把整张地图交给你,就是变量 powerMap——一个二维数字数组,0 是危险的错误位置,任何正数都是安全的力量源点。策略非常清晰:用双层循环把地图每个格子扫一遍,遇到正数就走过去念咒 "VENI" 召唤/迎战,然后清理出现的敌人、捡起掉落的物品。

思路拆解

算法分四步。第一,拿到地图:先 findNearest(findFriends()) 找到巫师,再读取他的 wizard.powerMap。第二,双层遍历:外层 for i in range(len(powerMap)) 走行,内层 for j in range(len(powerMap[i])) 走列,取出 powerMap[i][j]。第三,按值分支:if powerMap[i][j] > 0 才处理这个格子。第四,坐标转换 + 行动:用 convert(i, j) 把行列换成 (x, y),moveXY 过去、say("veni"),再打退出现的敌人、捡走物品。

# 关键片段:遍历地图 + 按值分支 + 坐标转换
for i in range(len(powerMap)):
    for j in range(len(powerMap[i])):
        if powerMap[i][j] > 0:          # 正数 = 力量源点
            coor = convert(i, j)          # 行列 → x,y
            hero.moveXY(coor['x'], coor['y'])
            hero.say("veni")            # 念出咒语
            # ...随后清理敌人、捡起物品...
念咒后要善后:每念一次 "VENI" 都可能召唤出敌人或物品。念咒之后加一个 while enemy and enemy.health > 0: hero.attack(enemy) 把敌人打倒,再 findNearest(findItems()) 捡起掉落物,这样才能安全推进到下一个源点。

先找点、再决策、后行动:本关的精髓是把"读地图"和"做动作"彻底分开——循环负责找到该去的坐标,if 负责决定去不去,转换函数负责翻译坐标。这种分层思路正是处理网格类问题的通用模板。
第 24 关 · 危险谷

🔥 危险谷 —— 照嵌套数组精准布雷

扣哒世界 危险谷关卡 - koudashijie.com
图4:危险谷——侦察兵把食人魔布局报成嵌套数组,你据此布置火焰陷阱伏击

策略:用侦察情报建一片地雷场

食人魔劫持了农民做人质,你要营救他们。侦察兵已经先行摸清了敌情,把整个山谷的布局报告成一个嵌套数组 hero.grid:子数组里 0 是农民(绝不能在那儿放陷阱),1 是食人魔(要在那儿建 "fire-trap")。策略是:遍历整张 grid,只在值为 1 的位置精准布置火焰陷阱,布完后退到掩体后,让冲进来的食人魔自投罗网。

思路拆解

算法三步走。第一,外层遍历行:for i in range(len(hero.grid)),并把当前行存进变量 row = hero.grid[i]——记住"被包含的数组就是一个数组",row 本身还能继续遍历。第二,内层遍历列:for j in range(len(row)),取出 tile = row[j]。第三,判断并布雷:if tile == 1 就用坐标公式 buildXY("fire-trap", 36 + 6*j, 20 + 6*i) 建陷阱。全部布完,moveXY(29, 55) 撤到安全的掩体后方。

# 关键片段:分步取行 + 内层判断 + 内联坐标公式
for i in range(len(hero.grid)):
    row = hero.grid[i]            # row 也是一个数组!
    for j in range(len(row)):
        tile = row[j]
        if tile == 1:              # 1 = 食人魔
            hero.buildXY("fire-trap", 36 + 6*j, 20 + 6*i)

hero.moveXY(29, 55)           # 最后撤到掩体后
千万别在农民(0)上建陷阱:本关最大的陷阱是"误伤人质"。if 判断必须严格锁定 tile == 1,只对食人魔布雷。少了这个判断、或写错成 0,就会把火焰陷阱建在农民脚下——救人反成害人。
分步取行让代码更好读:本关特意先写 row = hero.grid[i] 再遍历 row,而不是全程用 hero.grid[i][j]。把整行存进变量后,内层循环读起来更直观,也少写一层方括号——这是处理二维数组时值得养成的清晰写法。

14.7 常见错误

Common Mistakes

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

14.7.1.1 二维数组的典型问题 Typical issues with 2D arrays

扣哒世界 二维数组常见错误速查 - koudashijie.com
图5:行列颠倒、索引当坐标、少一层循环,是本单元最常见的失误
易错点清单:
  • 行列顺序颠倒——记住是 grid[行][列] 即 grid[i][j],把 i、j 写反会读到完全错误的格子甚至越界。
  • 把索引当坐标——直接 moveXY(i, j) 是错的;i、j 是行列号,必须先用转换公式换成真实的 (x, y)。
  • 只写一层循环——遍历二维数组必须双层嵌套;只用一层循环拿到的是"整行数组",而不是单个数字。
  • 内层循环长度写死——应写 len(powerMap[i]) 或 len(row),写死 range(5) 换张地图就会越界或漏格。
  • 忘记按值分支——扫到每个格子都要用 if 判断值(正数 / 1 / 0),无脑对所有格子行动会误伤农民或跑到坏点上。
  • 坐标公式记反——横向 x 由列 j 决定、纵向 y 由行 i 决定;把 i、j 在公式里放反会让陷阱/移动全部错位。
  • is 与 == 混用——判断数字相等在标准 Python 里请用 ==,is 只适合判断"是否同一对象"。
  • 越界访问——访问 grid[i][j] 时 i、j 不能超过各自的长度,靠 range(len(...)) 遍历天然避免越界。

本单元小结 ✨

恭喜你解锁了描述"网格世界"的核心数据结构——二维数组!这一单元你理解了它的本质:数组的数组,外层是行、内层是列。你学会了用 grid[i][j] 精确读取任意格子,用双层 for 循环按行主序扫遍整张网格,用 if 根据格子的值(正数 / 1 / 0)做出不同决策,还掌握了把行列索引换算成笛卡尔 (x, y) 坐标的公式套路。在"力量的源点"里你遍历巫师地图、找出所有力量源点应战,在"危险谷"里你照着侦察情报精准布雷、避开人质。来回顾一下核心概念:

核心概念关键点对应关卡
二维数组结构数组里每个元素又是数组两关
行与列外层是行 i,内层是列 j两关
读取元素grid[i][j] 双重方括号两关
分步 vs 叠加先取行再取列,或一步到位危险谷
双层循环遍历外循环走行、内循环走列两关
len 自适应range(len(...)) 不写死尺寸两关
按值分支正数/1 才行动,0 跳过两关
坐标转换偏移 + 索引×间距 → (x, y)力量的源点

懂得把整个世界读成一张网格、逐行逐列扫描并按值决策的英雄,才是云峰之上真正洞察全局的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🗺️🔥

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

扣哒世界教研团队