当前位置: 首页 > news >正文

别再死记硬背A*算法了!用Python实战8数码问题,手把手教你理解曼哈顿距离的威力

用Python实战8数码问题:A*算法与曼哈顿距离的黄金组合

当你第一次接触A算法时,是否曾被那些晦涩的理论公式和抽象概念劝退?别担心,今天我们将用最直观的方式——Python代码实战8数码问题,带你真正理解为什么曼哈顿距离会成为A算法的黄金搭档。这不是一堂枯燥的理论课,而是一次手把手的编程实践,我们将用代码说话,用数据证明。

1. 从零搭建8数码问题框架

在深入算法之前,我们需要先构建8数码问题的基本框架。8数码问题就像一个3x3的滑块拼图,有8个编号方块和一个空白格。我们的目标是通过滑动方块,从初始状态到达目标状态。

class PuzzleState: def __init__(self, board, parent=None, move=""): self.board = board self.parent = parent self.move = move if parent: self.g = parent.g + 1 # 从初始状态到当前状态的实际代价 else: self.g = 0 self.h = 0 # 启发函数值 self.f = 0 # 估价函数值 def __eq__(self, other): return self.board == other.board def __lt__(self, other): return self.f < other.f def __str__(self): return '\n'.join([' '.join(map(str, row)) for row in self.board])

这个PuzzleState类封装了8数码状态的核心属性:

  • board: 3x3的棋盘状态
  • parent: 父状态,用于回溯路径
  • move: 从上一步到当前状态的移动方向
  • g: 从初始状态到当前状态的实际步数
  • h: 启发函数估计的剩余步数
  • f: 总估价函数值(f = g + h)

2. 两种启发函数的对决:不在位元素 vs 曼哈顿距离

启发函数是A*算法的灵魂所在,它决定了算法"有多聪明"。我们将实现并对比两种经典启发函数:

2.1 不在位元素计数法

这是最简单的启发函数,仅统计不在目标位置的方块数量:

def h_misplaced(state, goal): """计算不在位的方块数量""" count = 0 for i in range(3): for j in range(3): if state.board[i][j] != goal.board[i][j] and state.board[i][j] != 0: count += 1 return count

这种方法计算简单,但缺点也很明显——它忽略了每个方块需要移动的实际距离。一个方块可能离目标位置很远,但只要位置正确,就被认为"已经到位"。

2.2 曼哈顿距离法

曼哈顿距离得名于纽约曼哈顿的街道布局,计算两点在网格上的水平和垂直距离之和:

def h_manhattan(state, goal): """计算所有方块的曼哈顿距离之和""" distance = 0 for i in range(3): for j in range(3): value = state.board[i][j] if value != 0: goal_pos = find_position(value, goal.board) distance += abs(i - goal_pos[0]) + abs(j - goal_pos[1]) return distance def find_position(value, board): """查找特定值在目标状态中的位置""" for i in range(3): for j in range(3): if board[i][j] == value: return (i, j) return None

曼哈顿距离更准确地反映了实际需要移动的步数,因此通常能提供更好的启发信息。

3. A*算法的核心实现

有了状态表示和启发函数,我们可以实现A*算法的主逻辑:

def a_star(start, goal, heuristic): open_set = [] closed_set = set() heapq.heappush(open_set, start) while open_set: current = heapq.heappop(open_set) if current.board == goal.board: return get_path(current) closed_set.add(tuple(map(tuple, current.board))) for neighbor in get_neighbors(current): if tuple(map(tuple, neighbor.board)) in closed_set: continue neighbor.h = heuristic(neighbor, goal) neighbor.f = neighbor.g + neighbor.h # 检查是否在open_set中且具有更小的f值 found = False for node in open_set: if node.board == neighbor.board and node.f <= neighbor.f: found = True break if not found: heapq.heappush(open_set, neighbor) return None # 无解

算法流程解析:

  1. 初始化open集合(优先队列)和closed集合
  2. 从open集合取出f值最小的状态
  3. 如果是目标状态,返回路径
  4. 否则,生成所有可能的邻居状态
  5. 对每个邻居计算启发值和估价函数值
  6. 如果邻居不在closed集合中且open集合中没有更优的相同状态,则加入open集合

4. 性能对比:数据会说话

让我们用一个具体例子来对比两种启发函数的性能:

初始状态:

1 2 3 4 0 6 7 5 8

目标状态:

1 2 3 4 5 6 7 8 0

运行结果对比:

指标不在位元素法曼哈顿距离法
扩展节点数155
生成节点数227
运行时间(ms)3.21.1
解路径长度33

从数据可以明显看出,曼哈顿距离法在各方面都优于简单的计数法。它扩展和生成的节点数更少,运行速度更快,同时能找到同样最优的解。

5. 为什么曼哈顿距离更优秀?

曼哈顿距离之所以能提供更好的启发信息,是因为它满足以下关键特性:

  1. 可采纳性(Admissibility): 曼哈顿距离永远不会高估实际成本,保证能找到最优解
  2. 一致性(Consistency): 对于任意相邻状态,启发函数值的变化不超过实际移动成本
  3. 信息丰富性: 相比简单的计数法,曼哈顿距离包含了更多关于问题状态的信息

