使用A*演算法解迷宮最短路徑問題

AiLo發表於2018-08-01

原創文章,轉載請聯絡作者

時光只解催人老,不信多情,長恨離亭,淚滴春衫酒易醒。

前言

最近接觸了一個挺有意思的小課題,跟大家分享一下。就是利用A*演算法,來計算迷宮可行路徑。有關這個演算法的知識,大家可以看看A*演算法維基百科以及A星演算法詳解來稍作了解。
程式碼地址在此Maze,喜歡Python的小可愛們可以拿去練練手。

提要說明

本題中的迷宮,是以宮格型別呈現的,在程式碼中的呈現為二維陣列。其次在迷宮中的移動,也只有上、下、左、右四個動作可選。如下所示:

其中1代表入口,2代表障礙物不可通行,3代表出口

[[3, 2, 2, 2, 2, 2, 2, 2, 1],
 [0, 0, 2, 2, 2, 2, 2, 0, 0],
 [2, 0, 0, 2, 2, 2, 0, 0, 2],
 [2, 2, 0, 0, 2, 0, 0, 2, 2],
 [2, 2, 2, 0, 0, 0, 2, 2, 2]]
複製程式碼

其實在A*演算法中,對單位搜尋區域的描述為--節點nodes。在本題中,我們可以把搜尋區域視為正方形,會更簡單一點。

A*演算法邏輯解析

A*演算法的邏輯其實並不是很難,簡化起來就是兩個詞:評估迴圈
從起點開始行動,首先找到起點周圍可以行走的節點,然後在這個節點中,評估出距離終點最優(最短)的節點。那麼這個最優節點,將作為下一步行動的點,以此類推,直至找到終點。
可以看到,在這個邏輯中,其實最重要的就是評估這一步了。A*演算法的評估函式為:
f(n) = g(n) + h(n)

g(n)--代表移動到這個點的代價,在本題中均為1.因為只可以水平或者數值運動。要是斜角可以移動的話,那麼這個值就為√2
h(n)--從這個點移動到終點的代價,這是一個猜測值。本題中,將迷宮視作座標系的話,那麼h(n)就是取和終點x、y各自差值的最小者。譬如點[4,2]和終點[1,1]的h(n)取值為:1

程式碼實現

程式碼中對點的描述,均為實際值,並非以0為開始值計算。

定位起點和終點,使用列表儲存四個移動命令,以下程式碼env_data代表迷宮陣列:

# 上下左右四個移動命令,只具備四個移動命令
orders = ['u', 'd', 'l', 'r']

# 定位起點和終點
start_loc = []
des_loc = []
for index, value in enumerate(env_data, 1):
    if len(start_loc) == 0 or len(des_loc) != 0:
        if 1 in value:
            start_loc = (index, value.index(1) + 1)
        if 3 in value:
            des_loc = (index, value.index(3) + 1)
    else:
        break
複製程式碼

判斷節點所有可執行的移動命令:

def valid_actions(loc):
    """
    :param loc:
    :return: 當前位置所有可用的命令
    """
    loc_actions = []
    for order in orders:
        if is_move_valid(loc, order):
            loc_actions.append(order)
    return loc_actions

def is_move_valid(loc, act):
    """
    判斷當前點,是否可使用此移動命令
    """
    x = loc[0] - 1
    y = loc[1] - 1
    if act not in orders:
        return false
    else:
        if act == orders[0]:
            return x != 0 and env_data[x - 1][y] != 2
        elif act == orders[1]:
            return x != len(env_data) - 1 and env_data[x + 1][y] != 2
        elif act == orders[2]:
            return y != 0 and env_data[x][y - 1] != 2
        else:
            return y != len(env_data[0]) - 1 and env_data[x][y + 1] != 2
複製程式碼

拿到節點周圍移動單位為1的所有可到達點,不包括此節點:

def get_all_valid_loc(loc):
    """
    計算當前點,附近所有可用的點
    :param loc:
    :return:
    """
    all_valid_data = []
    cur_acts = valid_actions(loc)
    for act in cur_acts:
        all_valid_data.append(move_robot(loc, act))
    if loc in all_valid_data:
        all_valid_data.remove(loc)
    return all_valid_data
    
def move_robot(loc, act):
    """
    移動機器人,返回新位置
    :param loc:
    :param act:
    :return:
    """
    if is_move_valid(loc, act):
        if act == orders[0]:
            return loc[0] - 1, loc[1]
        elif act == orders[1]:
            return loc[0] + 1, loc[1]
        elif act == orders[2]:
            return loc[0], loc[1] - 1
        else:
            return loc[0], loc[1] + 1
    else:
        return loc
複製程式碼

h(n)函式體現:

def compute_cost(loc):
    """
    計算loc到終點消耗的代價
    :param loc:
    :return:
    """
    return min(abs(loc[0] - des_loc[0]), abs(loc[1] - des_loc[1]))
複製程式碼

開始計算

使用road_list來儲存走過的路徑,同時用另一個集合儲存失敗的節點——即此節點附近無可用節點,死衚衕

# 已經走過的路徑list,走過的路
road_list = [start_loc]
# 證實是失敗的路徑
failed_list = []

# 沒有到達終點就一直迴圈
while road_list[len(road_list) - 1] != des_loc:
    if len(road_list) == 0:
        print("迷宮無解")
        break
    # 當前點
    cur_loc = road_list[len(road_list) - 1]
    # 當前點四周所有可用點
    valid_loc_data = get_all_valid_loc(cur_loc)
    # 如果可用點裡包括已經走過的節點,則移除
    for cl in road_list:
        if cl in valid_loc_data:
            valid_loc_data.remove(cl)
    # 如果可用點集合包括失敗的節點,則移除
    for fl in failed_list:
        if fl in valid_loc_data:
            valid_loc_data.remove(fl)
    # 沒有可用點,視作失敗,放棄該節點。從走過的路集合中移除掉
    if len(valid_loc_data) == 0:
        failed_list.append(road_list.pop())
        continue
    # 用評估函式對可用點集合排序,取末端的值,加入走過的路集合中
    valid_loc_data.sort(key=compute_cost, reverse=True)
    road_list.append(valid_loc_data.pop())
複製程式碼

看執行結果

使用A*演算法解迷宮最短路徑問題

結語

人生苦短,我用Python。程式碼地址在此Maze,喜歡Python的小可愛們可以拿去練練手。
在研究迷宮的過程中,發現生成迷宮的演算法也是很有意思的,等忙完這段時間再去研究研究。嘻~~~~~
以上

使用A*演算法解迷宮最短路徑問題

相關文章