问题描述
八数码问题:在3×3的方格棋盘上,摆放着1到8这八个数码,有1个方格是空的,其初始状态如图1所示,要求对空格执行空格左移、空格右移、空格上移和空格下移这四个操作使得棋盘从初始状态到目标状态。
1 |
2 |
3 |
8 |
0 |
4 |
7 |
6 |
5 |
2 |
8 |
3 |
1 |
0 |
4 |
7 |
6 |
5 |
(b)目标状态
A*算法介绍
A* 算法实际是一种启发式搜索,所谓启发式搜索,就是利用一个估价函数评估每次决策的价值,决定先尝试哪一种方案,这样可以极大的优化普通的广度优先搜索。
A*算法是一个可采纳的最好优先算法。A*算法的估价函数可表示为:
f'(n) = g'(n) + h'(n)
这里,f'(n)是估价函数,g'(n)是起点到终点的最短路径值,h'(n)是n到目标的最断路经的启发值。由于这个f'(n)其实是无法预先知道的,所以我们用前面的估价函数f(n)做近似。g(n)代替g'(n),但g(n)>=g'(n)才可(大多数情况下都是满足的,可以不用考虑),h(n)代替h'(n),但h(n)<=h'(n)才可。可以证明应用这样的估价函数是可以找到最短路径的,也就是可采纳的。
貌似其经常用在游戏中?算法关键数据结构
Node状态结点:
1.保存每个搜索得到的状态,所以需要有保存状态的方式。
int chest[G_N][G_N];
2.明显需要指向前一个状态节点的指针,以及指向自己后续状态的指针。
Node *prior; //父节点指针
vector<Node*> childs;
3.由于采用启发式搜索,所以还需要保存自身的估价值。
int h_value,g_value,f_value;
Open表:
用于存放待扩展的结点。其中节点的排列顺序是依照估价函数排列,每次取出估价最好的结点扩展(类似优先队列)。如果可以由初始状态到达目标状态,那么一定可以在open表内结点取完前搜索到,否则当前初始态无法达到目标状态。
Closed表:
用于保存已经搜索过的结点。可以直接使用线性表来模拟。最优结果搜索路径应该隐含在这里。
算法伪代码:
Void fun() {
Openlist.push(初始节点);
While(当openlist不为空) {
//取出最优的
Node tmp = openlist.front();
If(tmp == target) {
依据父子关系回溯输出结果路径。
}
while(tmp可以生成不同的child) {
生成孩子child_i,以及child_i的估价值;
判断孩子是否与自己祖先相同,若相同舍弃,
/*进行生成下一个孩子并检测;
若不同
1.检测child_i是否在open表中,若在,则比较child_i 与child_in_open的估值函数,
若child_i的估值函数优于child_in_open,则将child_i的评价值以及指向的父节点指针
赋予child_in_open,并将child_in_open加入到tmp的孩子结点指针列表里。(标记为父子关系)
此时可以释放child_i,因为孩子已经用child_in_open代替。
2.检测child_i是否在closed表中,若在,则比较child_i 与child_in_closed的估值函数,
若child_i的估值函数优于child_in_closed,则将child_i的评价值以及指向的父节点指针赋予
child_in_closed, 并将child_in_closed加入到tmp的孩子结点指针列表里。将child_in_closed的
孩子列表清空,并压入open表中待扩展。
此时可以释放child_i,因为孩子已经用child_in_closed代替。
3.若child_i不在两个表中,将child_i压入open表中。并为child_i与tmp标记父子关系。
*/
}// end_of_while(tmp可以生成不同的child)
//tmp扩展完成,压入closed结点列表
Closed.push(tmp);
} //end_of_While(当openlist不为空)
}// end_of_fun()
源代码github地址:
https://github.com/YIWANFENG/Algorithm-github
此例中设置了2种启发函数。
算法注释:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
const short G_N = 3; //方形数码问题每行元素个数
class Node{
public:
int x,y; //0(特殊值所在位置)
int chest[G_N][G_N]; //此时分布状态
int h_value,g_value,f_value; //股价函数值
Node *prior; //父节点指针
vector<Node*> childs; //孩子指针
Node *next_open; //用于访问open队列
Node *prior_open; //用于访问open队列
Node() {
x=y=0;
h_value = g_value = f_value = 0;
prior = NULL;
next_open = prior_open =NULL;
}
void inherit(Node* ff) {
//仅复制x,y,chest
if(!ff) return ;
x = ff->x; y = ff->y;
for (int i = 0; i < G_N; i++) {
for (int j = 0; j < G_N; j++) {
chest[i][j] = ff->chest[i][j];
}
}
prior = ff;
}
void hn2(Node *target) {
//启发函数 1
//不在目标位置点数作为h_value
int num = 0;
for(int i = 0 ; i < G_N ; i++) {
for(int j = 0 ; j < G_N ; j++) {
if(this->chest[i][j] != target->chest[i][j])
num++;
}
}
h_value = num;
}
void hn(Node *target) {
//启发函数 2
class Distance {
public:
int operator() (int x,int y,int value,Node *target) {
//返回(x,y)对应的值value在target中的位置与(x,y)的距离
for (int i = 0; i < G_N; i++)
for (int j = 0; j < G_N; j++)
if (value == target->chest[i][j])
return abs(x - i) + abs(y - j);
return 1000;
}
} distance;
//所有点距离目标位置和作为h_value
int num = 0;
for (int i = 0; i < G_N; i++) {
for (int j = 0; j < G_N; j++) {
num += distance(i, j, chest[i][j], target);
}
}
h_value = num;
}
//n到目标的最短路径的启发值
void gn() {
if(this->prior == NULL)
this->g_value = 0;
else
this->g_value = this->prior->g_value + 1;
}
//fn估价函数
void fn() {
f_value = g_value + h_value;
}
void show() {
for (int i = 0; i < G_N; i++) {
for (int j = 0; j < G_N; j++) {
cout << this->chest[i][j] << ' ';
}
cout << endl;
}
}
};
bool is_same_node(Node *a, Node *b) {
if (a->x != b->x || a->y != b->y) return false;
for (int i = 0; i < G_N; i++) {
for (int j = 0; j < G_N; j++) {
if (a->chest[i][j] != b->chest[i][j]) {
return false;
}
}
}
return true;
}
class OpenList {
//此class用于访问在open队列
public:
Node* head_;
void add_one(Node*one) {
//list中加入one
if(one == NULL) return ;
one->next_open = NULL;
Node* tmp = head_;
if(head_ == NULL) {
head_ = one;
one->prior_open = NULL;
} else {
while(tmp->next_open!=NULL) tmp=tmp->next_open;
tmp->next_open = one;
one->prior_open = tmp;
one->next_open = NULL;
}
}
void delete_one(Node *one) {
//list中删除one
if(one->prior_open==NULL) {
head_ = one->next_open;
if(one->next_open) //找了几天的bug,哎,此处之前漏写,导致链表操作失误
one->next_open->prior_open = NULL;
} else {
one->prior_open->next_open = one->next_open;
if(one->next_open != NULL) one->next_open->prior_open = one->prior_open;
}
one->next_open = one->prior_open = NULL;
}
Node* front() {
//取出 Openlist 中 f_value最大的元素地址
Node *tmp = head_;
Node *min=head_;
while(tmp!=NULL) {
if(tmp->f_value < min->f_value) {
min = tmp;
}
tmp = tmp->next_open;
}
return min;
}
bool has_child(Node *child,Node *&who) {
//判断OPEN表中是否含有结点child
//并由who指出其在open中的位置
Node *tmp = head_;
while(tmp) {
if(is_same_node(tmp, child)) {
who = tmp;
return true;
}
tmp = tmp->next_open;
}
who = NULL;
return false;
}
bool empty() {
return (head_ == NULL);
}
};
Node target;
//priority_queue<Node*,vector<Node*>,cmp> OPEN; //open表,保存等待扩展的结点
vector<Node*> CLOSED; //closed表,保存访问过的节点
OpenList open_list; //用于访问open结点
bool is_on_closed(Node *child,Node * &who,int &who_pos) {
//判断child是否在CLOSED中出现过,并由who指出其在closed中的位置
for(who_pos=0; who_pos < CLOSED.size(); who_pos++) {
if(is_same_node(child,CLOSED[who_pos])) {
who = (CLOSED[who_pos]);
return true;
}
}
who_pos = -1;
who = NULL;
return false;
}
bool is_target(Node*target,Node* one) {
//判断是否到达目标状态
return is_same_node(target, one);
}
bool is_same_as_parent(Node *one) {
//判断是否和其父辈相同
Node * tmp= one->prior;
while (tmp != NULL) {
if (is_same_node(one, tmp)) {
return true;
}
tmp = tmp->prior;
}
return false;
}
void calculate_g_to_update_open(Node *source,Node* child,Node *who) {
//source 为child的父亲结点
///***计算估值函数并修改
//须知其父是谁,且因为待扩展,不许考虑其子
child->gn();
child->fn();
if(child->g_value < who->g_value) {
who->g_value = child->g_value;
who->f_value = child->f_value;
who->prior = child->prior;
}
source->childs.push_back(who);
delete child;
}
void calculate_g_to_update_closed(Node *source,Node* child,Node *who,int who_pos_on_closed) {
//source 为child的父亲结点
///***计算估值函数并修改,已扩展,需考虑其子
child->gn();
child->fn();
source->childs.push_back(who);
if(child->g_value < who->g_value) {
who->g_value = child->g_value;
who->f_value = child->f_value;
who->prior = child->prior;
who->childs.clear();
CLOSED.erase(CLOSED.begin() + who_pos_on_closed);
open_list.add_one(who);
delete child;
}
}
void check_child(Node *source,Node *child) {
//source --child的父节点
//检查孩子在整个搜索图中的位置以及状态并做相应动作
if (is_same_as_parent(child)) return;
child->hn(&target);
Node *who;
int who_pos_on_closed;
if(open_list.has_child(child,who)) {
//cout<<"on open"<<endl;
calculate_g_to_update_open(source,child,who);
} else if(is_on_closed(child,who, who_pos_on_closed)) {
//cout<<"on closed"<<endl;
calculate_g_to_update_closed(source,child,who, who_pos_on_closed);
} else {
//cout<<"add open"<<endl;
source->childs.push_back(child);
open_list.add_one(child);
}
}
void creat_child(Node *source) {
//产生source结点的后继可能结点(即走一步后的状态)
class Swap {
public:
void operator()(int &a, int &b) {
int t = a;
a = b;
b = t;
}
}swap;
//向左交换
if(source->y>0) {
//cout<<"左"<<endl;
Node *child = new Node();
child->inherit(source);
swap(child->chest[child->x][child->y],
child->chest[child->x][child->y - 1]);
child->x = source->x;
child->y = source->y-1;
check_child(source,child);
}
//向右交换
if(source->y<G_N-1) {
//cout<<"右"<<endl;
Node *child = new Node();
child->inherit(source);
swap(child->chest[child->x][child->y],
child->chest[child->x][child->y + 1]);
child->x = source->x;
child->y = source->y + 1;
check_child(source, child);
}
//向上交换
if(source->x>0) {
//cout<<"上"<<endl;
Node *child = new Node();
child->inherit(source);
swap(child->chest[child->x][child->y],
child->chest[child->x - 1][child->y]);
child->x = source->x - 1;
child->y = source->y;
check_child(source, child);
}
//向下交换
if(source->x<G_N-1) {
//cout<<"下"<<endl;
Node *child = new Node();
child->inherit(source);
swap(child->chest[child->x][child->y],
child->chest[child->x + 1][child->y]);
child->x = source->x + 1;
child->y = source->y;
check_child(source, child);
}
}
void show_path(Node *one) {
Node *tmp = one;
while (tmp) {
cout << "↑" << endl;
tmp->show();
tmp = tmp->prior;
}
}
void search_Astar(Node *target, Node *init_node)
{
//压入初始状态
open_list.add_one(init_node);
// 进入搜索树搜索图
Node *tmp = NULL;
while (!open_list.empty()) {
//取出估价最优的元素,并从open表中除名
tmp = open_list.front();
open_list.delete_one(tmp);
//检查该结点是否为目标节点
if(is_target(target,tmp)) {
cout<<"Yes"<<endl;
break;
}
//有后即状态
if(true) {
//产生后继状态(后继结点)
creat_child(tmp);
}
//该点检测完毕,压入closed表
CLOSED.push_back(tmp);
}
show_path(tmp);
//搜索结束,最终结点是tmp
}
void init_node_status(Node * a, int b[])
{
for (int i = 0; i < G_N; i++) {
for (int j = 0; j < G_N; j++) {
a->chest[i][j] = b[i*G_N + j];
if (b[i*G_N + j] == 0) {
a->x = i;
a->y = j;
}
}
}
a->hn(&target);
a->gn();
a->fn();
}
int main()
{
Node init_node;
init_node.next_open = NULL;
int a[] = { 2,0,3,1,8,4,7,6,5 };
int b[] = { 1,2,3,8,0,4,7,6,5 };
init_node_status(&target, b);
init_node_status(&init_node, a);
target.show();
search_Astar(&target, &init_node);
cout << "Waitint for input" << endl;
cin.get();
return 0;
}
明显由上可知,A*算法类似于优先队列搜索,但与之不同的是估值函数的设置,这也就是A*与优先队列搜索最大不同的地方,所以A*算法可以认为是优先队列搜索。
算法难点在于设置合适的启发式函数与评价估计函数。
以及由上可知我们并未利用节点的孩子节点。因为我再次偷懒。实际的高效做法是在检测孩子节点时,应该在第2步检测中递归修改其孩子节点的估价值。(此时明显会从closed追踪修改到open表中)由此open表需要再排序(注意使用STL的局限性)。
其次,这样第1步检测中若child_in_open的父指针被更改,则应先将其从其原父节点的孩子列表中删除以不影响第二步。
PS:
但是这里注意不是八数码问题所有状态可达某一状态,需要通过分析其初始状态的逆序数问题来判断是否可达目标态。
PS的PS:
在调试阶段,间断想了几天的bug,就是想不到问题在哪。直到前不久突然发现自己的链表在出节点后(此处应逐步跟踪,不过我在深层搜索时一般懒得逐步,选择人肉debug),弹出的节点的前后指针未赋空,在这个算法过程中会导致死循环的出现。所以不仅需要注意主要逻辑,还要把简单的部分写完整,不然debug起来真的很累人。