去做题

第十一单元:配对查找 · 用嵌套循环找出成双成对

Unit 11: Pair Finding — Nested Loops for Matching
计算机科学 5 · 安息云峰 Cloudrip Mountain · 第 17–18 关:无暇的宝石对 → 孪生力量

11.1 什么是配对查找

What Is Pair Finding

墓地里散落着许多宝石,你必须找齐四对价值相同的宝石才能逃离;月亮神殿与太阳神殿前,你要找出四对双胞胎圣骑士让他们成对祈祷。这两个任务有一个共同的核心:不再是"从数组里挑出符合条件的单个元素",而是"从数组里找出两个互相匹配的元素"。要判断"两个元素是否配对",就得把它们两两放在一起比较——这需要一种全新的循环结构:嵌套循环(nested loops)。本单元就来学习用嵌套循环做配对查找。

本单元主线:配对查找要遍历数组里所有可能的两两组合,用外层 for i 套内层 for j 实现;用 if i == j: continue 跳过元素和自己比较;再判断这一对是否匹配——宝石看 value 是否相等,双胞胎看名字除最后一位外是否逐位相同;找到就返回这一对或说出它们的名字。

11.1.1 从"单个"到"成对"(From Single to Pair)

11.1.1.1 一个循环不够,要两个 One loop isn't enough for pairs

之前的过滤只需一个循环,因为"要不要这个元素"只看它自己。但配对不同——判断元素 A 要不要,得看它能不能和 B、C、D……中的某一个配上。也就是说,对每一个元素,都要把其余元素再看一遍。关卡概览说得很到位:"每次迭代所有元素时,都需要对每个元素进行迭代!"这句话正是嵌套循环的精髓。

扣哒世界 配对查找示意 - koudashijie.com
图1:配对查找——对每一个元素,都要和其余元素逐一比较,找出互相匹配的那一对

11.1.2 组合的数量(How Many Pairs)

11.1.2.1 n 个元素有多少种配对 Counting the combinations

如果数组有 n 个元素,用双重循环全部两两比较,会检查 n × n 次;去掉"自己和自己"这 n 次,剩 n × (n − 1) 次有序对;如果不区分"A 配 B"和"B 配 A",真正不同的组合只有 n × (n − 1) / 2 对。理解这个数量,能帮你明白为什么后面要用 continue 和 i + 1 来省去重复。

🔢 4 个元素能配几对?
全部有序组合:4 × 4 = 16 种;
去掉自己配自己(4 种):剩 12 种有序对;
不分先后的真正配对:12 ÷ 2 = 6 对。
这就是为什么"跳过自比较"和"内层从 i+1 开始"能大幅减少无用比较。

11.2 嵌套循环

Nested Loops

11.2.1 for i 套 for j(A Loop Inside a Loop)

11.2.1.1 外层选第一个,内层选第二个 Outer picks the first, inner picks the second

嵌套循环就是"循环里面再放一个循环"。外层 for i 每固定一个元素 items[i],内层 for j 就把整个数组从头到尾走一遍,让 items[i] 依次和每个 items[j] 相遇。这样,所有两两组合都不会被遗漏。

# 外层固定第一个,内层遍历第二个
for i in range(len(items)):
    itemI = items[i]
    for j in range(len(items)):
        itemJ = items[j]
        # 这里 itemI 和 itemJ 就是一对

11.2.2 遍历所有组合(Visiting Every Combination)

11.2.2.1 内层每转完一圈,外层才走一步 The inner loop finishes before the outer advances

执行顺序要理解透:内层循环要完整跑完一整圈,外层才前进一步。当 i=0 时,j 从头走到尾;然后 i=1,j 又从头走到尾……如同时钟的分针转一整圈,时针才动一格。下面这张矩阵展示了 4 个元素时,双重循环会踏过哪些 (i, j) 组合。

i \ j0123
0跳过(0,1)(0,2)(0,3)
1(1,0)跳过(1,2)(1,3)
2(2,0)(2,1)跳过(2,3)
3(3,0)(3,1)(3,2)跳过
对角线是"自己配自己":表格对角线上的 (0,0)、(1,1)…就是 i == j 的情形,必须跳过。其余格子才是有意义的配对——注意 (0,1) 和 (1,0) 其实是同一对宝石,说明还有优化空间(见 11.3.2)。

11.3 跳过自比较

Skipping Self-Comparison