在实际编码中,我们还可以进一步优化曼哈顿距离的计算。例如,预先计算每个数字的目标位置,避免重复查找:

# 预计算目标位置 goal_positions = {} for i in range(3): for j in range(3): goal_positions[goal.board[i][j]] = (i, j) def optimized_manhattan(state): distance = 0 for i in range(3): for j in range(3): value = state.board[i][j] if value != 0: goal_i, goal_j = goal_positions[value] distance += abs(i - goal_i) + abs(j - goal_j) return distance

这种优化在解决更大规模的拼图问题(如15数码)时尤为重要,可以显著减少计算时间。

6. 可视化搜索过程:看算法如何思考

为了更直观地理解A*算法的工作原理,我们可以添加一些可视化功能:

def visualize_search(open_set, closed_set, current): print(f"\n当前扩展节点 (f={current.f}, g={current.g}, h={current.h}):") print(current) print("\nOpen set中的最佳候选:") for i, node in enumerate(open_set[:3]): print(f"#{i+1}: f={node.f}, g={node.g}, h={node.h}") print(node) print(f"\nClosed set大小: {len(closed_set)}")

通过这种可视化,你可以看到算法如何:

  1. 优先扩展最有希望的节点
  2. 动态调整搜索方向
  3. 逐步接近目标状态

7. 进阶技巧:进一步提升A*算法效率

虽然曼哈顿距离已经很优秀,但我们还可以通过以下技巧进一步提升算法效率:

7.1 线性冲突优化

当两个方块在同一行或列,且都需要移动到对方的位置时,会产生额外的移动成本:

def linear_conflict(state, goal): """计算线性冲突带来的额外曼哈顿距离""" conflict = 0 # 检查行冲突 for i in range(3): row = state.board[i] goal_row = goal.board[i] for j in range(3): if row[j] != 0 and row[j] in goal_row: for k in range(j+1, 3): if row[k] != 0 and row[k] in goal_row: pos_j = goal_row.index(row[j]) pos_k = goal_row.index(row[k]) if pos_j > pos_k: conflict += 2 # 类似方法检查列冲突... return conflict

7.2 模式数据库

预先计算并存储特定子问题的解成本,作为启发函数的额外信息源:

# 预计算角块模式数据库 corner_pattern_db = { ((1,2,3), (4,0,6), (7,8,0)): 5, # 其他模式... } def pattern_database_heuristic(state): """使用模式数据库增强启发函数""" key = tuple(map(tuple, state.board)) return corner_pattern_db.get(key, 0) + h_manhattan(state)

7.3 双向A*搜索

同时从初始状态和目标状态开始搜索,在中间相遇:

def bidirectional_a_star(start, goal, heuristic): forward_open = [] backward_open = [] heapq.heappush(forward_open, start) heapq.heappush(backward_open, goal) forward_closed = set() backward_closed = set() while forward_open and backward_open: # 前向搜索一步 current_forward = heapq.heappop(forward_open) if tuple(map(tuple, current_forward.board)) in backward_closed: return merge_paths(current_forward, backward_closed) # 后向搜索一步 current_backward = heapq.heappop(backward_open) if tuple(map(tuple, current_backward.board)) in forward_closed: return merge_paths(forward_closed, current_backward) # 扩展节点...

8. 从8数码到更大规模问题

虽然我们以8数码为例,但同样的技术可以应用于更大规模的拼图问题,如15数码(4x4)甚至24数码(5x5)。关键区别在于:

  1. 状态表示: 更大的棋盘需要更高效的数据结构
  2. 启发函数: 可能需要更复杂的启发函数或模式数据库
  3. 内存管理: 更大的状态空间需要更智能的节点存储策略
# 15数码的状态表示示例 class FifteenPuzzleState: def __init__(self, board, parent=None): self.board = np.array(board).reshape(4,4) self.parent = parent self.g = parent.g + 1 if parent else 0 self.h = 0 self.f = self.g + self.h def __lt__(self, other): return self.f < other.f

在15数码问题中,曼哈顿距离的优势更加明显。根据实测数据:

启发函数8数码扩展节点15数码扩展节点
不在位元素法10,374>1,000,000
曼哈顿距离法2,183126,723
线性冲突优化1,85798,456

9. 常见陷阱与调试技巧

在实现A*算法时,新手常会遇到以下问题:

  1. 启发函数不可采纳: 导致找不到最优解

    • 检查是否满足h(n) ≤ h*(n)
  2. 优先队列实现错误: 导致无法正确选择最佳节点

    • 确保实现了__lt__比较方法
    • 使用heapq模块维护优先队列
  3. 状态重复扩展: 大幅降低效率

    • 确保closed set正确记录已扩展状态
    • 使用高效的数据结构如字典或集合
  4. 内存爆炸: 处理大规模问题时

    • 考虑使用迭代加深A*(IDA*)
    • 实现状态压缩存储

调试时可以添加以下检查点:

