01 / 本课学习路线
本课学习路线
阅读与推演约 128 分钟,练习约 60 分钟,进阶练习另需约 50 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
本课两种递归只差一行:标记要不要撤销。先确认标记的是「事实」(这格属于哪一块)还是「尝试」(这条路径上暂时占用)——事实不撤销,尝试必须撤销。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 说出连通块标记与回溯标记的语义差别(事实 vs 尝试) | 第 03 节 | 自查第 2 条 |
| 按正确顺序写出递归边界:越界 → 空地 → 已访问,通过(AC)P3501 | 第 04、08 节 | 自查第 3 条、必做任务 1 |
| 说出回溯三步(选择、递归、撤销)各在代码哪一行,通过 P3813 | 第 05、08 节 | 自查第 4 条、必做任务 2 |
| 区分不会排除合法方案的安全剪枝,与错误限制搜索范围的条件 | 第 05、07 节 | 自查第 5 条、练习 3 |
| 一次 DFS 同时聚合两个量;用回溯枚举排列并按数据范围判断可行 | 第 06、07 节 | 进阶练习 1、2 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成上一课「广度优先搜索:网格与多源扩散」。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | 写一个递归函数 total(n) 返回 1 + 2 + … + n;哪一行是递归边界? | 函数与递归 |
| 自测 2 | 内层函数要修改外层函数的变量 count,需要加什么声明?修改模块级变量呢? | 作用域 |
| 自测 3 | 无向边 (1, 2)、(2, 3) 建成邻接表 adj,adj[2] 是什么? | 列表与上一课第 03 节 |
| 自测 4 | 每行一串数字字符(如 22220),怎样读到文件尾并按字符访问 grid[r][c]? | 标准输入输出与首次独立提交 |
| 自测 5 | sys.setrecursionlimit(10**6) 放在哪里?为什么 300×300 的地图可能需要它? | 上一课第 03 节补充「BFS、DFS 与递归深度」 |
展开先修自测答案
自测 1:def total(n): return 0 if n == 0 else n + total(n - 1);n == 0 那一句是递归边界,必须放在递归调用之前。
自测 2:内层改外层用 nonlocal count;改模块级变量用 global count。本课参考程序两种都出现。
自测 3:adj[2] == [1, 3]——无向边两个方向都要加。
自测 4:grid = [line.strip() for line in sys.stdin.read().split("\n") if line.strip()],字符串本身可以 grid[r][c] 取字符,价值用 int(grid[r][c])。
自测 5:放在程序开头、任何递归调用之前。300×300 全是矿且连成一条时递归深度可达 9 万层,默认上限约 1000 层会抛 RecursionError。
03 / 概念与术语
连通块、递归边界、回溯三步、剪枝
同一个递归,两种用法:连通块 DFS 求「这一块一共多少」,回溯求「一共有多少种方案」。
| 术语 | 含义 | 代码写法 |
|---|---|---|
| 连通块 | 四方向相邻的同类格子连成的一块 | 从一格 DFS,走到哪算到哪 |
| 递归边界 | 不再往下递归、直接返回的条件 | if 越界 or 空地 or 已访问: return 0 |
| 事实标记 | 「这格属于哪一块已经定了」——标记不回退 | seen[r][c] = True,之后不改 |
| 尝试标记 | 「当前这条尝试路径上占用了它」——路径放弃后释放 | visited[x] = True … visited[x] = False |
| 回溯三步 | 选择 → 递归 → 撤销 | color[u] = 1; paint(u + 1); color[u] = 0 |
| 剪枝 | 在递归前排除「已经违反约束」或「不可能更优」的分支 | if all(color[v] == 0 for v in adj[u]) / if total >= best: return |
| 递归树 | 每层对应一个决策点,每个分支对应一种选择 | 第 05 节画出链状图的整棵树 |
| 题 | 问什么 | 标记类型 | 聚合 / 枚举 |
|---|---|---|---|
| P3501 矿堆 | 最大的一块价值多少 | 事实(不撤销) | 聚合:价值之和 |
| P3517 单入口区域 | 最大的单入口块及其入口 | 事实(不撤销) | 聚合:面积 + 边界格数 |
| P3813 无向图染色 | 有多少种染法 | 尝试(撤销) | 枚举:方案数 |
| P3801 基站维修 | 最短的访问顺序 | 尝试(撤销) | 枚举:排列取最小 |
Python 递归上限约 1000 层
100×100 地图全是矿、蛇形连成一条时递归深度上万,不调高递归上限(sys.setrecursionlimit)会抛出递归深度超限错误(RecursionError)。开头加 sys.setrecursionlimit(10**6),或改显式栈迭代——本课模板里已经带了。
补充学习(选学)为什么连通块不撤销、回溯要撤销约 5 分钟两种 visited 的语义差别,一句话判据
连通块 DFS 的 visited 语义是「这格属于哪一块已经定了」——事实不会变,标记就不回退;回溯的标记语义是「当前这条尝试路径上占用了它」——路径放弃后占用就该释放。一句话判据:先确认标记的是「事实」还是「尝试」——事实不撤销,尝试必须撤销。
对应到题型:数块数、算块内聚合(价值、面积、入口数)是事实类;数方案数、找一条可行路径、枚举排列是尝试类。P3801 的送修路线枚举也是尝试类——每层选一个未访问基站,递归返回后把它标回未访问。
04 / 连通块 DFS:P3501
从每个未访问的矿格出发,把整块价值累加
P3501「寻找最大价值的矿堆」:若干行数字字符(0 空地、1 银矿值 1、2 金矿值 2,行数不给),矿堆由上下左右相邻的矿格连成;输出价值最大的矿堆的价值。题面示例:22220 / 00000 / 00000 / 01111 → 8。
| 起点 | DFS 走过的格子 | 价值 | 全局最大 |
|---|---|---|---|
| (0,0) | (0,0) (0,1) (0,2) (0,3) | 2 + 2 + 2 + 2 = 8 | 8 |
| (3,1) | (3,1) (3,2) (3,3) (3,4) | 1 + 1 + 1 + 1 = 4 | 8 |
外层双重循环扫到 (0,1)、(0,2)、(0,3) 时它们已被标记,不会再启动新的 DFS——每格只算一次。输出 8。
一次 DFS 的递归过程:从 (0,0) 出发(返回值 = 本格价值 + 四方向递归之和)
dfs(0,0): 标记,值 2;向下 (1,0)=0 → 0;向上越界 → 0;向右 dfs(0,1)
dfs(0,1): 标记,值 2;向下 0;向上越界;向右 dfs(0,2)
dfs(0,2): 标记,值 2;… 向右 dfs(0,3)
dfs(0,3): 标记,值 2;向右 (0,4)=0 → 0;向左 (0,2) 已访问 → 0;返回 2
返回 2 + 2 = 4
返回 2 + 4 = 6
返回 2 + 6 = 8| 顺序 | 判断 | 先后颠倒会怎样 |
|---|---|---|
| ① | 越界 → 返回 0 | 先读 grid[r][c] 再判越界:r = rows 时抛出 IndexError(负下标则悄悄读到另一行) |
| ② | 空地 '0' → 返回 0 | — |
| ③ | 已访问 → 返回 0 | 漏掉:同一块被反复进入,递归不会停 |
连通块深度优先搜索练习模板
Pythonimport sys
sys.setrecursionlimit(10**6)
def main() -> None:
grid = [list(line.strip()) for line in sys.stdin if line.strip()]
rows, cols = len(grid), len(grid[0])
seen = [[False] * cols for _ in range(rows)]
def dfs(r: int, c: int) -> int:
# 待完成 1:递归边界(越界、空地、已访问),越界必须最先判断
...
seen[r][c] = True
# 待完成 2:本格价值(银 1 金 2)加上四方向递归聚合
...
best = 0
for r in range(rows):
for c in range(cols):
# 待完成 3:什么条件下才从 (r, c) 开始一次新的 DFS
...
print(best)
main()连通块 DFS 每格访问一次,时间 O(rows×cols)。回溯染色最坏 O(2^节点数),靠冲突剪枝把实际分支压小——写完要能口述最坏规模,对照题目约束判断可不可行。补看:岛屿数量动画。
05 / 回溯枚举:P3813
按题目页规则:每点染黑或红,只有红色不能相邻
P3813「无向图染色」:第一行 M N(M 个节点 1..M,N 条边,0 < M < 15),随后 N 行每行一条边;每个节点染黑或红,红色节点不能相邻(黑黑可以);输出染色方案总数。
规则以题目页为准:不是「相邻必须不同色」
本题容易被误读成「k 种颜色、相邻不同色」。题目页的规则只禁止红—红相邻:链 1—2—3 有 5 种方案(黑黑黑、红黑黑、黑红黑、黑黑红、红黑红),按「相邻不同色」只会数出 2 种。第 09 节错误表给出了这一差异。
| 节点 1 | 节点 2 | 节点 3 | 结果 |
|---|---|---|---|
| 黑 | 黑 | 黑 | ✓ 方案 1 |
| 黑 | 黑 | 红(邻居 2 是黑) | ✓ 方案 2 |
| 黑 | 红(邻居 1 是黑) | 黑 | ✓ 方案 3 |
| 黑 | 红 | 红?邻居 2 是红 → 剪掉 | ✗ |
| 红 | 黑 | 黑 | ✓ 方案 4 |
| 红 | 黑 | 红(邻居 2 是黑) | ✓ 方案 5 |
| 红 | 红?邻居 1 是红 → 剪掉 | — | ✗ |
共 5 种。每个节点两个分支:染黑无需检查;染红前看邻接表里已染色的邻居有没有红。节点 3 的邻居只有 2,所以「1 红、3 红」是允许的(方案 5)。
撤销为什么必要:漏掉 color[u] = 0 会怎样
正确: 每个节点试完「红」分支后恢复为黑,上层再换下一种选择 漏撤销(链 1—2—3,记 0 黑 1 红): 前两个叶子 000、001 正常;节点 3 试完红后没有恢复,一直残留着红 节点 2 想染红: 检查邻居 3 是红 → 被剪掉,方案「黑红黑」丢失 节点 1 染红后: 节点 2 只能黑;节点 3 的「黑」分支其实还带着残留的红 → 叶子 101;节点 3 再染红 → 又是 101 结果: 数出 4 种,但四个叶子是 000、001、101、101——丢了「黑红黑」,「红黑黑」被残留的红记成了「红黑红」;正确是 5 种
复杂度:每个节点 2 种选择,最坏 2^M 个叶子、树高 M,O(M·2^M);M < 15 时约 5×10⁵,不会超时。剪枝「邻居已有红就不染红」只排除违反约束的分支,是安全的;「颜色编号必须递增」之类的条件会剪掉合法方案——那是错误地限制了搜索范围,不是剪枝。
06 / 一次 DFS 聚合两个量:P3517
面积与边界格数一起数,边界格恰为 1 的块才是单入口
P3517「查找单入口区域」:第一行 m n,之后 m 行每行 n 个 X / O(空格分隔);空闲区域由连通的 O 组成,位于边界的 O 是入口;找有且只有一个入口的最大区域:唯一时输出「入口行 入口列 面积」,多个同样大时只输出面积,没有输出 NULL。
| 块 | 格子 | 面积 | 位于边界的格子 | 单入口? |
|---|---|---|---|---|
| 1 | (1,1) (1,2) (2,1) (2,2) (3,2) | 5 | (3,2)(最后一行) | 是,入口 (3,2) |
输出 3 2 5。只从边界上的 O 出发搜索即可:不含任何边界格的块不可能是单入口区域,也不会被其它块的搜索碰到(它们不连通)。
| 输入 | 块与入口 | 输出 |
|---|---|---|
| 3 3 / X O X / X O X / X X X | 块 {(0,1),(1,1)} 面积 2,边界格只有 (0,1) | 0 1 2 |
| 3 3 / O X O / X X X / X X X | 两块各 1 格、各 1 个入口,面积相同 | 1(只输出面积) |
| 2 2 / X X / X X | 没有 O | NULL |
角落格同时在两条边上,但它只是一个格子——入口数按格数,不按方向数;否则「O X X / X X X / X X X」会被误判为两个入口。
07 / 排列回溯与剪枝:P3801
枚举访问顺序,走完加回程,路程不小于最优就剪
P3801「基站维修工程师」:第一行 n(1 < n < 10),之后 n 行 n 列距离矩阵(dist),dist[x][y] 不一定等于 dist[y][x];从 0 出发每个基站恰好一次再回到 0,输出最短总路程。
| 路线 | 路程 | 说明 |
|---|---|---|
| 0→1→2→3→0 | 2 + 4 + 2 + 5 = 13 | 最短 |
| 0→1→3→2→0 | 2 + 4 + 2 + 6 = 14 | |
| 0→2→1→3→0 | 6 + 4 + 4 + 5 = 19 | |
| 0→2→3→1→0 | 6 + 2 + 4 + 2 = 14 | |
| 0→3→1→2→0 | 5 + 4 + 4 + 6 = 19 | |
| 0→3→2→1→0 | 5 + 2 + 4 + 2 = 13 | 与第一条并列 |
输出 13。剩余 n − 1 = 3 个基站的全排列 3! = 6 条;n < 10 时最多 8! = 40320 条,回溯足够。剪枝:当前路程已 ≥ 最优就返回——它只丢掉不可能更短的分支。
不对称矩阵:0 1 9 / 9 0 1 / 1 9 0
0→1→2→0: 1 + 1 + 1 = 3 ← 顺着箭头走很便宜 0→2→1→0: 9 + 9 + 9 = 27 输出 3;若把 dist[x][y] 与 dist[y][x] 当成一样(用 dist[y][x] 累加),会得到 11
08 / 从步骤到程序
四道题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。
| 题 | 步骤 | 代码 | 说明 |
|---|---|---|---|
| P3501 | 越界 → 空地 → 已访问;标记;本格 + 四方向 | if r < 0 or …: return 0;seen[r][c] = True;total += dfs(...) | 外层只从未访问矿格启动 |
| P3813 | 染黑直接递归;染红先查邻居;返回后撤销 | paint(u + 1);if all(color[v] == 0 …);color[u] = 0 | global count 计数 |
| P3517 | 从边界 O 出发;返回 (面积, 边界格数) | area, doors = dfs(...);if doors == 1 | 字典按面积收集入口 |
| P3801 | 选择、递归、撤销;全访问后加回程;剪枝 | visited[nxt] = True … False;total + dist[cur][0];if total >= best: return | 距离按 dist[cur][nxt] 方向 |
展开完整参考程序 1:P3501 寻找最大价值的矿堆(先自己写完并提交一次,再展开对照)
完整程序:P3501(标准输入 → 标准输出)
Pythonimport sys
sys.setrecursionlimit(10**6)
grid = [line.strip() for line in sys.stdin.read().split("\n") if line.strip()] # 每行一串数字字符
rows, cols = len(grid), len(grid[0])
seen = [[False] * cols for _ in range(rows)]
def dfs(r, c): # 返回 (r, c) 所在矿堆的价值;标记不回退
if r < 0 or r >= rows or c < 0 or c >= cols: # 越界最先判
return 0
if grid[r][c] == "0" or seen[r][c]: # 空地 / 已访问
return 0
seen[r][c] = True
total = int(grid[r][c]) # 银 1、金 2
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
total += dfs(r + dr, c + dc)
return total
best = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] != "0" and not seen[r][c]: # 只从未访问的矿格开始一次新搜索
best = max(best, dfs(r, c))
print(best)自测建议:题面示例(输出 8)、全空地(输出 0)、第 10 节练习 2 的 121 / 212(输出 9)。输入按题目页参考题解读到空行或文件尾。
展开完整参考程序 2:P3813 无向图染色
完整程序:P3813(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
m, e = int(data[0]), int(data[1]) # 节点 1..m,e 条边
adj = [[] for _ in range(m + 1)]
for i in range(e):
a, b = int(data[2 + 2 * i]), int(data[3 + 2 * i])
adj[a].append(b)
adj[b].append(a)
color = [0] * (m + 1) # 0 黑、1 红;黑黑可以相邻,红红不能
count = 0
def paint(u): # 决定节点 u 的颜色
global count
if u == m + 1: # 所有节点都染完 → 一种方案
count += 1
return
paint(u + 1) # 选择 1:染黑,不需要检查
if all(color[v] == 0 for v in adj[u]): # 选择 2:染红——已染色的邻居里不能有红
color[u] = 1
paint(u + 1)
color[u] = 0 # 撤销,换下一种选择
paint(1)
print(count)自测建议:链 1—2—3 → 5、三角形 → 4、无边 3 点 → 8、四点环 → 7。
展开完整参考程序 3:P3517 查找单入口区域(进阶)
完整程序:P3517(标准输入 → 标准输出)
Pythonimport sys
sys.setrecursionlimit(10**6)
data = sys.stdin.read().split()
m, n = int(data[0]), int(data[1])
cells = data[2:2 + m * n]
grid = [cells[i * n:(i + 1) * n] for i in range(m)]
seen = [[False] * n for _ in range(m)]
def on_border(r, c):
return r == 0 or r == m - 1 or c == 0 or c == n - 1
def dfs(r, c): # 返回 (面积, 边界格数)
seen[r][c] = True
area, doors = 1, (1 if on_border(r, c) else 0)
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == "O" and not seen[nr][nc]:
a, d = dfs(nr, nc)
area += a
doors += d
return area, doors
found = {} # 面积 → 入口坐标列表(只记单入口区域)
for r in range(m):
for c in range(n):
if on_border(r, c) and grid[r][c] == "O" and not seen[r][c]: # 只从边界上的 O 出发
area, doors = dfs(r, c)
if doors == 1: # 整块只有这一个边界格
found.setdefault(area, []).append((r, c))
if not found:
print("NULL")
else:
biggest = max(found)
if len(found[biggest]) == 1:
r, c = found[biggest][0]
print(r, c, biggest)
else:
print(biggest) # 多个同样大的单入口区域:只输出面积自测建议:第 06 节的三种输出形式(3 2 5、1、NULL)与第 10 节练习 5。
展开完整参考程序 4:P3801 基站维修工程师(进阶)
完整程序:P3801(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
n = int(data[0])
vals = [int(x) for x in data[1:1 + n * n]]
dist = [vals[i * n:(i + 1) * n] for i in range(n)] # dist[x][y] 不一定等于 dist[y][x]
visited = [False] * n
visited[0] = True
best = float("inf")
def dfs(cur, count, total): # 当前在 cur,已访问 count 个基站,路程 total
global best
if total >= best: # 已经不比当前最优短,再走也没用
return
if count == n: # 全部访问过 → 回到 0
best = min(best, total + dist[cur][0])
return
for nxt in range(1, n):
if not visited[nxt]:
visited[nxt] = True # 选择
dfs(nxt, count + 1, total + dist[cur][nxt]) # 递归
visited[nxt] = False # 撤销
dfs(0, 1, 0)
print(best)自测建议:第 07 节两组手算(13、3)。
09 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| P3501 先读格子再判越界 | 题面示例 | 抛出 IndexError | 8 | 运行错误(RE) |
| P3501 没有已访问标记 | 任何含矿的地图 | 相邻矿格互相递归不停 | — | 运行错误(RE) |
| P3501 深地图不调递归上限 | 300×300 蛇形矿 | RecursionError | 正确价值 | 运行错误(RE) |
| P3813 漏掉撤销 color[u] = 0 | 3 2 / 1 2 / 2 3 | 4 | 5 | 答案错误(WA) |
| P3813 按「相邻必须不同色」写 | 3 2 / 1 2 / 2 3 | 2 | 5 | 答案错误(WA) |
| P3517 角落格按方向数入口(算 2 个) | 3 3 / O X X / X X X / X X X | NULL | 0 0 1 | 答案错误(WA) |
| P3801 按对称距离累加(用 dist[nxt][cur]) | 3 / 0 1 9 / 9 0 1 / 1 9 0 | 11 | 3 | 答案错误(WA) |
| P3801 忘记回程 | 第 07 节 4 基站 | 8 | 13 | 答案错误(WA) |
| P3801 剪枝写成 total > best 以外的条件(如按访问顺序编号递增) | 任意 | 漏掉合法路线 | — | 答案错误(WA) |
第四行的 4:节点 3 试完红没有恢复,节点 2 的「红」分支被误剪,节点 1 的分支里又多出一个重复叶子(第 05 节)。第五行的 2:「相邻不同色」只允许黑红黑与红黑红。
| 做法 | 时间 | 本课的规模 |
|---|---|---|
| 连通块 DFS | O(rows×cols) | 300×300 = 9×10⁴ 格 |
| 染色回溯 | O(M·2^M) | M < 15 → 约 5×10⁵ |
| 排列回溯 | O((n−1)!) | n < 10 → 至多 8! = 40320 条路线 |
| 排列回溯无剪枝、n = 15 | 14! ≈ 8.7×10¹⁰ | 超时——本题 n < 10 才可行 |
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 05 节表的格式,画出三角形 1—2、2—3、1—3 的递归树,数出方案数;再数四点环 1—2—3—4—1。
展开练习 1 答案
三角形:任意两点相邻,红最多一个 → 全黑 1 种 + 单红 3 种 = 4。四点环:0 个红 1 种;1 个红 4 种;2 个红只能是对角 (1,3) 或 (2,4) 2 种;3 个及以上必有相邻 → 共 7。
练习 2(改一个条件):P3501 改成八方向连通(含对角),题面示例的答案变吗?地图 121 / 212 呢?
展开练习 2 答案
题面示例:两块之间隔着两行空地,八方向也连不上,仍是 8。121 / 212:四方向已经全连通(价值 9),八方向不变,仍是 9。只改方向数组,边界判断与标记不动。
练习 3(改一个条件):把 P3813 的规则改成「相邻必须不同色(黑红都不能相邻同色)」,染红前的检查怎么改?链 1—2—3 与三角形的方案数各是多少?
展开练习 3 答案
两个分支都要检查:染黑前要求已染色邻居里没有黑,染红前要求没有红。链:黑红黑、红黑红 2 种;三角形:0 种——这是另一道题的规则,与 P3813 不同。
练习 4(独立实现):完成「代码自测」的 count_red_black,再加两条断言:四点环应为 7,单个节点应为 2。
展开练习 4 答案
count_red_black 的参考实现(自带断言)
Pythondef count_red_black(m, edges):
adj = [[] for _ in range(m + 1)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
color = [0] * (m + 1) # 0 黑、1 红
count = 0
def paint(u):
nonlocal count
if u == m + 1: # 全部决定完 → 一种方案
count += 1
return
paint(u + 1) # 选择 1:染黑,无需检查
if all(color[v] == 0 for v in adj[u]): # 选择 2:染红——邻居里不能已有红
color[u] = 1
paint(u + 1)
color[u] = 0 # 撤销
paint(1)
return count
assert count_red_black(3, [(1, 2), (2, 3)]) == 5
assert count_red_black(3, [(1, 2), (2, 3), (1, 3)]) == 4
assert count_red_black(3, []) == 8
assert count_red_black(4, [(1, 2), (2, 3), (3, 4), (4, 1)]) == 7 # 四点环:0 红 1 种 + 1 红 4 种 + 2 红(对角)2 种
assert count_red_black(1, []) == 2五条断言覆盖:链、三角形、无边、四点环、单点。撤销那一行删掉后第一条断言就会失败(得到 4)。
练习 5(迁移):不运行程序,对 P3517 输入 3 4 / X X X X / O O X O / X X X X 写出输出;再对 P3801 输入 3 / 0 1 9 / 9 0 1 / 1 9 0 列出两条路线的路程。
展开练习 5 答案
P3517:块 {(1,0),(1,1)} 面积 2,边界格只有 (1,0)(第一列)→ 单入口;块 {(1,3)} 面积 1,边界格 (1,3) → 也是单入口;最大是面积 2 且唯一 → 输出 1 0 2。P3801:0→1→2→0 = 1 + 1 + 1 = 3;0→2→1→0 = 9 + 9 + 9 = 27 → 输出 3。
11 / 读题要求与复习自评
四道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | P3501 | P3813 | P3517 | P3801 |
|---|---|---|---|---|
| 输入 | 若干行数字字符串(行数不给) | M N;N 行边(节点 1..M) | m n;m 行 X/O | n;n×n 距离矩阵 |
| 输出 | 最大矿堆价值 | 染色方案数 | 入口行 列 面积 / 面积 / NULL | 最短总路程 |
| 标记 | 事实,不撤销 | 尝试,撤销 | 事实,不撤销 | 尝试,撤销 |
| 数据范围 | ≤ 300×300 | M < 15 | m、n ≤ 200 | n < 10,距离 < 500 |
| 样例 | 题面 4 行 → 8 | 自拟 3 2 / 1 2 / 2 3 → 5 | 自拟 4×4 → 3 2 5 | 自拟 4 基站 → 13 |
需要对照解法时,展开本课第 08 节的完整参考程序;题目页另有思路与参考代码,可在题目页查看。完成条件:本课算完成 = P3501、P3813 两道必做题都通过判题,并勾选全部六条「学习完成检查」;六条检查是自评,勾选不改变题目的通过(AC)状态;进阶练习与复习题单独统计,不影响完成状态。复习时用三个问题自测:① 不看正文,说出递归边界的三条判断及顺序;② 不看表格,画出链 1—2—3 的递归树并数出 5 种;③ 说出 P3801 的剪枝为什么不会丢掉最优解。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/2 道
回溯统计染色方案数(count_red_black)
代码自测自主练习练习重点:按节点序决定黑 / 红、染红前查邻居、返回后撤销;预计用时:15 分钟
完成标准:能口述漏掉撤销时,三个断言分别会错成什么
需要时查看提示
颜色数组 color[u] 存当前颜色(0 黑 1 红),试完「红」递归返回后置回 0。染红前只看已染色的邻居里有没有红。无边图的断言 2^3 = 8 能帮你发现「分支写错层」的问题。参考实现在第 10 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
def count_red_black(m, edges):
# 你来写:节点 1..m、无向边 edges;每点染黑或红,红色不能相邻(黑黑可以)
# 返回染色方案总数(回溯:选择→递归→撤销)
...
# 手推过的链 1—2—3:黑黑黑、红黑黑、黑红黑、黑黑红、红黑红 共 5 种
assert count_red_black(3, [(1, 2), (2, 3)]) == 5
# 三角形:最多只能有一个红 → 全黑 + 三种单红 = 4
assert count_red_black(3, [(1, 2), (2, 3), (1, 3)]) == 4
# 无边时每点独立:2^3 = 8
assert count_red_black(3, []) == 8P3501 · 寻找最大价值的矿堆
必做任务 1练习重点:连通块 DFS 聚合价值(银 1 金 2),全局取最大;预计用时:20 分钟
完成标准:能解释 visited 为什么不撤销、每格为什么只算一次
需要时查看提示
递归边界按「越界 → 空地('0') → 已访问」顺序判。网格按字符读,价值映射 '1'→1、'2'→2。空地图(全 0)答案是 0;程序开头调高递归上限。第 04 节把题面示例逐块列出。
P3813 · 无向图染色
必做任务 2练习重点:邻接表存图 + 回溯枚举 + 红红不相邻剪枝;预计用时:25 分钟
完成标准:能说出递归树每层对应什么、剪枝剪掉的是哪类分支
需要时查看提示
按节点编号逐个决定:染黑直接递归;染红前检查已染色邻居里没有红;决定完最后一个节点方案数 +1。返回前把 color[u] 恢复为黑——链 1—2—3 的 5 种就是检验这一行的。规则以题目页为准(只有红红不能相邻)。
P3517 · 查找单入口区域
进阶练习 1进阶练习练习重点:连通块 DFS 同时统计两件事:面积 + 边界入口数;预计用时:25 分钟
完成标准:能说出「入口」的判定条件和去重方式
需要时查看提示
一次 DFS 里同时数:块内 'O' 格数、位于边界上的 'O' 格数。入口数恰为 1 的块才参与「最大」比较。角落格也只算一个入口——按格去重,不按方向。三种输出形式见第 06 节。
P3801 · 基站维修工程师
进阶练习 2进阶练习练习重点:n<10 的全排列回溯:访问标记选择后要撤销;预计用时:25 分钟
完成标准:能根据最坏约 n! 条排列、数据上限和时间限制判断回溯是否可行
需要时查看提示
从基站 0 出发枚举访问顺序,走完所有基站加上回程距离取最小。距离矩阵不对称(x→y ≠ y→x),累加时用 dist[当前][下一个]。最坏 8! = 40320 条路径,回溯足够;「路程已不小于最优」可以剪。第 07 节列出了 4 基站的 6 条路线。
提交结果
提交结果说明与处理方法
- WA
答案错误
优先检查三处:回溯没撤销(方案数偏少)、P3813 规则写成相邻不同色、P3801 用了对称距离或忘了回程——第 09 节的表给出了每种错误的具体输出
- RE
运行错误
先取格子值再判越界;深网格没调 sys.setrecursionlimit 会抛出 RecursionError;没有已访问标记会无限递归
- TLE
超时
连通块没有 visited 时退化 O(n²);回溯没做冲突剪枝,整棵递归树都会被遍历
- AC
通过
再测全空地图、单格矿、无边图三个边界;口述一遍你的剪枝为什么是安全的
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。