逐行讲解python实现A*路径规划

news2024/11/26 11:38:34

目录

  • 搜索步骤
  • 关键点
    • 开集合和闭集合
      • 复杂度优化
    • 代价
      • 父节点替换
    • 距离
    • 地图设置
  • 完整代码
  • 备注

搜索步骤

A*路径规划是一种广度优先搜索算法,需要在栅格地图上进行搜索。其主要搜索步骤如下:

  1. 得到栅格地图,确定起点和终点位置;
  2. 计算起点周围点的代价,放到开集合中,可以周围4个点(四连通),也可以8个点(八连通);
  3. 开集合中提取代价最小的点,将其放到闭集合中,并计算这个点周围点的代价,此时要判断周围点是否已经在开集合闭集合中,并做特殊处理(后面详细说明),若不在两个集合中,则将其放到开集合中;
  4. 循环执行步骤3,直到弹出的点是终点。

关键点

开集合和闭集合

  • 开集合:待走的位置。相当于候选点集合,每到一个点,将周围点放入候选集合中,往后有可能往候选点去走。
  • 闭集合:已经走过的位置,不会再被考虑。

复杂度优化

  1. 上述第三步中 “从开集合中提取代价最小的点” ,常规方法会循环遍历开集合,找到代价值最小的索引来提取,但循环结构太耗费时间。
    改进:用小顶堆来解决(一种特殊的数据结构,若不了解可自行百度),小顶堆可以一直保持索引0的点为代价最小的点,它的插入和自更新时间复杂度为O(nlogn),相对循环更快。
    import heapq
    
    # 创建一个列表用来存储
    open_set = [15]
    # 往列表中压入一个新的数,并自行调整小顶堆的顺序
    heapq.heappush(open_set, 10)
    heapq.heappush(open_set, 20)
    heapq.heappush(open_set, 30)
    heapq.heappush(open_set, 14)
    # 弹出堆顶,永远是最小的数
    a = heapq.heappop(open_set)
    print(a)
    
  2. 上述第三步中 “判断周围点是否已经在开集合或闭集合中”,常规方法会遍历开集合或闭集合,判断该点是否在其中,同样循环太耗费时间。
    改进:在创建地图的时候,地图上的每一个点都是一个对象,对应的类中新创建一个属性来记录当前点是否在开集合或闭集合中。用空间换时间,每次判断可直接索引查找,复杂度降维O(1)。
    # 地图上的每一个点都是一个Point对象,用于记录该点的类别、代价等信息
    class Point:
        def __init__(self, x=0, y=0):
            self.x = x
            self.y = y
            self.val = 0  # 0代表可通行,1代表障碍物
            self.cost_g = 0
            self.cost_h = 0
            self.cost_f = 0
            self.parent = None  # 父节点
            self.is_open = 0  # 0:不在开集合  1:在开集合  -1:在闭集合
    
        # 用于heapq小顶堆的比较
        def __lt__(self, other):
            return self.cost_f < other.cost_f
    
    注:heapq操作的列表里的元素默认都是数值,可以直接比较大小的,但现在存储的是类对象,所以需要额外写__lt__方法,用于比较大小。
    注:父节点,把当前点设为周围扩散点的父节点,用于标识该点来源于哪里,用于回溯完整路径。

代价

代价,即走到某个位置需要花费的成本。代价越小,这条路径就越好。

每个点的代价分为3种:

  1. g 代价:当前点到起点的代价,等于当前点与父节点之间的距离+父节点的 g 代价;
  2. h 代价:当前点到终点的代价,等于当前点与终点之间的距离;
  3. f 代价:当前点总代价,等于g 代价 + h 代价;

父节点替换

当前点在扩散时需要将周围的8个点都放入开集合中(假设8连通),这也意味着某一个点可能由周围8个点扩散过来。

那这个点到底归谁呢(决定它由谁扩散而来),当然是谁代价小就选谁。

对于一个点,它的h值是不变的,永远是与终点的距离,但是它的g值是不一样的,g值跟走过的路径有关,走的越长代价越大,所以选择走的最少的点作为自己的父节点。

所以具体实现步骤如下:

  1. 对于扩散到的点,首先判断是否有效(越出边界或障碍物),以及是否已经在闭集合中(已经走过的就不用再走一遍了)
  2. 若通过上一步,先将来源点(谁扩散来的)作为父节点,去计算g值(g值需要依靠父节点g值计算)
  3. 如果这个点不在开集合中,都好说,直接加入开集合就好
  4. 如果在开集合中,说明之前有别的点扩散到这过,比较这两个来源点到这的g值大小,选择小的那个作为自己的父节点。

下图为增加这个操作和不增加的对比,左图如果不增加,它会一直保留第一次扩散过来的点作为自己的父节点,右图切换父节点后就会选择更近的路。
在这里插入图片描述

该部分代码如下

    def diffusion_point(self, x, y, parent):
        # 无效点或者在闭集合中,跳过
        if not self.is_valid_point(x, y) or self.map.map[x][y].is_open == -1:
            return
        p = self.map.map[x][y]
        pre_parent = p.parent
        p.parent = parent
        # 先计算出当前点的总代价
        cost_g = self.g_cost(p)
        cost_h = self.h_cost(p)
        cost_f = cost_g + cost_h
        # 如果在开集合中,判断当前点和开集合中哪个点代价小,换成小的,相同x,y的点h值相同,g值不一定相同
        if p.is_open == 1:
            if cost_f < p.cost_f:
                # 如果从当前parent遍历过来的代价更小,替换成当前的代价和父节点
                p.cost_g, p.cost_h, p.cost_f = cost_g, cost_h, cost_f
            else:
                # 如果从之前父节点遍历过来的代价更小,保持之前的代价和父节点
                p.parent = pre_parent
        else:
            # 如果不在开集合中,说明之间没遍历过,直接加到开集合里就好
            p.cost_g, p.cost_h, p.cost_f = cost_g, cost_h, cost_f
            heapq.heappush(self.open_set, p)
            p.is_open = 1

距离

若使用不同距离,可在完整代码中替换对应部分

  1. 欧式距离(欧几里得距离):两个点之间的直线距离

        def g_cost(self, p):
            '''
            计算 g 代价,当前点与父节点的距离 + 父节点的 g 代价(欧氏距离)
            :param p: 当前扩散的节点
            :return: p 的 g 代价
            '''
            x_dis = abs(p.parent.x - p.x)
            y_dis = abs(p.parent.y - p.y)
            return np.sqrt(x_dis ** 2 + y_dis ** 2) + p.parent.cost_g
    
        def h_cost(self, p):
            '''
            计算 h 代价,当前点与终点之间的距离(欧氏距离)
            :param p: 当前扩散的节点
            :return: p 的 h 代价
            '''
            x_dis = abs(self.end_point.x - p.x)
            y_dis = abs(self.end_point.y - p.y)
            return np.sqrt(x_dis ** 2 + y_dis ** 2)
    
  2. 曼哈顿距离:计算x轴和y轴之差的绝对值,适合在四连通时使用(只上下左右走),计算简单

        def g_cost(self, p):
            '''
            计算 g 代价,当前点与父节点的距离 + 父节点的 g 代价(曼哈顿距离)
            :param p: 当前扩散的节点
            :return: p 的 g 代价
            '''
            x_dis = abs(p.parent.x - p.x)
            y_dis = abs(p.parent.y - p.y)
            return x_dis + y_dis + p.parent.cost_g
    
        def h_cost(self, p):
            '''
            计算 h 代价,当前点与终点之间的距离(曼哈顿距离)
            :param p: 当前扩散的节点
            :return: p 的 h 代价
            '''
            x_dis = abs(self.end_point.x - p.x)
            y_dis = abs(self.end_point.y - p.y)
            return x_dis + y_dis
    
  3. 对角距离(待续)

地图设置

创建一个地图类,具体的地图用二维列表存储,每个元素都是Point对象。set_obstacle方法可手动设置障碍物。

class Map:
    def __init__(self, map_size):
        self.map_size = map_size
        self.width = map_size[0]
        self.height = map_size[1]
        self.map = [[Point(x, y) for y in range(self.map_size[1])] for x in range(self.map_size[0])]

    # 手动设置障碍物,可多次调用设置地图
    # 由于地图方向不同,这里的topleft并不总是左上角,topleft代表x和y全都较小的点
    def set_obstacle(self, topleft, width, height):
        for x in range(topleft[0], topleft[0] + width):
            for y in range(topleft[1], topleft[1] + height):
                self.map[x][y].val = 1

完整代码

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Author      : Cao Zejun
# @Time        : 2024/4/7 17:36
# @File        : Astar_blog.py
# @Software    : Pycharm
# @description : 用于CSDN上的Astar算法演示

import time
import numpy as np
from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
import heapq


# 地图上的每一个点都是一个Point对象,用于记录该点的类别、代价等信息
class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        self.val = 0  # 0代表可通行,1代表障碍物
        self.cost_g = 0  # 三个代价
        self.cost_h = 0
        self.cost_f = 0
        self.parent = None  # 父节点
        self.is_open = 0  # 0:不在开集合  1:在开集合  -1:在闭集合

    # 用于heapq小顶堆的比较
    def __lt__(self, other):
        return self.cost_f < other.cost_f


class Map:
    def __init__(self, map_size):
        self.map_size = map_size
        self.width = map_size[0]
        self.height = map_size[1]
        self.map = [[Point(x, y) for y in range(self.map_size[1])] for x in range(self.map_size[0])]

    # 手动设置障碍物,可多次调用设置地图
    # 由于地图方向不同,这里的topleft并不总是左上角,topleft代表x和y全都较小的点
    def set_obstacle(self, topleft, width, height):
        for x in range(topleft[0], topleft[0] + width):
            for y in range(topleft[1], topleft[1] + height):
                self.map[x][y].val = 1


class AStar:
    def __init__(self, map, start_point, end_point, connect_num=8, ax=None, print_diffusion_point=False):
        self.map: Map = map
        self.start_point = start_point
        self.end_point = end_point
        self.open_set = [self.start_point]  # 开集合,先放入起点,从起点开始遍历

        self.start_point.is_open = 1  #
        self.connect_num = connect_num  # 连通数,目前支持4连通或8连通
        self.diffuse_dir = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (1, -1), (-1, -1)]  # 遍历的8个方向,只需取出元组,加到x和y上就可以

    def g_cost(self, p):
        '''
        计算 g 代价,当前点与父节点的距离 + 父节点的 g 代价(欧氏距离)
        :param p: 当前扩散的节点
        :return: p 的 g 代价
        '''
        x_dis = abs(p.parent.x - p.x)
        y_dis = abs(p.parent.y - p.y)
        return np.sqrt(x_dis ** 2 + y_dis ** 2) + p.parent.cost_g

    def h_cost(self, p):
        '''
        计算 h 代价,当前点与终点之间的距离(欧氏距离)
        :param p: 当前扩散的节点
        :return: p 的 h 代价
        '''
        x_dis = abs(self.end_point.x - p.x)
        y_dis = abs(self.end_point.y - p.y)
        return np.sqrt(x_dis ** 2 + y_dis ** 2)

    def is_valid_point(self, x, y):
        # 无效点:超出地图边界或为障碍物
        if x < 0 or x >= self.map.width:
            return False
        if y < 0 or y >= self.map.height:
            return False
        return self.map.map[x][y].val == 0

    def search(self):
        self.start_time = time.time()  # 用于记录搜索时间
        p = self.start_point
        # p 为当前遍历节点,等于终点停下
        while not (p == self.end_point):
            # 弹出代价最小的开集合点,若开集合为空,说明没有路径
            try:
                p = heapq.heappop(self.open_set)
            except:
                raise 'No path found, algorithm failed!!!'

            p.is_open = -1

            # 遍历周围点
            for i in range(self.connect_num):
                dir_x, dir_y = self.diffuse_dir[i]
                self.diffusion_point(p.x + dir_x, p.y + dir_y, p)
        return self.build_path(p)  # p = self.end_point

    def diffusion_point(self, x, y, parent):
        # 无效点或者在闭集合中,跳过
        if not self.is_valid_point(x, y) or self.map.map[x][y].is_open == -1:
            return
        p = self.map.map[x][y]
        pre_parent = p.parent
        p.parent = parent
        # 先计算出当前点的总代价
        cost_g = self.g_cost(p)
        cost_h = self.h_cost(p)
        cost_f = cost_g + cost_h
        # 如果在开集合中,判断当前点和开集合中哪个点代价小,换成小的,相同x,y的点h值相同,g值不一定相同
        if p.is_open == 1:
            if cost_f < p.cost_f:
                # 如果从当前parent遍历过来的代价更小,替换成当前的代价和父节点
                p.cost_g, p.cost_h, p.cost_f = cost_g, cost_h, cost_f
            else:
                # 如果从之前父节点遍历过来的代价更小,保持之前的代价和父节点
                p.parent = pre_parent
        else:
            # 如果不在开集合中,说明之间没遍历过,直接加到开集合里就好
            p.cost_g, p.cost_h, p.cost_f = cost_g, cost_h, cost_f
            heapq.heappush(self.open_set, p)
            p.is_open = 1

    def build_path(self, p):
        print('search time: ', time.time() - self.start_time, ' seconds')
        # 回溯完整路径
        path = []
        while p != self.start_point:
            path.append(p)
            p = p.parent
        print('search time: ', time.time() - self.start_time, ' seconds')
        # 打印开集合、闭集合的数量
        print('open set count: ', len(self.open_set))
        close_count = 0
        for x in range(self.map.width):
            for y in range(self.map.height):
                close_count += 1 if self.map.map[x][y] == -1 else 0
        print('close set count: ', close_count)
        print('total count: ', close_count + len(self.open_set))
        # path = path[::-1]  # path为终点到起点的顺序,可使用该语句翻转
        return path