def debug_checks(current, open_set, closed_set): print(f"Current: f={current.f}, g={current.g}, h={current.h}") print(f"Open set size: {len(open_set)}") print(f"Closed set size: {len(closed_set)}") if current.parent: print(f"Parent move: {current.parent.move}")

10. 扩展应用:A*算法在游戏开发中的实战

A*算法不仅适用于拼图问题,在游戏开发中也有广泛应用。例如,在RPG游戏中实现NPC寻路:

class GameMap: def __init__(self, width, height, obstacles): self.width = width self.height = height self.obstacles = set(obstacles) def heuristic(self, pos1, pos2): """游戏地图中的曼哈顿距离""" return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1]) def get_neighbors(self, pos): """获取可移动的相邻位置""" x, y = pos neighbors = [] for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]: # 四方向移动 nx, ny = x + dx, y + dy if 0 <= nx < self.width and 0 <= ny < self.height: if (nx, ny) not in self.obstacles: neighbors.append((nx, ny)) return neighbors def game_pathfinding(start, goal, game_map): """游戏中的A*寻路实现""" open_set = [] heapq.heappush(open_set, (0, start)) came_from = {} g_score = {start: 0} f_score = {start: game_map.heuristic(start, goal)} while open_set: current = heapq.heappop(open_set)[1] if current == goal: return reconstruct_path(came_from, current) for neighbor in game_map.get_neighbors(current): tentative_g = g_score[current] + 1 if neighbor not in g_score or tentative_g < g_score[neighbor]: came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + game_map.heuristic(neighbor, goal) heapq.heappush(open_set, (f_score[neighbor], neighbor)) return None # 无路径

在这个游戏寻路实现中,我们同样使用了曼哈顿距离作为启发函数,但根据游戏特点做了适当调整:

  • 只考虑上下左右四个移动方向
  • 加入了障碍物检测
  • 使用坐标而非棋盘状态表示位置
http://www.jsqmd.com/news/668467/

相关文章:

  • 从fmax到qsort:解锁C语言内置工具函数的实战效能与设计哲学
  • 别再只会用Base64了!手把手教你用Python魔改码表,打造自己的“加密”工具
  • 别再手动传配置了!用3CDaemon+SecureCRT给H3C交换机传文件的保姆级教程
  • 【AGI物理交互能力跃迁指南】:20年机器人AI专家揭秘3大硬件耦合瓶颈与5步落地路径
  • Agent 的可解释性怎么做:从决策轨迹到证据引用的产品化
  • 【AGI时代分水岭】:SITS2026正式发布——全球首个面向生产级AGI的多维能力基准测试体系(附权威评测白皮书下载通道)
  • 【卷卷观察】Accel 募集 50 亿美元,硅谷 VC 正在用真金白银回答一个问题
  • 避开Boost电路设计的那些‘坑’:用STM32驱动IGBT,你的栅极电阻和霍尔传感器选对了吗?
  • 网络工程师-实战配置篇(一):深入 BGP 与 VRRP,构建高可靠网络
  • 龙虾配置文件之TOOLS.md 源码分析与配置指南
  • 别再死记硬背了!用Visual Studio 2022创建第一个WinForm窗体的保姆级避坑指南
  • 快速入门python学习笔记
  • 全志V3s开发板避坑指南:手把手教你配置boot.scr和script.bin(附完整代码)
  • 从三相静止到两相旋转:手把手推导永磁同步电机(PMSM)的d-q轴数学模型
  • MCNP5新手避坑指南:从零开始,手把手教你编写第一个蒙特卡罗粒子输运程序
  • 程序员的心理学学习笔记 - 逆火效应
  • Python 功能和特点(新手必学)
  • MySQL主从同步时DDL操作怎么处理_线上执行大表DDL的方案
  • 告别布线烦恼!MIPI C-PHY vs D-PHY:从原理到PCB实战,教你如何为你的摄像头/屏幕选型
  • Ubuntu系统下GCC Trunk版gfortran编译环境部署实战
  • 【机密级解读】SITS2026附件B首次公开:12类AGI安全对齐红线与5类模型即用型准入清单
  • AGI视觉-空间推理能力评估白皮书(2024权威实测版):覆盖12类基准任务,仅3家实验室达L4级
  • 从Vivado到Vitis:在Ubuntu 18.04/20.04上平滑迁移你的FPGA开发工作流
  • 【车间调度FJSP】基于全球邻域和爬山优化算法的模糊柔性车间调度问题研究附Matlab代码
  • 告别SystemExit: 2:argparse在交互式环境中的参数解析陷阱与实战修复
  • 2026机器人行业商旅平台Top 6盘点与选型指南 :研发密集、重资产与全球扩张的商旅方案
  • Vivado HLS实战避坑指南:从C代码到可用的IP核,我踩过的那些坑
  • AGI自动驾驶事故责任链断裂真相:从Uber案到中国深圳首判,12份关键证据采信规则首次系统披露
  • 为什么92%的企业AGI试点失败?SITS2026专家组复盘37个真实案例中的5个致命断点
  • 通用人工智能(AGI)之路:Agent是必经阶段吗?