11.3.1 if i==j: continue(The continue Guard)

11.3.1.1 一个元素不能和自己配对 An element can't pair with itself

当 i == j 时,items[i] 和 items[j] 是同一个元素。它的 value 当然等于它自己,若不排除,程序会误以为"找到一对",实际却只有一颗宝石——在关卡里这会让你犯错、晕倒!解决办法是 if i == j: continue:continue 会跳过本轮内层循环的剩余代码,直接进入下一个 j,从而绕开自我比较。

for i in range(len(items)):
    for j in range(len(items)):
        if i == j:
            continue          # 同一个元素,跳过
        # 到这里 i、j 一定不同
        ...
continue 与 break 的区别:continue 是"跳过这一轮,继续下一轮";break 是"直接结束整个循环"。这里用 continue——我们只想跳过 i==j 这一种情况,其余组合还要继续检查,绝不能 break 把循环整个停掉。

11.3.2 从 i+1 起省一半(Start Inner at i+1)

11.3.2.1 让内层从 i+1 开始,避免重复 Start j at i+1 to avoid duplicates

关卡概览给出了一个更优雅的写法:让内层 for j in range(i + 1, len(array)) 从 i + 1 开始。这样 j 永远大于 i,天然就不会有 i == j,也不会重复检查已经比过的对(比如比了 (0,1) 就不会再比 (1,0))。两种写法都对:i==j: continue 直观易懂,i+1 起更高效。

写法一:continue 跳过
for j in range(len(arr)):
    if i == j:
        continue
    ...
写法二:从 i+1 起
for j in range(i + 1, len(arr)):
    # j 恒大于 i,无重复
    ...

11.4 值匹配与返回

Value Matching & Returning

11.4.1 比较 value 相等(Compare the Values)

11.4.1.1 两颗宝石 value 相同就是一对 Equal values make a pair

跳过自比较后,就用 == 判断这两个不同元素是否匹配。宝石对看价值:if items[i].value == items[j].value 成立,就是一对同值宝石。这是最直接的"值匹配算法"。

11.4.2 返回配对结果(Return the Pair)

11.4.2.1 找到就返回,找不到返回 None Return the pair, or None if none found

一旦找到匹配的一对,就用 return [itemI, itemJ] 把这两个元素打包成数组返回——return 会立刻结束函数,不再检查后面的组合。如果双重循环全部跑完都没找到,说明没有配对,函数最后 return None。调用处再用 if gemPair: 判断有没有拿到结果。

# 值匹配 + 返回配对
def findValuePair(items):
    for i in range(len(items)):
        for j in range(len(items)):
            if i == j:
                continue
            if items[i].value == items[j].value:
                return [items[i], items[j]]  # 找到!
    return None                              # 没有配对
为什么用 return 打包成数组?函数一次要交回两个结果,把它们放进一个数组 [itemI, itemJ] 一并返回最方便。调用处 gemPair = findValuePair(gems) 拿到后,用 gemPair[0]、gemPair[1] 取出这一对,分别去移动收集。

11.5 字符串逐位比较

Comparing Strings Character by Character

11.5.1 除最后一位相同(Same Except the Last Letter)

11.5.1.1 双胞胎名字只差最后一个字母 Twins differ only in the final letter

"孪生力量"里的匹配规则更细致:双胞胎的名字长度相同、且除最后一个字母外每一位都一样(比如 "Reynaldo" 和 "Reynalda")。判断逻辑分两步:先比长度,长度不同直接不是双胞胎;再逐位比较到倒数第二位——用 for i in range(len(name1) - 1) 只遍历到 len - 1(正好跳过最后一位),任何一位不同就 return False;全都相同则 return True。这段 areTwins 函数关卡已经给好,你直接用即可。

# 判断两名字是否只差最后一位(关卡已给)
def areTwins(unit1, unit2):
    name1 = unit1.id
    name2 = unit2.id
    if len(name1) != len(name2):
        return False                 # 长度不同,直接排除
    for i in range(len(name1) - 1):  # 只比到倒数第二位
        if name1[i] != name2[i]:
            return False             # 有一位不同
    return True                      # 前面全同 → 是双胞胎
只比到倒数第二位,最后一位可以不同 R e y n a l d o R e y n a l d a 前 7 位逐位相同 ✓ 最后一位不同,无所谓
为什么是 len - 1?因为下标从 0 到 len - 1 覆盖全部字符,而 range(len - 1) 产生 0 到 len - 2——恰好不包含最后一位的下标。这正好实现"除最后一位外全部比较"的需求。这和第八单元子串匹配里"逐字符比较"的思路一脉相承。

