零、A*算法学习参考资料
1.讲解视频
A*寻路算法详解 #A星 #启发式搜索_哔哩哔哩_bilibili
2.A*算法学习网站
A* 算法简介
一、实验目的
熟悉和掌握A*算法实现迷宫寻路功能,要求掌握启发式函数的编写以及各类启发式函数效果的比较。
二、实验要求
同课本 附录B 实验指导书 实验五的指导要求和实验报告要求:
1.画出用A*算法求解迷宫最短路径的流程图。
2.设置不同的地图,以及不同的初始状态和目标状态,记录A*算法的求解结果,包括最短路径、扩展节点数、生成结点数和算法运行时间。
3.对于相同的初始状态和目标状态,设计不同的启发式函数,比较不同启发式函数对迷宫寻路速度的提升效果,包括扩展节点数、生成结点数和算法运行时间。
实验内容
迷宫问题是在以方格表示的地图场景中,对于给定的起点、终点和障碍物,如何找到一条从起点开始避开障碍物到达终点的最短路径。 假设在一个n*m的迷宫里,入口坐标和出口坐标分别为(1,1)和(5,5),每一个坐标点有两种可能:0和1,其中0表示该位置允许通过,1表示该位置不允许通过。
三、实验材料
A*算法的基本原理:
A*算法在搜索过程中,考虑了每个节点到起点的实际代价(g(n))和从该节点到目标的估算代价(h(n))。它使用以下公式来计算每个节点的评估代价:
f(n)=g(n)+h(n)
g(n):从起点到节点n的实际代价,通常是路径长度或其他度量。
h(n):从节点n到目标节点的估算代价,通常通过启发式方法来计算。常见的启发式函数包括曼哈顿距离、欧几里得距离等。
f(n):节点n的总代价,它是g(n)和h(n)的和。A*算法会优先选择具有最小f值的节点进行扩展。
A*算法的步骤:
1. 初始化:将起点加入开放列表(Open List),并设置其g和h值。起点的f值为g + h。
2. 循环搜索:
3. 目标找到:当目标节点被加入闭合列表时,路径搜索结束。此时,可以从目标节点回溯到起点,得到最短路径。
4. 结束:如果开放列表为空,表示没有路径可达目标。
特点:
启发式:A*算法通过启发式函数来引导搜索过程,使得搜索在达到目标时更为高效。
最优性:只要启发式函数是可接受的(即不高估到目标的代价),A*算法可以保证找到最短路径。可接受的启发式函数通常需要满足一致性或单调性条件。
启发式函数的选择:
启发式函数h(n)的选择对A*算法的性能和效率有重要影响。常见的启发式函数包括:
曼哈顿距离:适用于格子状地图,只能横向或纵向移动。
欧几里得距离:适用于平面上的直线距离。
四、实验步骤
1.画出A*算法求解迷宫最短路径问题的流程图。
2.分析不同启发式函数h(n)对迷宫寻路求解的速度提升效果。
3.分析A*算法求解不同规模迷宫最短路径问题的性能。
4.提交源程序。
5.总结实验心得体会。
五、思考与问答
问:A*算法如何处理迷宫中的障碍物?
答:在A*算法中,每次扩展节点时,都需要检查其邻居节点是否为障碍物。如果邻居是障碍物,算法就会跳过这个邻居,不将其加入开放列表。障碍物被认为是无法通过的区域,因此它们在路径搜索过程中是不可达的。
问:A*算法保证找到最短路径吗?
答:是的,只要启发式函数是“可接受的”,即不高估从当前节点到目标节点的实际代价,A算法就能保证找到最短路径。对于迷宫问题,常见的启发式函数(如曼哈顿距离、欧几里得距离等)是可接受的,因此A可以找到从起点到终点的最短路径。
问:A*算法的效率如何?如何应对大规模迷宫?
答:A算法的效率取决于迷宫的复杂度和启发式函数的设计。在大规模迷宫中,A算法的时间复杂度和空间复杂度可能很高,因为需要维护开放列表和闭合列表。在这种情况下,可以采取以下优化策略:
- 启发式函数优化:选择合适的启发式函数,如曼哈顿距离、欧几里得距离等,以缩小搜索范围。
- 迭代加深A*(IDA*):通过限制搜索深度,减少内存消耗。
- 跳跃搜索:在某些情况下,通过跳过某些中间步骤,减少计算量。
六、源代码
'''from queue import PriorityQueuedef a_star_search(graph,start,goal):"""使用 A* 搜索算法在图中寻找从起点到终点的路径。参数:graph (Graph): 要搜索的图。start: 搜索的起点。goal: 搜索的终点。返回:tuple: 包含came_from和cost_so_far的元组。came_from (dict): 一个字典,存储了每个节点的前一个节点,用于回溯路径。cost_so_far (dict): 一个字典,存储了到每个节点的当前最小代价。"""# 使用优先级队列 frontier 来存储待探索的节点,优先级由估计的总代价决定frontier = PriorityQueue()# 将起点 start 放入 frontier 中,其优先级为 0frontier.put(start, 0)# 创建 came_from 字典,用于记录每个节点的前一个节点came_from = dict()# 创建 cost_so_far 字典,用于记录到每个节点的当前最小代价cost_so_far = dict()# 将起点 start 的前一个节点设置为 None,并将其代价设置为 0came_from[start] = Nonecost_so_far[start] = 0# 当 frontier 不为空时,循环执行以下操作while not frontier.empty():# 从 frontier 中取出具有最小代价的节点 currentcurrent = frontier.get()# 如果 current 节点等于目标节点 goal,则搜索结束if current == goal:break# 遍历 current 节点的所有邻居节点 nextfor next in graph.neighbors(current):# 计算从起点到 next 节点的新代价 new_costnew_cost = cost_so_far[current] + graph.cost(current, next)# 如果 next 节点没有被访问过,或者通过 current 节点到达 next 节点的代价更小if next not in cost_so_far or new_cost < cost_so_far[next]:# 更新到 next 节点的代价为 new_costcost_so_far[next] = new_cost# 计算从起点到 next 节点的估计总代价 prioritypriority = new_cost + heuristic(goal, next)# 将 next 节点及其估计总代价 priority 放入 frontier 中frontier.put(next, priority)# 将 next 节点的前一个节点设置为 currentcame_from[next] = current# 返回 came_from 和 cost_so_far 字典,用于回溯路径和计算总代价return came_from, cost_so_fardef heuristic(a, b):"""计算两个点 a 和 b 之间的曼哈顿距离。参数:a (tuple): 第一个点的坐标,格式为 (x1, y1)。b (tuple): 第二个点的坐标,格式为 (x2, y2)。返回:int: 两点之间的曼哈顿距离。"""# 解包元组 a 得到 x1 和 y1(x1, y1) = a# 解包元组 b 得到 x2 和 y2(x2, y2) = b# 计算 x 坐标之差的绝对值x_diff = abs(x1 - x2)# 计算 y 坐标之差的绝对值y_diff = abs(y1 - y2)# 返回曼哈顿距离,即 x 和 y 坐标之差的绝对值之和return x_diff + y_diff'''
# A* 算法代码from queue import PriorityQueue
import time
import mathclass MazeGraph:"""迷宫图的表示类,包含迷宫的行列数据,以及获取邻居节点和路径代价的方法。"""def __init__(self, maze):"""初始化 MazeGraph 对象。参数:maze (list of lists): 二维列表,代表迷宫的布局。返回:None"""# 将传入的迷宫赋值给实例变量 self.mazeself.maze = maze# 获取迷宫的行数,并赋值给实例变量 self.rowsself.rows = len(maze)# 获取迷宫的列数,并赋值给实例变量 self.colsself.cols = len(maze[0])def neighbors(self, node):"""返回当前节点的所有邻居节点"""# 获取节点的 x 和 y 坐标x, y = node# 初始化邻居节点列表neighbors = []# 定义四个方向的偏移量:左、右、上、下directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]# 遍历每个方向for dx, dy in directions:# 计算邻居节点的坐标nx, ny = x + dx, y + dy# 检查邻居节点是否在迷宫范围内,并且不是墙壁(值为 0)if 0 <= nx < self.rows and 0 <= ny < self.cols and self.maze[nx][ny] == 0:# 如果是有效邻居节点,则将其添加到列表中neighbors.append((nx, ny))# 返回所有有效的邻居节点return neighborsdef cost(self, from_node, to_node):"""返回从 from_node 到 to_node 的路径代价,这里代价为 1(单位代价)参数:from_node (tuple): 起点坐标。to_node (tuple): 终点坐标。解释:行走每一个格子的代价为 1(单位代价)"""return 1def a_star_search(graph, start, goal):"""使用 A* 搜索算法在图中寻找从起点到终点的路径。参数:graph (MazeGraph): 迷宫图对象。start: 搜索的起点。goal: 搜索的终点。返回:tuple: 包含came_from和cost_so_far的元组。came_from (dict): 一个字典,存储了每个节点的前一个节点,用于回溯路径。cost_so_far (dict): 一个字典,存储了到每个节点的当前最小代价。"""# 使用优先级队列 frontier 来存储待探索的节点,优先级由估计的总代价决定frontier = PriorityQueue()frontier.put((0, start)) # 将起点 start 放入 frontier 中,其优先级为 0# 创建 came_from 字典,用于记录每个节点的前一个节点came_from = dict()# 创建 cost_so_far 字典,用于记录到每个节点的当前最小代价cost_so_far = dict()# 将起点 start 的前一个节点设置为 None,并将其代价设置为 0came_from[start] = Nonecost_so_far[start] = 0# 记录扩展节点数和生成节点数expand_count = 0generate_count = 0# 记录起始时间start_time = time.time()while not frontier.empty():current_cost, current = frontier.get()expand_count += 1 # 扩展节点数加一# 如果 current 节点等于目标节点 goal,则搜索结束if current == goal:break# 遍历 current 节点的所有邻居节点 nextfor next in graph.neighbors(current):# 计算从起点到 next 节点的新代价 new_costnew_cost = cost_so_far[current] + graph.cost(current, next)# 如果 next 节点没有被访问过,或者通过 current 节点到达 next 节点的代价更小if next not in cost_so_far or new_cost < cost_so_far[next]:# 更新到 next 节点的代价为 new_costcost_so_far[next] = new_cost# 在这选择估价曼哈顿算法# heuristic = heuristic_manhattan(next, goal)# 在这选择估价欧几里得算法heuristic = heuristic_euclidean(next, goal)priority = new_cost + heuristic # fn = gn + hn# 将 next 节点及其估计总代价 priority 放入 frontier 中frontier.put((priority, next))# 将当前结点更新在came_from中came_from[next] = currentgenerate_count += 1 # 生成节点数# 计算运行时间end_time = time.time()execution_time = end_time - start_timereturn came_from, cost_so_far, expand_count, generate_count, execution_timedef heuristic_manhattan(a, b):"""计算两个点 a 和 b 之间的曼哈顿距离。参数:a (tuple): 第一个点的坐标,格式为 (x1, y1)。b (tuple): 第二个点的坐标,格式为 (x2, y2)。返回:int: 两点之间的曼哈顿距离。"""(x1, y1) = a(x2, y2) = breturn abs(x1 - x2) + abs(y1 - y2)def heuristic_euclidean(a, b):"""计算两个点 a 和 b 之间的欧几里得距离。参数:a (tuple): 第一个点的坐标,格式为 (x1, y1)。b (tuple): 第二个点的坐标,格式为 (x2, y2)。返回:float: 两点之间的欧几里得距离。"""(x1, y1) = a(x2, y2) = breturn math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)def reconstruct_path(came_from, start, goal):"""从 came_from 字典中回溯路径。参数:came_from (dict): 包含每个节点的前一个节点的字典。start: 起点。goal: 终点。返回:list: 从 start 到 goal 的路径。"""# 初始化路径列表path = []# 将当前节点设置为终点current = goal# 当当前节点不等于起点时,循环执行以下操作while current!= start:# 将当前节点添加到路径中path.append(current)# 从 came_from 字典中获取当前节点的前一个节点,并更新当前节点current = came_from[current]# 将起点添加到路径中path.append(start)# 反转路径,使其从起点到终点path.reverse()# 返回重构的路径return pathdef print_map(maze):"""打印迷宫地图。参数:maze (list of lists): 二维列表,代表迷宫的布局。返回:None"""# 遍历迷宫的每一行for row in maze:# 使用 join 方法将每一行的单元格值连接成一个字符串,单元格之间用空格分隔print(" ".join(str(cell) for cell in row))def mark_path_on_map(maze, path):"""在迷宫中标记路径,使用字母 A, B, C 等。参数:maze (list of lists): 二维列表,代表迷宫的布局。path (list of tuples): 路径上的点的列表,每个点是一个 (x, y) 坐标对。返回:list of lists: 修改后的迷宫地图,其中路径上的点被标记为字母。"""# 初始化字母为 'A'letter = 'A'# 遍历路径上的每个点for (x, y) in path:# 包括起点和终点maze[x][y] = letter# 更新字母到下一个letter = chr(ord(letter) + 1)# 返回标记路径后的迷宫地图return mazeimport randomdef generate_maze(rows, cols, obstacle_probability=0.3):# 初始化迷宫地图,所有单元格默认为可通行(0)maze = [[0 for _ in range(cols)] for _ in range(rows)]# 定义起点和终点start = (0, 0)end = (rows - 1, cols - 1)# 使用深度优先搜索生成从起点到终点的路径def dfs(x, y):stack = [(x, y)]visited = set()visited.add((x, y))while stack:cx, cy = stack.pop()# 随机打乱邻居顺序neighbors = [(cx + dx, cy + dy) for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]]random.shuffle(neighbors)for nx, ny in neighbors:if 0 <= nx < rows and 0 <= ny < cols and (nx, ny) not in visited:if (nx, ny) == end or dfs(nx, ny):maze[cx][cy] = 0maze[nx][ny] = 0return Truereturn False# 生成路径dfs(*start)# 随机生成障碍物for i in range(rows):for j in range(cols):if (i, j) != start and (i, j) != end and maze[i][j] == 0:if random.random() < obstacle_probability:maze[i][j] = 1return mazedef main():''' # 定义迷宫地图,0 表示可通行,1 表示障碍物maze = [[0, 0, 0, 0, 0],[1, 0, 1, 0, 1],[0, 0, 1, 1, 1],[0, 1, 0, 0, 0],[0, 0, 0, 1, 0]]''''''# 测试样例1:无解maze = [[0, 0, 0, 0, 0],[1, 1, 1, 0, 1],[0, 1, 1, 1, 0],[0, 1, 0, 1, 0],[0, 0, 0, 1, 0]
]
''''''# 测试样例2:maze = [[0, 0, 0, 0, 0],[0, 0, 0, 0, 0],[0, 0, 0, 0, 0],[0, 0, 0, 0, 0],[0, 0, 0, 0, 0]
]
''''''# 测试样例3:maze = [[0, 1, 0, 0, 0],[0, 1, 0, 1, 0],[0, 1, 0, 1, 0],[0, 1, 0, 1, 0],[0, 0, 0, 0, 0]
]'''maze = generate_maze(10, 10)# 设置起点和终点坐标start = (0, 0) # 起点 (1,1)end = (9, 9) # 终点 (5,5)# 创建迷宫图对象graph = MazeGraph(maze)# 调用 A* 搜索算法,返回 came_from 字典和 cost_so_far 字典came_from, cost_so_far, expand_count, generate_count, execution_time = a_star_search(graph, start, end)# 如果找到路径if end in came_from:# 重构路径path = reconstruct_path(came_from, start, end)# 打印最短路径print("最短路径: ", [f"({p[0] + 1},{p[1] + 1})" for p in path])# 在迷宫地图上标记路径marked_maze = mark_path_on_map([row[:] for row in maze], path)# 打印带路径标记的迷宫地图print("迷宫地图带路径标记:")print_map(marked_maze)# 输出扩展节点数、生成节点数、算法运行时间print(f"扩展节点数: {expand_count}")print(f"生成节点数: {generate_count}")print(f"算法运行时间: {execution_time:.30f} 秒")else:# 如果没有找到路径,打印提示信息print("没有找到路径")if __name__ == "__main__":main()