算法篇-14-A*算法解决八数码问题

问题描述

八数码问题:在3×3的方格棋盘上,摆放着1到8这八个数码,有1个方格是空的,其初始状态如图1所示,要求对空格执行空格左移、空格右移、空格上移和空格下移这四个操作使得棋盘从初始状态到目标状态。

1

2

3

8

 0

4

7

6

5

 ( a )初始状态     

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起来真的很累人。





 
 
 
 
 
 
 

猜你喜欢

转载自blog.csdn.net/hffhjh111/article/details/70313093