A星路徑搜索


摘要:

  在人工智能中有一類問題是有確定解的,如路徑、五子棋等,這樣的問題非常適合使用搜索來解決。 路徑搜索是一個很有趣的問題,在人工智能中算是很基礎的問題。最近一直在讀《Artificial Intelligence-A Modern Approach》,搜索部分看完印象最深的就是A星算法了,這個在游戲開發中也最常用。於是乎做個總結,明天就掀過這篇了。

路徑搜索算法:

Dijkstra:

  Dijkstra 最短路徑算法,大學數據結構教科書上都講過,這里也不贅述了。但是為了及和一下幾個算法做比較,我google 了一個圖,非常直接的顯示Dijkstra算法的搜索過程:

  圖中以中心為起點,以輻射狀不斷向中心外搜索(每次取距離起點最近的點),一圈一圈向外擴張,直到逼近目標點,完成路徑搜索。

Best-First-Search:

  BSF 每次擴張節點,都選擇最接近目標的節點。Dijkstra 是每次都選擇據起點最近的節點。區別是到起點的距離總是已知的,而都終點的距離只能是估計的。所以BSF 提供了啟發式函數h。常見的啟發式函數h有:

  • 基於絕對距離,計算當前節點到目標點的絕對距離(此時並不能知曉該路徑是否可行,也許會有阻礙)
  • 基於方向的,如果目標在東方,那么只向東南、東、東北三個方向擴展,在障礙物少的情況下,BSF可以非常快、非常直接的搜索到目標。如果因障礙阻塞,改變了路徑方向,BSF找到的不一定是最近的路徑。

A 星算法

  A 星算法兼具Dijkstra 准確和 BSF 的快速,在搜索路徑時,通過啟發式函數h 計算當前節點到目標節點的距離,而起點到當前點距離已知,則每次選擇f = g + h 最小的節點。A星總是嘗試找到最短的路徑,阻礙物少的情況下性能接近BSF。

A 星算法原理

 

A 星算法實現

主邏輯實現:

  

int astar_t::search_path(uint32_t from_index, uint32_t to_index, vector<uint32_t>& path_)
{
//! open 表中保存待擴展的節點
//! visited 保存此次搜索訪問過的節點,待搜索完成,將其狀態恢復到默認狀態
open_table_t open;
vector<map_node_t*> visited;

search_node_t current;
search_node_t neighbor_node;
vector<map_node_t*> neighbors;

//! 先將起始點加入到open 表中
current.set_pos_index(from_index);
open.insert(current);

visited.push_back(m_map.get_node(current.get_pos_index()));

//! 大循環,直到open為空(找不到目標) 或 找到目標
while (false == open.empty()) {
open.pop_first(current);

if (current.get_pos_index() == to_index)
{
break;
}

//! 添加到close 表
m_map.get_node(current.get_pos_index())->set_closed();

//! 找到當前節點的所有鄰居節點, 不同的游戲中該函數實現可能不同
//! 有的游戲可以走斜線,而有些不能,如坦克大戰就不能走斜線
m_map.get_neighbors(current.get_pos_index(), neighbors);

for (size_t i = 0; i < neighbors.size(); ++i)
{
map_node_t* neighbor_map_node = neighbors[i];
neighbor_node.set_pos_index(neighbor_map_node->get_pos_index());

//! 計算該點的 g 和 h 值
neighbor_node.set_gval(m_map.distance(current.get_pos_index(), neighbor_map_node->get_pos_index()));
neighbor_node.set_hval(m_map.heuristic(neighbor_map_node->get_pos_index(), to_index));

//! 如果該點已經在open表中,檢查g值,若g值更小,說明當前路徑更近,更新open表
if (true == neighbor_map_node->is_open())
{
if (neighbor_node.get_gval() < neighbor_map_node->get_gval())
{
open.update(neighbor_map_node->get_fval(), neighbor_node);
neighbor_map_node->set_gval(neighbor_node.get_gval());
neighbor_map_node->set_hval(neighbor_node.get_hval());
neighbor_map_node->set_parrent(current.get_pos_index());
}
}
//! 如果該點既沒有在open,也沒有在close中,直接添加到open
else if (false == neighbor_map_node->is_closed())
{
open.insert(neighbor_node);
neighbor_map_node->set_open();
neighbor_map_node->set_parrent(current.get_pos_index());
visited.push_back(neighbor_map_node);
}
//! 如果已經在close 中,簡單跳過
else {} //! closed ignore
}
neighbors.clear();
}

//! 找到了目標,逆序遍歷,得到完整的路徑
if (current.get_pos_index() == to_index)
{
path_.push_back(current.get_pos_index());
uint32_t next = m_map.get_node(current.get_pos_index())->get_parrent();
while (next != from_index)
{
path_.push_back(next);
next = m_map.get_node(next)->get_parrent();
}
path_.push_back(from_index);
}

//! 最后將所有的已訪問過的節點狀態清楚, 為下次搜索做准備
for (size_t i = 0; i < visited.size(); ++i)
{
visited[i]->clear();
}
visited.clear();
return 0;
}

 