11.6 实战关卡精讲

Level Strategy & Analysis

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

第 17 关 · 无暇的宝石对

💎 无暇的宝石对 —— 找出四对同值宝石

扣哒世界 无暇的宝石对关卡 - koudashijie.com
图2:无暇的宝石对——找出四对价值相同的宝石,每收集一对就回起点向巫师获取加速

关卡背景与目标

要逃离墓地,你必须收集4 对宝石,每对由两颗价值相同的宝石组成。若配错就会晕倒!好在巫师会用 "Haste"(加速)法术帮你——每收集完宝石返回起点就能获得加速。所以流程是:找出一对同值宝石 → 去捡第一颗 → 回起点加速 → 去捡第二颗 → 回起点加速。

策略:先写"找一对"的函数,再循环执行动作

关卡把任务拆成了两块。第一块是算法:补全 findValuePair(items) 函数,用双重循环 + 跳过自比较 + 值匹配,返回一对同值宝石。第二块是动作:在 while True 里每帧调用它,若拿到 gemPair,就按"移动到 gemA → 回起点 (40,44) → 移动到 gemB → 回起点"的顺序把这一对收入囊中,回起点是为了触发巫师的加速。

思路拆解

findValuePair 的算法就是 11.4 讲的模板:外层 for i、内层 for j 遍历所有组合,if i == j: continue 跳过同一颗宝石,再 if items[i].value == items[j].value 判断价值是否相等,相等就 return [items[i], items[j]],全部没找到则 return None。动作部分则用 gemPair[0]、gemPair[1] 取出两颗宝石的坐标去 moveXY,中间穿插回起点 moveXY(40, 44)。

# 关键片段:收集一对宝石的动作序列
while True:
    gems = hero.findItems()
    gemPair = findValuePair(gems)
    if gemPair:
        gemA = gemPair[0]
        gemB = gemPair[1]
        hero.moveXY(gemA.pos.x, gemA.pos.y)  # 捡第一颗
        hero.moveXY(40, 44)              # 回起点加速
        hero.moveXY(gemB.pos.x, gemB.pos.y)  # 捡第二颗
        hero.moveXY(40, 44)              # 再回起点加速
i == j 必须跳过,否则会晕倒:如果漏了 if i == j: continue,当 i 和 j 指向同一颗宝石时,它的 value 必然等于自己,函数会误报"找到一对",可实际只有一颗。按错误结果行动就会犯错晕倒。这一句是本关的安全阀。
回起点不是多余动作:每收集一颗就 moveXY(40, 44) 回起点,是为了从巫师那里获得 "Haste" 加速——有了加速才能足够快地跑完全程收齐四对。这提醒我们:算法找出目标后,行动的顺序和节奏同样是通关策略的一部分。
第 18 关 · 孪生力量

🌗 孪生力量 —— 呼唤四对双胞胎

扣哒世界 孪生力量关卡 - koudashijie.com
图3:孪生力量——找出四对名字只差最后一字母的双胞胎圣骑士,成对呼唤唤醒日月之力

关卡背景与目标

你要引诱一只近视的巨型食人魔经过月亮神殿和太阳神殿,趁机布下陷阱。启动神殿之力的钥匙是四对双胞胎圣骑士——他们名字相同、只有最后一个字母不同。你需要找出每一对双胞胎,并把两人的名字用一个空格隔开一起说出来:hero.say(twin1.id + " " + twin2.id),唤醒日月的力量。

策略:复用配对模板,换掉匹配条件

这一关的骨架和"无暇的宝石对"一模一样——还是双重循环遍历所有两两组合、跳过自比较。唯一的变化是"匹配条件":宝石看 value == value,双胞胎则调用关卡已给好的 areTwins(unit1, unit2)(内部做名字逐位比较)。策略就是:拿到 friends = hero.findFriends(),用嵌套循环两两配对,if f1 != f2 and areTwins(...) 成立就说出两人名字。

思路拆解

