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 星数据结构
- 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:
- astar_t 应该模板化, heuristic、distance、get_neighbors都应该是可定制的
- 性能参数测试,如1000*1000地图上最坏情况的搜索开销
- open表更新还可以优化,当更新g值后若小于迭代器前一个节点,才需要执行删除再插入
源代码: http://ffown.googlecode.com/svn/trunk/fflib/ext/algorithm/astar2/
- HTML5视音频代码实例 & WEBM格式转换器
- 解析Tensorflow官方PTB模型的demo
- MyBatis源码解析(一)——MyBatis初始化过程解析
- MyBatis源码解析(二)——动态代理实现函数调用
- Git命令速记
- linux设备驱动第三篇:如何写一个简单的字符设备驱动
- Tensorflow高级API的进阶--利用tf.contrib.learn建立输入函数
- Spring速查手册(三)——Spring+JDBC
- [WebKit] JavaScriptCore解析--基础篇(一)字节码的生成及抽象语法树的构建详情分析
- Spring速查手册(二)——Bean的作用域
- pyTorch自然语言处理简单例子
- 一文初探Tensorflow高级API使用(初学者篇)
- Spring速查手册——Bean装配
- 回溯法(一)——n皇后问题
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- R语言如何和何时使用glmnet岭回归
- r语言中对LASSO回归,Ridge岭回归和Elastic Net模型实现
- cmd里如何查看历史命令并执行
- akka-typed(10) - event-sourcing, CQRS实战
- 【每日一题】37. Sudoku Solver
- A quick introduction to innodb_ruby (2.对innodb_ruby的简单介绍)
- Webkit 内核初探
- 配置跨域后,框架帮我们做了什么?
- python应用(1):安装与使用
- TCP粘包和拆包
- 性能测试必备命令(1)- free
- 记一次有趣的挖矿病毒
- 性能测试必备知识(10)- Linux 是怎么管理内存的?
- Ng-Matero V10 正式发布!
- 30.Python装饰器