if __name__ == '__main__':
    map = Map((50, 50))

    # 用于显示plt图
    ax = plt.gca()
    ax.set_xlim([0, map.width])
    ax.set_ylim([0, map.height])
    plt.tight_layout()

    # 设置障碍物
    map.set_obstacle([10, 27], 20, 4)
    # 将障碍物显示到plt上
    ax.add_patch(Rectangle([10, 27], width=20, height=4, color='gray'))

    # 设置起始点和终点,并创建astar对象
    start_point = map.map[5][5]
    end_point = map.map[20][40]
    astar = AStar(map, start_point, end_point)
    path = astar.search()

    # 搜索之后打印,功能拓展时不影响搜索时间
    # 打印开集合点和闭集合点,可视化扩散点数量
    for x in range(map.width):
        for y in range(map.height):
            if map.map[x][y].is_open == -1:
                ax.add_patch(Rectangle([x, y], width=1, height=1, color='green'))
            if map.map[x][y].is_open == 1:
                ax.add_patch(Rectangle([x, y], width=1, height=1, color='blue'))
    # 可视化起点到终点完整路径
    for p in path:
        ax.add_patch(Rectangle([p.x, p.y], width=1, height=1, color='red'))

    # plt.savefig('./output/tmp.jpg')  # 可选择将其保存为本地图片
    plt.show()

备注

若有错误,可在评论区指出,我会及时修改

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1580088.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue项目打包配置生产环境去掉console.log语句的方法

一、Vue2项目 使用webpack内置的 terser 工具&#xff0c;在vue.config.js文件加上相应的配置即可。 二、Vue3项目 同样是使用 terser 工具&#xff0c;不过vite没有内置terser&#xff0c;需要手动安装依赖 安装完后在vite.config.js文件加上相应的配置即可。 2024-4-9

深挖抖快近2000个品类,我们发现了10万亿“她经济”的新商机!

在这个数字化时代&#xff0c;女性消费力量正以前所未有的速度崛起。根据埃森哲数据显示&#xff0c;我国现有近4亿20岁-60岁的女性消费者&#xff0c;其每年所掌握的消费支出高达10万亿元&#xff01; 面对庞大的“她经济”市场&#xff0c;专属于女性的三八妇女节&#xff0c…

【Python系列】读取 Excel 第一列数据并赋值到指定列

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

米多论文方便吗 #经验分享#经验分享

米多论文是一款专业的论文写作、查重和降重工具&#xff0c;被广泛认可为高效、靠谱、方便的软件。无论是学生、科研人员还是教师&#xff0c;都可以从中受益匪浅。 首先&#xff0c;米多论文拥有强大的查重功能&#xff0c;可以帮助用户快速检测论文中的抄袭内容&#xff0c;提…

openGauss 5.0 单点企业版部署_Centos7_x86(上)

背景 通过openGauss提供的脚本安装时&#xff0c;只允许在单台物理机部署一个数据库系统。如果您需要在单台物理机部署多个数据库系统&#xff0c;建议您通过命令行安装&#xff0c;不需要通过openGauss提供的安装脚本执行安装。 本文档环境&#xff1a;CentOS7.9 x86_64 4G1…

人社大赛算法赛题解题思路分享+季军+三马一曹团队

团队成员介绍: 梅鵾 上海交通大学 众安科技 算法工程师 吴栋梁 复旦大学 众安科技 算法工程师 李玉娇 复旦大学 众安科技 算法工程师 一、赛题背景分析及理解 本赛题提供了部分地区2016年度的医疗保险就医结…

最新剧透前沿信息GPT-5或将今年发布

GPT2 很糟糕 &#xff0c;GPT3 很糟糕 &#xff0c;GPT4 可以 &#xff0c;但 GPT5 会很好。 PS:GPT2 很糟糕,3 很糟糕,4 可以,5 很可以。 如果想升级GPT4玩玩&#xff0c;地址 今年发布的具有推理功能的 GPT5不断发展&#xff0c;就像 iPhone 一样 Sam Altman 于 17 日&am…