外层 for f1 in range(len(friends))、内层 for f2 in range(len(friends)) 遍历所有圣骑士组合;用 if f1 != f2 and areTwins(friends[f1], friends[f2]) 一次性完成"跳过自比较"和"双胞胎判断"两件事——注意这里把 f1 != f2 和 areTwins 用 and 连起来,只有既不是同一人、又确实是双胞胎,才 hero.say(friends[f1].id + " " + friends[f2].id)。呼唤完四对后,再前后移动(如 moveXY(64, 36) 与 moveXY(14, 36))来回引诱食人魔踏入陷阱。

# 关键片段:嵌套循环 + areTwins 找双胞胎
friends = hero.findFriends()
for f1 in range(len(friends)):
    for f2 in range(len(friends)):
        if f1 != f2 and areTwins(friends[f1], friends[f2]):
            hero.say(friends[f1].id + " " + friends[f2].id)
把两个条件用 and 合并:if f1 != f2 and areTwins(...) 是一种简洁写法:f1 != f2 排除自比较,areTwins(...) 判断是不是双胞胎,二者都为真才行动。它等价于先 if f1 == f2: continue 再判断,但一行搞定,更紧凑。
复用同一套算法框架:本关最值得体会的是——配对查找的框架是通用的,变的只是"怎样才算匹配"。宝石关比 value,双胞胎关比名字。学会把"遍历所有组合"和"匹配条件"分开思考,你就能用同一个模板解决无数配对问题。这正是抽象思维在编程里的体现。
别忘了引诱食人魔:找齐双胞胎、唤醒神殿力量后,还要靠英雄来回移动把近视的食人魔引到神殿光束之下。光束只对食人魔危险,英雄不必害怕。算法找对了人,最后还得靠走位完成诱敌这一步。

11.7 常见错误

Common Mistakes

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

11.7.1.1 配对查找的典型问题 Typical issues in pair finding

扣哒世界 配对查找常见错误速查 - koudashijie.com
图4:忘跳过自比较、把 continue 写成 break、逐位比较越界,是本单元最常见的失误
易错点清单:
  • 忘跳过自比较——漏了 if i == j: continue,元素和自己配成"假对",导致配错晕倒。
  • 把 continue 写成 break——break 会整个终止循环,只应"跳过这一轮"时误用会漏掉大量组合。
  • 内层循环变量写错——内层应用 j(或 f2)而非又用 i,否则根本没在遍历第二个元素。
  • 逐位比较越界——双胞胎比较应到 len - 1;若写成 len 会比到最后一位,把本该不同的末字母也算进去。
  • 返回值忘打包——一对结果要 return [a, b] 打包成数组,只返回一个元素调用处取不到另一半。
  • 没处理 None——函数可能返回 None,调用处要 if gemPair: 判断,否则对 None 取下标会报错。
  • and 条件顺序或逻辑错——应是 f1 != f2 and areTwins(...),别漏掉排除自比较的那一半。
  • 说名字格式错——双胞胎名字间要有空格:id + " " + id,漏了空格无法正确唤醒。

本单元小结 ✨

恭喜你逃出墓地、唤醒了日月之力!这一单元你掌握了处理"成对关系"的利器——配对查找与嵌套循环。你理解了"对每个元素都要再遍历所有元素"的核心思想,学会了用外层 for i 套内层 for j 走遍所有两两组合、用 if i == j: continue(或内层从 i+1 起)跳过自比较;你既会用 value == value 做值匹配,也会用逐位比较到 len - 1 判断"除最后一位外相同"的双胞胎名字;还学会了把配对结果 return [a, b] 打包返回、用 if 结果: 判断成败。最重要的是,你体会到"配对框架不变、匹配条件可换"的抽象思维——同一套嵌套循环,既能配宝石又能配双胞胎。来回顾一下核心概念:

核心概念关键点对应关卡
配对查找找出两个互相匹配的元素两关
嵌套循环外层 for i 套内层 for j两关
遍历所有组合内层跑完一圈外层才前进两关
跳过自比较if i == j: continue两关
省一半组合内层从 i + 1 起概览进阶
值匹配value == value无暇的宝石对
返回配对return [a, b] / None无暇的宝石对
字符串逐位比较比到 len - 1,末位不计孪生力量
and 合并条件f1 != f2 and areTwins(...)孪生力量
框架复用框架不变,换匹配条件两关

懂得用嵌套循环遍历万千组合、又能精准跳过无效比较、找出天生一对的英雄,
才是云峰之上真正洞察关联的程序员。继续向雪山之巅挺进吧,勇敢的程序员!🏔️💎🌗

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

扣哒世界教研团队