简介:这份Word版人工智能习题答案面向计算机专业学生及备考人工智能课程的读者,针对教材课后习题缺少标准解答、复习时难以自检的痛点,提供系统化的参考答案。资源为单个doc文档,压缩包约560KB,内容按章节组织,覆盖绪论、知识表示方法等核心模块。文档围绕人工智能的学科与能力定义、发展中的控制论与图灵测试等思潮、物理符号系统假设及其推论、符号主义与联结主义和行为主义三大学派的认知观、认知行为的多层次研究,以及问题求解、逻辑推理、专家系统、机器学习、模式识别等研究应用领域展开,并配有传教士与野人渡河、状态空间法求最短旅行路径等经典例题的完整推导过程。已有41人学习,适合需要对照教材梳理知识点、核对解题思路的读者参考。
1. 一份被低估的 AI 习题答案,到底能拿来干什么
很多人看到「完整word版人工智能习题答案(1).doc」这种文件名,第一反应是「这不就是期末抱佛脚的复习资料吗」。我一开始也这么想,直到有次帮一个做计算机培训的朋友整理教学素材,才发现这类文档的价值远不止应付考试。它覆盖了从绪论、知识表示、搜索推理到计算智能的完整章节脉络,每一道题背后其实都对应着一个经典 AI 问题的求解思路——传教士与野人、梵塔、八数码、迷宫搜索、谓词演算化子句形,这些不是纸上谈兵,而是理解搜索算法和知识表示最直接的抓手。这份资源适合三类人:正在学人工智能课程需要对照答案理清推导过程的学生,想快速回顾经典 AI 算法的手艺人,以及需要现成例题做教学演示的培训者。它不教你调参炼丹,但能把「状态空间」「与或图」「消解反演」这些概念从黑匣子里拽出来,让你看到每一步是怎么走的。
2. 从传教士与野人到八数码:状态空间法怎么落地
2.1 状态表示与算符设计
文档里 2-2 的传教士和野人问题,是理解状态空间法最经典的入口。题目要求用船把 3 个传教士和 3 个野人从右岸渡到左岸,船最多载两人,任何时刻河两岸都不能出现野人数超过传教士数的情况。答案给出的做法是用 S(nC, nY) 表示对岸状态,nC 是传教士数,nY 是野人数,然后用 d(dC, dY) 表示每次渡河后对岸状态的变化量。这个思路的核心是把「渡河」抽象成状态转移,把「安全条件」抽象成状态约束。
我一般会先把所有合法状态列出来,再画状态转移图。合法状态只有三种情况:nC=0、nC=3、或者 nC=nY 且 nC 不等于 0 或 3。这样一筛,状态空间就从 16 个缩减到 10 个左右,搜索起来快很多。算符的设计也有讲究:i 为偶数时船驶向对岸,dC 和 dY 非负;i 为奇数时船驶回,dC 和 dY 非正。这个奇偶交替的约定,本质上是在用步数编码船的方向,避免在状态里额外存一个「船在哪边」的变量。
# 传教士与野人问题的状态空间搜索框架 # 状态用 (nC, nY, boat) 表示,boat=1 表示船在对岸,boat=0 表示船在原岸 from collections import deque def is_valid(nC, nY): """检查状态是否安全:两岸都不能出现野人数量超过传教士""" # 对岸检查 if nC > 0 and nY > nC: return False # 原岸检查:原岸传教士 = 3 - nC,原岸野人 = 3 - nY if (3 - nC) > 0 and (3 - nY) > (3 - nC): return False return True def get_successors(state): """生成所有合法后继状态""" nC, nY, boat = state successors = [] # 船上的载人方案:1 或 2 人,组合包括 (1,0),(0,1),(2,0),(0,2),(1,1) moves = [(1,0),(0,1),(2,0),(0,2),(1,1)] for dC, dY in moves: if boat == 1: # 船在对岸,要驶回原岸,对岸人数减少 new_nC, new_nY = nC - dC, nY - dY else: # 船在原岸,要驶向对岸,对岸人数增加 new_nC, new_nY = nC + dC, nY + dY # 人数必须在 0 到 3 之间 if 0 <= new_nC <= 3 and 0 <= new_nY <= 3: if is_valid(new_nC, new_nY): successors.append((new_nC, new_nY, 1 - boat)) return successors # 广度优先搜索找最短渡河方案 def solve(): start = (0, 0, 0) # 初始:对岸没人,船在原岸 goal = (3, 3, 1) # 目标:对岸 3 传教士 3 野人,船在对岸 queue = deque([(start, [])]) visited = {start} while queue: state, path = queue.popleft() if state == goal: return path + [state] for nxt in get_successors(state): if nxt not in visited: visited.add(nxt) queue.append((nxt, path + [state])) return None result = solve() for i, s in enumerate(result): print(f"Step {i}: nC={s[0]}, nY={s[1]}, boat={'对岸' if s[2]==1 else '原岸'}")这段代码里is_valid是关键,它同时检查两岸的安全性,缺一不可。get_successors里的moves列表覆盖了船的所有载人组合,注意 (1,1) 这种一传教士一野人的组合容易被漏掉。boat变量用 0 和 1 交替,省去了单独记录船位置的字段。跑出来的路径长度是 11 步,和文档里给出的 d1 到 d11 完全对应。
2.2 与或图在梵塔问题中的表达
文档 2-5 的四圆盘梵塔问题,答案用四元数列 (nA, nB, nC, nD) 表示状态,每个分量表示对应圆盘落在哪根柱子上。初始状态 1111,目标状态 3333。这个表示法的好处是状态编码极其紧凑,四个数字就能唯一确定一个局面。答案里给出的求解过程是一棵与或图,每个节点代表一个子问题,与节点表示「同时满足」,或节点表示「选择一种方式」。
我一般会先把三圆盘梵塔的与或图画一遍,确认理解了再上四圆盘。三圆盘的目标是把 A 柱上的三个盘移到 C 柱,分解成三个子问题:把上面两个盘从 A 移到 B,把最大的盘从 A 移到 C,再把两个盘从 B 移到 C。每个子问题又可以继续分解,直到只剩一个盘可以直接移动。这个递归分解的过程,就是与或图展开的过程。文档里按从上往下的顺序依次处理叶结点,本质上就是深度优先遍历与或图,先解决最底层的原子问题,再逐层向上合并。
提示:与或图的解图不是普通路径,而是一棵子树。判断一个与或图是否有解,要看是否存在一个解图,其中所有叶节点都是可解节点。梵塔问题一定有解,因为每个原子问题「移动一个盘」都是可直接执行的。
2.3 八数码难题的有界深度优先搜索
文档 3-7 用有界深度优先搜索求解八数码难题,最大深度定为 5,按顺时针方向(上、右、下、左)试探空格移动。这个顺序很关键,它决定了搜索树的展开次序。答案里给出了 S0 到 S8 的完整状态序列,最终到达目标状态。
有界深度优先搜索和普通深度优先的区别在于多了一个深度限制。当节点深度等于最大深度时,不再扩展,直接回溯。这个限制是为了防止搜索陷入无限深的支路,但代价是可能错过深度超过限制的解。八数码问题里,从初始状态到目标状态的最短路径长度是固定的,如果最大深度设得太小,可能找不到解;设得太大,又退化成普通深度优先,效率下降。
# 八数码难题的有界深度优先搜索 # 状态用长度为 9 的元组表示,0 代表空格 def dfs_bounded(start, goal, max_depth): """有界深度优先搜索,返回从 start 到 goal 的路径""" # 方向顺序:上、右、下、左,对应空格移动方向 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] visited = set() def dfs(state, depth, path): if state == goal: return path if depth >= max_depth: return None visited.add(state) # 找到空格位置 zero_idx = state.index(0) row, col = divmod(zero_idx, 3) for dr, dc in directions: nr, nc = row + dr, col + dc if 0 <= nr < 3 and 0 <= nc < 3: new_idx = nr * 3 + nc new_state = list(state) new_state[zero_idx], new_state[new_idx] = new_state[new_idx], new_state[zero_idx] new_state = tuple(new_state) if new_state not in visited: result = dfs(new_state, depth + 1, path + [new_state]) if result is not None: return result visited.discard(state) # 回溯时移除,允许其他路径访问 return None return dfs(start, 0, [start]) # 文档中的初始状态和目标状态 start = (2, 8, 1, 1, 6, 3, 7, 5, 4) # 按行展开 goal = (1, 2, 3, 8, 4, 7, 6, 5, 0) # 注意:文档目标状态需按实际题目调整 path = dfs_bounded(start, goal, max_depth=5) if path: for i, s in enumerate(path): print(f"Step {i}: {s}") else: print("在深度限制内未找到解")这段代码里visited集合在回溯时会移除当前状态,这是为了允许不同路径访问同一状态。如果不去除,搜索会变成「每个状态只访问一次」,可能漏掉某些解。directions的顺序决定了搜索的优先方向,文档里用的是上、右、下、左,代码里保持一致。max_depth参数需要根据实际问题调整,八数码的某些初始状态可能需要 20 步以上才能到达目标,深度限制设 5 可能不够,这时候要么放宽限制,要么换用 A* 之类的启发式搜索。
3. 谓词演算化子句形:九个步骤的实操拆解
3.1 从自然语言到谓词公式
文档 2-7 要求把英文句子「A computer system is intelligent if it can perform a task which, if performed by a human, requires intelligence」表示成谓词演算公式。答案先定义了五个谓词:INTLT(x) 表示 x 是智能的,PERFORM(x,y) 表示 x 能执行 y,REQUIRE(x) 表示 x 需要智能,CMP(x) 表示 x 是计算机系统,HMN(x) 表示 x 是人类。然后给出公式:(任意 x){ (存在 t)(存在 y)[ HMN(y) 合取 PERFORM(y,t) 合取 REQUIRE(t) 合取 CMP(x) 合取 PERFORM(x,t) ] 蕴含 INTLT(x) }。
这个转换过程的关键是识别句子里的逻辑结构。「if it can perform a task」是条件,「which, if performed by a human, requires intelligence」是对 task 的限定。我一般会先把句子拆成主句和从句,主句是「A computer system is intelligent」,从句是「it can perform a task」,而 task 又有一个定语从句「if performed by a human, requires intelligence」。拆完之后,用存在量词约束 task 和 human,用全称量词约束 computer system,再用合取把条件串起来。
3.2 九步变换法的逐步操作
文档 3-3 给出了化子句形的九个步骤,这是谓词演算里最核心的机械操作。我结合 2-6 的第 (4) 小题来演示一遍。原公式是:~{(任意 x){P(x) 蕴含{(任意 y)[p(y) 蕴含 p(f(x,y))] 合取 (任意 y)[Q(x,y) 蕴含 P(y)]}}}。
第一步消去蕴涵符号。把 A 蕴含 B 换成 ~A 析取 B。公式变成:~{(任意 x){~P(x) 析取{(任意 y)[~p(y) 析取 p(f(x,y))] 合取 (任意 y)[~Q(x,y) 析取 P(y)]}}}。
第二步减少否定辖域。最外层的否定要往里推,用狄摩根定律把 ~(A 合取 B) 换成 ~A 析取 ~B,把 ~(任意 x) 换成 (存在 x)~。这一步最容易翻车,因为否定符号的辖域边界很容易搞错。我的习惯是每推一步就重新画一遍括号,确保否定只作用在原子公式上。
第三步对变量标准化。把不同量词约束的变量改成不同的名字,避免混淆。比如把内层的 y 改成 w 和 v。
第四步消去存在量词。引入 Skolem 函数,如果存在量词不在任何全称量词的辖域内,就用 Skolem 常量。这里 (存在 x) 在最外层,可以用常量 A 代替;(存在 w) 在 (任意 x) 的辖域内,要用 Skolem 函数 f(x) 代替。
第五步化为前束形。把所有全称量词移到公式左边,母式放在右边。
第六步把母式化为合取范式。反复用分配律,把公式写成多个合取项的合取。
第七步消去全称量词。前缀里的全称量词可以直接去掉,因为剩下的变量默认都是全称量化的。
第八步消去合取符号。用 {合取项1, 合取项2} 的形式替换合取。
第九步更换变量名称。让每个变量只出现在一个子句里,避免跨子句的变量混淆。
# 谓词演算公式化子句形的简化演示 # 以 2-6 第 (2) 小题为例:(任意 x)(任意 y)(On(x,y) -> Above(x,y)) # 手动模拟九个步骤的关键变换 def step_by_step(): print("原始公式: (ANY x)(ANY y)(On(x,y) -> Above(x,y))") print() print("步骤1 消去蕴涵: (ANY x)(ANY y)(~On(x,y) OR Above(x,y))") print("步骤2 减少否定辖域: 否定已在原子公式上,无需操作") print("步骤3 变量标准化: x 和 y 已是不同变量,无需操作") print("步骤4 消去存在量词: 无存在量词,无需操作") print("步骤5 化为前束形: (ANY x)(ANY y)(~On(x,y) OR Above(x,y))") print("步骤6 化为合取范式: 已是析取形式,无需操作") print("步骤7 消去全称量词: ~On(x,y) OR Above(x,y)") print("步骤8 消去合取符号: 单个子句,无需操作") print("步骤9 更换变量名称: ~On(x,y) OR Above(x,y)") print() print("最终子句: {~On(x,y), Above(x,y)}") step_by_step()这段代码没有做真正的符号计算,而是把每一步的变换结果打印出来,方便对照检查。实际工程里如果要做自动化的子句形转换,需要写一个递归下降的解析器,处理量词、连接词和括号的嵌套。我一般会先用这种「手动模拟」的方式验证理解是否正确,确认每一步的逻辑都通了,再考虑写代码自动化。
3.3 消解反演的证明框架
文档 3-4 给出了消解反演求取答案的步骤:否定目标公式 L 得到 ~L,把 ~L 加入公式集 S,把 {~L, S} 化成子句集,然后应用消解原理推导空子句 NIL。这个框架的核心是反证法,把「证明 L 成立」转化为「证明 ~L 与 S 矛盾」。
我一般会先把 S 和 ~L 的所有子句列出来,然后两两配对做消解。消解的时候要注意互补文字:如果两个子句里分别有 P 和 ~P,就可以把这两个子句消解成一个新子句,新子句是去掉互补文字后剩余文字的析取。这个过程反复进行,直到推出空子句。空子句的出现意味着矛盾,证明原假设 ~L 不成立,所以 L 成立。
注意:消解反演只能用于证明,不能用于生成新知识。而且消解过程可能不终止,如果公式集本身不可满足但子句数量无限,搜索会一直进行下去。实际使用时要设置步数上限或子句数量上限。
4. 搜索策略选型:宽度优先、有界深度与等代价的边界
4.1 三种搜索策略的适用场景
文档 3-2 比较了宽度优先搜索、有界深度优先搜索和等代价搜索。宽度优先把新节点放到 OPEN 表末端,保证先扩展浅层节点,适合找最短路径(在边权相等的情况下)。有界深度优先把新节点放到 OPEN 表前头,优先扩展深层节点,适合解空间深但分支因子小的问题。等代价搜索按 g(i) 递增顺序扩展,适合边权不等、需要找最小代价路径的问题。
我选搜索策略的时候,先看三个指标:解空间的深度、分支因子、边权是否相等。如果边权相等且需要最短路径,宽度优先是首选,实现简单,不会陷入深支路。如果解空间很深但解集中在某些分支,有界深度优先更合适,但深度限制需要凭经验或试错确定。如果边权不等,等代价搜索是唯一能保证最优解的选择,但需要维护一个按 g 值排序的优先队列。
| 搜索策略 | OPEN 表操作 | 适用场景 | 最优性保证 |
|---|---|---|---|
| 宽度优先 | 新节点放末端 | 边权相等,找最短路径 | 是 |
| 有界深度优先 | 新节点放前头 | 解空间深,分支因子小 | 否 |
| 等代价搜索 | 按 g 值排序 | 边权不等,找最小代价 | 是 |
4.2 重排 OPEN 表的原则
文档 3-1 提到重排 OPEN 表意味着决定优先扩展哪个节点,不同的排序标准对应不同的搜索策略。如果想尽快找到解,把最有可能达到目标的节点排在前面;如果想找代价最小的解,按代价从小到大排。这个原则听起来简单,但实际操作时「最有可能达到目标」很难量化,需要启发式函数来估计。
我一般会用 f(n) = g(n) + h(n) 作为排序依据,g(n) 是从起点到 n 的实际代价,h(n) 是从 n 到目标的估计代价。h(n) 的设计是关键,如果 h(n) 永远不超过实际代价,A* 算法能保证找到最优解;如果 h(n) 设计得过于乐观,搜索效率会下降;如果 h(n) 设计得过于悲观,可能找不到最优解。八数码问题里常用的 h(n) 是「不在正确位置的数字个数」或「所有数字到目标位置的曼哈顿距离之和」,后者更精确,但计算量也更大。
4.3 迷宫搜索的宽度优先实现
文档 3-6 用宽度优先搜索求迷宫的出路,最终路径是 S→A→B→C→F。这个例子比较简单,但能说明宽度优先的扩展顺序。我把它扩展成一个通用的迷宫搜索框架,支持任意大小的迷宫和任意起点终点。
# 迷宫宽度优先搜索 # 迷宫用二维列表表示,0 表示通路,1 表示墙壁 from collections import deque def bfs_maze(maze, start, end): """返回从 start 到 end 的最短路径,如果不存在返回 None""" rows, cols = len(maze), len(maze[0]) # 方向:上、下、左、右 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] queue = deque([(start, [start])]) visited = {start} while queue: (r, c), path = queue.popleft() if (r, c) == end: return path for dr, dc in directions: nr, nc = r + dr, c + dc if 0 <= nr < rows and 0 <= nc < cols: if maze[nr][nc] == 0 and (nr, nc) not in visited: visited.add((nr, nc)) queue.append(((nr, nc), path + [(nr, nc)])) return None # 示例迷宫:0 通路,1 墙壁 maze = [ [0, 1, 0, 0, 0], [0, 1, 0, 1, 0], [0, 0, 0, 1, 0], [1, 1, 0, 0, 0], [0, 0, 0, 1, 0] ] start = (0, 0) end = (4, 4) path = bfs_maze(maze, start, end) if path: print(f"最短路径长度: {len(path) - 1}") print(f"路径: {path}") else: print("无解")这段代码里visited集合在入队时就标记,而不是出队时标记,这是为了避免同一节点被多次加入队列。path随节点一起入队,虽然增加了内存开销,但省去了回溯父节点的步骤,代码更直观。如果迷宫很大,可以用父节点字典代替路径列表,节省内存。
5. 避坑与排查:做习题答案时最容易翻车的五个地方
5.1 状态表示不完整导致搜索漏解
现象:传教士与野人问题里,用 (nC, nY) 表示状态,搜索出来的路径在某些步骤出现野人数量超过传教士的情况。
原因:状态表示里没有包含「船在哪边」的信息,导致状态转移时无法判断船的方向,可能生成非法的渡河方案。
解决:状态里必须加上船的位置,用 (nC, nY, boat) 三元组表示。boat 用 0 和 1 交替,或者用步数的奇偶性隐式表示。每次状态转移时,根据 boat 的值决定是增加还是减少对岸人数。
5.2 消解反演时否定符号辖域搞错
现象:化子句形时,消解出来的子句和标准答案对不上,或者消解过程推不出空子句。
原因:减少否定符号辖域时,狄摩根定律用错,把 ~(A 合取 B) 写成了 ~A 合取 ~B,或者把 ~(任意 x) 写成了 (任意 x)~。
解决:每推一步就重新检查括号匹配,确保否定符号只作用在原子公式上。可以用一棵语法树来辅助,否定符号从根节点往下推,遇到合取换析取,遇到全称换存在,遇到存在换全称。
5.3 有界深度优先的深度限制设得太死
现象:八数码问题里,最大深度设为 5,搜索跑完了没找到解,但换一个初始状态又能找到。
原因:不同初始状态到目标状态的最短路径长度不同,固定深度限制无法适应所有情况。深度设得太小会漏解,设得太大又退化成普通深度优先。
解决:先用宽度优先或 A* 算一下最短路径长度,再根据这个长度设置深度限制。或者用迭代加深搜索,从深度 1 开始逐步增加限制,直到找到解。迭代加深结合了宽度优先的最优性和深度优先的空间效率。
5.4 与或图展开时把与节点和或节点搞反
现象:梵塔问题的与或图,按图求解时步骤顺序不对,或者某些子问题被重复求解。
原因:与节点表示「所有子问题都要解决」,或节点表示「解决其中一个即可」。如果把与节点当成或节点,会漏掉必须解决的子问题;如果把或节点当成与节点,会做无用功。
解决:画与或图时,用圆弧连接与节点的所有子节点,表示它们必须同时满足。或节点的子节点之间没有圆弧,表示选择关系。求解时,与节点的解图是所有子节点解图的并集,或节点的解图是任一子节点解图。
5.5 等代价搜索的 g 值更新不及时
现象:等代价搜索找到的路径不是最小代价路径,或者搜索过程中某些节点的 g 值比实际值大。
原因:当一个节点已经在 OPEN 表或 CLOSED 表里,又通过另一条路径到达时,如果新路径的 g 值更小,需要更新该节点的 g 值和父指针。如果忘了更新,搜索会沿着旧路径继续,错过更优解。
解决:每次扩展节点 i 时,对于每个后继节点 j,计算 g(j) = g(i) + c(i,j)。如果 j 已经在 OPEN 表里,比较新旧 g 值,如果新值更小就更新;如果 j 在 CLOSED 表里,同样比较,如果新值更小就把 j 移回 OPEN 表。这个操作叫「重新打开」,是等代价搜索和 A* 算法里最容易漏掉的一步。
6. 把习题答案变成教学工具:一个自动化验证脚本
文档里的习题答案虽然完整,但都是静态的文本和手写推导。我后来做了一个小工具,把传教士与野人、八数码、迷宫搜索这几个经典问题的求解过程自动化,输入初始状态就能输出完整的状态转移序列,还能和文档里的答案对照验证。这个工具的核心是一个统一的搜索框架,支持宽度优先、有界深度优先和等代价搜索三种策略,通过配置文件切换。
# 统一搜索框架:支持多种搜索策略 # 配置文件用字典表示,包含问题类型、初始状态、目标状态、搜索策略 class SearchProblem: def __init__(self, initial, goal, successors, is_goal=None): self.initial = initial self.goal = goal self.successors = successors self.is_goal = is_goal or (lambda s: s == goal) def solve_bfs(self): """宽度优先搜索""" from collections import deque queue = deque([(self.initial, [self.initial])]) visited = {self.initial} while queue: state, path = queue.popleft() if self.is_goal(state): return path for nxt in self.successors(state): if nxt not in visited: visited.add(nxt) queue.append((nxt, path + [nxt])) return None def solve_dfs_bounded(self, max_depth): """有界深度优先搜索""" visited = set() def dfs(state, depth, path): if self.is_goal(state): return path if depth >= max_depth: return None visited.add(state) for nxt in self.successors(state): if nxt not in visited: result = dfs(nxt, depth + 1, path + [nxt]) if result: return result visited.discard(state) return None return dfs(self.initial, 0, [self.initial]) # 以传教士与野人为例配置问题 def mw_successors(state): nC, nY, boat = state moves = [(1,0),(0,1),(2,0),(0,2),(1,1)] result = [] for dC, dY in moves: if boat == 1: new_nC, new_nY = nC - dC, nY - dY else: new_nC, new_nY = nC + dC, nY + dY if 0 <= new_nC <= 3 and 0 <= new_nY <= 3: if (new_nC == 0 or new_nC == 3 or new_nC == new_nY): if ((3-new_nC) == 0 or (3-new_nC) == 3 or (3-new_nC) == (3-new_nY)): result.append((new_nC, new_nY, 1 - boat)) return result problem = SearchProblem( initial=(0, 0, 0), goal=(3, 3, 1), successors=mw_successors ) path = problem.solve_bfs() print(f"传教士与野人问题最短路径长度: {len(path) - 1}") for i, s in enumerate(path): print(f" Step {i}: nC={s[0]}, nY={s[1]}, boat={'对岸' if s[2]==1 else '原岸'}")这个框架的好处是,换一个问题只需要改initial、goal和successors三个参数,搜索策略可以自由切换。我一般会先用宽度优先跑一遍,确认有解且路径长度合理,再用有界深度优先跑一遍,对比两者的扩展节点数量。如果宽度优先的扩展节点数远大于有界深度优先,说明解空间的分支因子大但深度浅,宽度优先更合适;反之则有界深度优先更高效。
从那以后我每次拿到一份习题答案,都会先挑几个经典问题写成可执行的代码,跑一遍验证答案里的推导过程。这样做的好处是,手写推导容易漏掉边界条件,但代码跑不通就是跑不通,没有后悔药可吃。希望这份拆解能帮到你,把这份习题答案从「考试资料」变成「算法入门的脚手架」。
本文还有配套的精品资源,点击获取