A 星數據結構

  1. open 表,維護待擴展的節點,每次從其中找到f = g + h 最小的節點,由pop_first 實現

  open 表 是按照f = g + h 由大到小順排序的,是一個multimap

  
typedef multimap<uint32_t, search_node_t> table_t;
struct open_table_t
{
table_t nodes;
bool empty() { return nodes.empty(); }
int pop_first(search_node_t& ret)
{
table_t::iterator it = nodes.begin();
ret = it->second;
nodes.erase(it);
return 0;
}
void insert(const search_node_t& node_)
{
nodes.insert(make_pair(node_.get_fval(), node_));
}
void update(uint32_t old_, const search_node_t& node_)
{
pair<table_t::iterator, table_t::iterator> ret = nodes.equal_range(old_);
table_t::iterator it = ret.first;
for (; it != ret.second; ++it)
{
if (it->second == node_)
{
//! 可以優化, 如果前一個比該節點小,才需要刪除
nodes.erase(it);
}
}
this->insert(node_);
}
};

 

2. map 管理器

  map 管理器記錄所有地圖信息,記錄某個坐標其相鄰坐標信息,記錄某個坐標是否可通行信息、地圖的寬、高等、兩點的距離等。map管理器中維護一個二維數組

  
 map_mgr_t(uint32_t width_, uint32_t height_):
m_map_nodes(NULL),
m_width(width_),
m_height(height_)
{
m_map_nodes = (map_node_t*)malloc(m_width * m_height * sizeof(map_node_t));
for (uint32_t i = 0; i < m_height; ++i)
{
for (uint32_t j = 0; j < m_width; ++j)
{
new(m_map_nodes + i * m_width + j) map_node_t(i * m_width + j);
}
}
}
 
3.  獲取鄰居節點方法如下,限制不能斜着走,不同的游戲可能有不同的實現
 
void get_neighbors(uint32_t pos_, vector<map_node_t*>& ret_)
{
map_node_t* tmp = m_map_nodes + pos_ - 1;
if (tmp >= m_map_nodes && tmp < m_map_nodes + m_height * m_width && tmp->is_can_pass())
{
ret_.push_back(tmp);
}
tmp = m_map_nodes + pos_ + 1;
if (tmp >= m_map_nodes && tmp < m_map_nodes + m_height * m_width && tmp->is_can_pass())
{
ret_.push_back(tmp);
}
tmp = m_map_nodes + pos_ - m_width;
if (tmp >= m_map_nodes && tmp < m_map_nodes + m_height * m_width && tmp->is_can_pass())
{
ret_.push_back(tmp);
}
tmp = m_map_nodes + pos_ + m_width;
if (tmp >= m_map_nodes && tmp < m_map_nodes + m_height * m_width && tmp->is_can_pass())
{
ret_.push_back(tmp);
}
}
 
4. 啟發式函數

  由於不能斜着走,那么啟發式函數h 只是獲得x、y上偏移和。

uint32_t heuristic(uint32_t from_, uint32_t to_)
{
return this->distance(from_, to_);
}

TODO:

  1. astar_t 應該模板化, heuristic、distance、get_neighbors都應該是可定制的
  2. 性能參數測試,如1000*1000地圖上最壞情況的搜索開銷
  3. open表更新還可以優化,當更新g值后若小於迭代器前一個節點,才需要執行刪除再插入

 

源代碼: http://ffown.googlecode.com/svn/trunk/fflib/ext/algorithm/astar2/

 更多精彩文章 http://h2cloud.org


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM