启发式搜索A-Star算法【附代码】

A*算法是对Best-First算法的一种改进,核心思想是广搜,利用open表和close表对节点进行剪枝,同时利用启发式测度来选择最优的扩展节点。

A*算法在满足一定条件下找到的解必然是最优解。

最短路得到最优解条件:A*算法的启发式函数h如果小于等于真实值n的话,那么算法是能得到最优解的,若h大于等于真实值n,那么就不能保证得到最优解。

主要数据结构:

opened_table:采用优先队列实现的小顶堆,用于存放待扩展结点,同时利用F值作为排序指标;
closed_table:采用set(红黑树)实现,便于快速查找格是否存在于closed_table中;
启发式函数:采用曼哈顿距离作为启发式函数,即当前节点与终点的水平、竖直相差方格的数目之和;

问题描述:

精灵王子Haposola一大早从城堡中起床,要去沙漠中河流消失的地方寻找失落的宝藏。长路漫漫,处处险恶,精灵王子要尽快到达目标地点。以下图为输入,寻找地图上给出的起点和终点间的最小代价路径。规则和条件如下。
(1) 每一步都落在方格中,而不是横竖线的交叉点。
(2) 灰色格子表示障碍,无法通行。
(3) 在每个格子处,若无障碍,下一步可以达到八个相邻的格子,并且只可以到达无障碍的相邻格子。其中,向上、下、左、右四个方向移动的代价为1,向四个斜角方向移动的代价为√2。
(4) 在一些特殊格子上行走要花费额外的地形代价。比如,黄色格子代表沙漠, 经过它的代价为4;蓝色格子代表溪流,经过它的代价为2;白色格子为普通地形,经过它的代价为0。
(5) 经过一条路径总的代价为移动代价+地形代价。其中移动代价是路径上所做的所有移动的代价的总和;地形代价为路径上除起点外所有格子的地形代价的总和。

地图

启发式函数:

H = G * 5 + F
其中G代表已走过的真实距离;F代表当前点到目标点的曼哈顿距离;

为什么要给G乘上5?

因为任务的要求是要走过路程最短,因此我们的启发式测度更加的看重实际走过的距离,而不是通过曼哈顿距离估计的路程。
所以给G赋予更大的权值,保证我们得到的解是最优解,即最短路程。【就是一个加权的思路】

地图处理:

值得一提的是,由于我们的地图是图片的形式给出的,那么我们需要将图片转化为数组读入程序。此处借助Python的第三方库PIL实现地图输入的采样、以及实验结果的生成。
通过均匀采样将地图图片输入转化为字符数组的函数。
同时对于预测得到的路径,我们也需要根据我们的结果,再生成结果地图,详见代码。

算法步骤:

1.  首先将给定图2输入程序;
2.  通过均匀采样,将其转化为字符数组,y代表沙漠,g代表障碍,w代表普通道路,b代表溪流;
3.  输入起点和终点,同时将起点、终点、字符数组存储的地图输入算法;
4.  把起始格添加到opened表;
5.  如果开启列表不为空,重复如下的工作:
    a)  寻找开启列表中F值最低的格子,作为当前格;
        对于8个方向,寻找当前格的活节点:
          i.    如果当前点是终点,则返回回溯的路径;
          ii.   如果活节点超出地图边界,则跳过;
          iii.  如果活节点位于closed表中,则跳过;
          iv.   如果它不在开opened表中,则计算其F,G,H值,然后添加进opened表;
          v.    如果它在opened表中,则判断opened表中格的G值是否大于当前活节点,如果大于,则说明当前活节点更优,代价更小,那么将opened表中这一格的父节点改成当前格,同时重新计算G,F值;
    b)  把当前格加入closed表;
6.  将路径结果与地图生成图片,方格像素为30*30px,边界像素为1px;

主要代码:

地图方格定义:

class AreaBlock(object):
    def __init__(self, father, kind, g, row, col):
        self.father = father
        self.kind = kind
        self.coordinate_row = row
        self.coordinate_col = col
        self.g = g
        self.f = 0.0

    def get_coordinate(self):
        return self.coordinate_row, self.coordinate_col

    def __lt__(self, other):
        """
        operator <
        :param other:
        :return:
        """
        return self.f < other.f

地图处理:

def process_map():
    """
    将图片变化为字符矩阵
    :return:
    """
    game_map = [[0] * 41]
    im = Image.open('map.png')
    pix = im.load()
    width = im.size[0]
    height = im.size[1]
    """
    取样点坐标
    """
    start_x = int(math.floor(width / 40)) - 10
    x_step = int(round(width / 40))
    start_y = int(math.floor(height / 20)) - 10
    y_step = int(round(height / 20))
    for i in range(20):
        row = [0]
        y = start_y + i * y_step
        for j in range(40):
            x = start_x + j * x_step
            if pix[x, y] == GRAY:
                row.append('g')
            elif pix[x, y] == BLUE:
                row.append('b')
            elif pix[x, y] == YELLOW:
                row.append('y')
            elif pix[x, y] == WHITE:
                row.append('w')
            else:
                print("error")
        game_map.append(row)
    return game_map

A*寻路:

def search(game_map, start_pos, end_pos):
    """
    寻路算法
    :param game_map:
    :param start_pos:
    :param end_pos:
    :return:
    """
    # 确定边界
    max_row = len(game_map) - 1
    max_col = len(game_map[0]) - 1
    # 创建open表, close表
    opened_table = PriorityQueue()
    closed_table = set()
    # 初始化open表
    opened_table.put(start_pos)
    while not opened_table.empty():
        current_area_block = opened_table.get()
        for direct in direction:
            col_index = current_area_block.coordinate_col + direct[0]
            row_index = current_area_block.coordinate_row + direct[1]
            # 边界检测, 如果它不可通过或者已经在关闭列表中, 跳过
            if (row_index, col_index) in closed_table or bound(row_index, col_index, max_row=max_row, max_col=max_col):
                continue
            if game_map[row_index][col_index] == 'g':
                continue
            # 计算移动代价
            move_cost = 1
            if math.fabs(direct[0]) == 1 and math.fabs(direct[1]) == 1:
                move_cost *= math.sqrt(2)
            # 计算地形代价
            land_cost = get_cost(game_map[row_index][col_index])
            # 创建block
            next_area = AreaBlock(father=current_area_block,
                                  kind=game_map[row_index][col_index],
                                  g=current_area_block.g + move_cost + land_cost,  # 地形代价 + 移动代价
                                  row=row_index,
                                  col=col_index)
            # 如果当前节点是目标节点,则找到路径
            if col_index == end_pos.coordinate_col and row_index == end_pos.coordinate_row:
                # 向父节点开始回溯,输出路径
                return find_path(next_area)
            # 检查该点是否在open表中
            result = None
            tmp_queue = []
            while not opened_table.empty():
                tmp_block = opened_table.get()
                if tmp_block.get_coordinate() == next_area.get_coordinate():
                    result = tmp_block
                    break
                tmp_queue.append(tmp_block)
            while len(tmp_queue) != 0:
                opened_table.put(tmp_queue.pop())
            opened_table.task_done()
            # 检查该点是否在open表中, 如果不在则加入, 同时计算启发值 F = G + H
            next_area.f = heuristic(next_area, end_pos)
            if result is None:
                opened_table.put(next_area)
            else:
                """
                用G值为参考检查新的路径是否更好。更低的G值意味着更好的路径。
                """
                if next_area.g < result.g:
                    opened_table.put(next_area)
                else:
                    opened_table.put(result)
        closed_table.add(current_area_block.get_coordinate())
    return None

采用栈实现路径的回溯:

def find_path(target_area_block):
    """
    回溯寻找路径
    :type target_area_block: AreaBlock
    :param target_area_block: 
    :return: 
    """
    print(target_area_block.g)
    # 用栈存储路径
    stack = [target_area_block.get_coordinate()]
    father_area_block = target_area_block.father
    while father_area_block is not None:
        stack.append(father_area_block.get_coordinate())
        father_area_block = father_area_block.father
    return stack

根据回溯的路径生成结果图片:

def save_result(game_map, path):
    """
    将结果保存为图片
    :param game_map:
    :param path:
    :return:
    """
    pix = convert_array_to_pixel(game_map, path)
    array = np.asarray(pix, dtype=np.uint8)
    image = Image.fromarray(array, 'RGBA')
    image.save("result.png")

def convert_array_to_pixel(arr, path):
    """
    将字符数组转化为像素
    :param arr:
    :param path:
    :return:
    """
    width = 20
    height = 20
    pixels = []
    for i in range(1, len(arr)):
        line = []
        for j in range(1, len(arr[i])):
            if arr[i][j] == 'g':
                line.append(GRAY)
            elif arr[i][j] == 'b':
                line.append(BLUE)
            elif arr[i][j] == 'y':
                line.append(YELLOW)
            elif arr[i][j] == 'w':
                line.append(WHITE)
        pixels.append(line)
    # 绘制路径
    for point in path:
        x, y = point
        pixels[x - 1][y - 1] = RED
    # 处理分辨率,放大
    result = [[BORDER] * ((len(pixels[0])) * width + (len(pixels[0]) + 1))]
    for i in range(len(pixels)):
        line = [BORDER]
        for j in range(len(pixels[i])):
            line.extend([pixels[i][j]] * width)
            line.append(BORDER)
        for j in range(height):
            result.append(line)
        result.append([BORDER] * len(line))
    del pixels
    return result

实验结果:

当起点为(11, 5)时,终点为(1, 36),cost=67.0416结果为(红色为路径):



当起点为(5, 3)时,终点为(20, 9),cost=22.0711结果为(红色为路径):



当起点为(20, 1)时,终点为(1, 15),cost=27.7279结果为(红色为路径):

总结

本次实验为了实现可视化,使用了python的第三方库Pillow,该库支持读取图片,并转化为数组的形式存储,每个点为RGBA的颜色值。考虑到给定的图片是等间隔的正方形组成,因此想到了利用等间隔采样来提取每个方格的颜色值,从而判定区域类型。

从我犯的错误中也可以总结出,A算法未必会得到最优解,只有在启发式函数满足一定条件的情况下,得到的解才满足最优解,否则只能得到可行解。由于A算法需要频繁查找活结点是否存在于closed表中,于是利用python封装好的set数据结构进行closed表的存储,set是使用红黑树实现的,因此查询速度很快;由于每次从opened表中读取的是代价最小的结点,因此采用PriorityQueue优先队列来维护一个小顶堆,每次从堆顶取得具有最小值的结点,对其进行扩展。

A算法的启发式函数得到的估计值h是不准确的,设当前区域到达目标区域的代价为n,那么h≤n,这种情况下,导致A算法搜索的区域数多,搜索范围大,效率低。同时,A*算法的启发式函数h如果小于等于真实值n的话,那么算法是能得到最优解的,若h大于等于真实值n,那么就不能保证得到最优解。

在生成路径结果的时候,考虑到了pillow的原理,读取图片-> RGBA字典 ->字符数组的流程,于是猜想通过逆过程来生成图片,首先将路径叠加到字符数组,然后再通过字符数组转为为RGBA的数组,于是来生成实验结果。

Github源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容