SD-WAN在金融行业的重要性

金融行业的数字化转型已成为当今的主要趋势&#xff0c;而软件定义广域网&#xff08;SD-WAN&#xff09;作为金融机构网络架构的新宠&#xff0c;其地位日益凸显。随着金融业务的日益复杂化和网络连接需求的不断增长&#xff0c;SD-WAN的优势愈发显著。本文将深入探讨SD-WAN在…

鲸鱼优化算法(Whale Optimization Algorithm)

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 算法背景 鲸鱼优化算法&#xff08;Whale Optimization Algorithm, WOA&#xff09;是一种模拟鲸鱼捕食行为的优化算法。想象一下&#xff0c;你…

java中使用雪花算法(Snowflake)为分布式系统生成全局唯一ID

&#xff08;全局唯一ID的解决方案有很多种&#xff0c;这里主要是介绍和学习Snowflake算法&#xff09; 什么是雪花算法&#xff08;Snowflake&#xff09; 雪花算法&#xff08;Snowflake Algorithm&#xff09;是由Twitter公司在2010年左右提出的一种分布式ID生成算法&…

参加2023 甲骨文圆桌会议

2023年10月13日&#xff0c;我参加了2023 甲骨文圆桌会议&#xff0c;并做了《Oracle高可用架构的最佳实践》主题演讲。 照片为当时活动现场&#xff1a;

中霖教育:2024年注册计量师考试报名即将开始!

2024年注册计量师考试报名即将开始&#xff0c;时间定于4月中旬左右。对于考生来说&#xff0c;以下需要注意&#xff1a; 1、报名 考生需要在人事考试网站进行用户信息注册&#xff0c;在成功注册后&#xff0c;才能进一步进行报名操作。注册信息包括用户基本信息&#xff1…

【案例分享】如何通过甘特图管理项目进度?

我将通过一个实际案例来具体说明我是如何通过甘特图来管理项目进度的。 案例背景&#xff1a; 我负责过一个软件开发项目&#xff1a;一款在线学习APP。项目团队包括项目经理、开发人员、测试人员、UI设计师等多个角色&#xff0c;预计项目周期为6个月。 案例实施过程&…

Three.js--》实现2D转3D的元素周期表

今天简单实现一个three.js的小Demo&#xff0c;加强自己对three知识的掌握与学习&#xff0c;只有在项目中才能灵活将所学知识运用起来&#xff0c;话不多说直接开始。 目录 项目搭建 平铺元素周期表 螺旋元素周期表 网格元素周期表 球状元素周期表 加底部交互按钮 项目…

C语言操作符详解(二)

一、位操作符 & 按位与 | 按位或 ^ 按位异或 ~ 按位取反 注意&#xff1a;它们的操作数必须是整数。 下面的码我都只取了后八位 1.1、按位与 使用补码进行按位与 规则:对应二进制位有0就是0,两个同时为1才为1. 1.2、按位或 使用补码进行按位或 规则:对应二进…

Windows内核是什么,如何保障内核安全

Windows操作系统发展到如今已有三十余年&#xff0c;是目前在全球范围内广泛使用的操作系统。Windows内核是操作系统的核心部分&#xff0c;内核包括了HAL(硬件抽象层)&#xff0c;设备驱动&#xff0c;微内核&#xff0c;各种管理设备&#xff0c;管理层以及系统服务界面&…

【优选算法专栏】专题十六:BFS解决最短路问题(二)

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

OpenHarmony实战:瑞芯微RK3566移植案例(下)

OpenHarmony实战&#xff1a;瑞芯微RK3566移植案例&#xff08;下&#xff09; OpenHarmony实战&#xff1a;瑞芯微RK3566移植案例&#xff08;中&#xff09; WIFI 整改思路及实现流程 整改思路 接下来熟悉HCS文件的格式以及"HDF WIFI”核心驱动框架的代码启动初始化…

Java入门基础知识第七课(超基础,超详细)——数组

前面二白讲了选择结构和循环结构&#xff0c;动手的同学会发现已经有了一定的难度&#xff0c;后面二白会专门收集一些经典的题目&#xff0c;训练多了才能让记忆更加深刻&#xff0c;这次咱们讲一下数组。 一、数组的定义 什么是数组呢&#xff0c;我们都知道变量是存储数据的…