用A*算法解决15数码问题(8数码问题)c++


给出了一种基于散列表和红黑树存储open表和close表的A*算法程序,提升了速度。

十五数码问题

完整代码可参见 githubgitee

十五数码问题是人工智能中状态搜索中的经典问题,其中,该问题描述为:在4×4的棋盘,摆有十五个棋子,每个棋子上标有1至15的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。
这是一个典型的图搜索问题,但是该问题并不需要正在建立图数据结构来进行求解,而是将图的搜索方法抽象,运用到该问题上。
初始状态为
在这里插入图片描述

目标状态为
在这里插入图片描述

通常,在程序中以0代替空位。

算法

启发函数

G ( S ) = D i s ( S ) + D e p t h ( S ) ∗ a l p h a G(S)=Dis(S)+Depth(S)*alpha G(S)=Dis(S)+Depth(S)alpha
其中Dis(S)表示该数字到目标数字的曼哈顿距离,Depth(S)表示结点的深度(初始结点深度为0)。系数alpha用于控制距离和深度信息两者的比重。当alpha较小时,则启发函数更体现结点的深度信息;当alpha较大时,启发函数体现更多的深度信息。

A*算法步骤

给定初始节点S0, 目标节点St, 深度信息权重alpha
Push(open, S0)  //将S0放入Open表
while Open非空 && 循环次数<1000000:
	Sp = Min(open)  //寻找open表中估价函数最小的
	Pop(Open, Sp)  //从Open表中删除Sp
	for d in [上,下,左,右]//对四个方向分别扩展
		if Extendable(Sp, d):   //判断节点是否可扩展
			Sp’ = Extend(Sp, d)  //扩展出的新结点
			G(Sp’) = Dis(Sp’) + depth(Sp’) * alpha
			if Find(Open, Sp’):
				比较两个节点启发函数大小,若Sp’较小,则用Sp'替换较大的结点
			else if Find(Close, Sp’):
				比较两个节点启发函数大小,若Sp’较小Push(Open, Sp’), 并从Close表中删除较大的节点
			else:
				Push(Open, Sp’)
			end if
		end if
	end for
	Push(Close, Sp)
end while

结点的表示

#define SIZE 4  //数组大小 SIZE*SIZE
typedef int table[SIZE][SIZE];   // to store the data

class State
{
public:
	int zero_row;  // position of zero
	int zero_col;
	double fvalue;
	int depth;  // the depth of state in the tree 
	double distance;   // the sum of the distance of the num out of position
	string str_num;    // str of data, used for open/close map
	State* father;   // previous node

	State(table, int);
	~State();
	int get_distance(unordered_map<int, pos>);   // city block distance
	double evaluate(unordered_map<int, pos>, double);  // return fvalue = depth*alpha + distance
	string get_str_num();  // data => string
	

	State* extend();  // generate a new node

	void move_up();  // move zero 扩展节点
	void move_left();
	void move_down();
	void move_right();
	void show_table(); // 打印棋盘

private:
	table data;
};

数据结构

目标状态St的存储

为了更快地计算棋盘中每个数到目标位置的曼哈顿距离,用散列表std::unordered_map存储目标状态中每个数的位置,一个数对应一个位置(row, col)。

//用一个结构体存储一个数的位置
typedef struct
{
	int row;
	int col;
}pos;

unordered_map<int, pos> get_target_map(table target)
{
	// write a table to a map
	unordered_map<int, pos> target_map;
	for (int i = 0; i < SIZE; ++i) {
		for (int j = 0; j < SIZE; ++j) {
			pos p{ i, j };
			target_map.insert(pair<int, pos>(target[i][j], p));
		}
	}
	return target_map;
}

Open表和Close表的存储

上边这一步的操作其实影响较小,在A*算法中真正耗费时间的主要是在open表和close表中搜索。
同样,采用散列表的方式存储open表和close表,判断该结点的状态是否存在。这里,我将每个结点的棋盘数字打平,并存储成字符串形式,作为散列表的Key. 如,目标状态应表示为

1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-0

转换代码

string table2string(table a)
{
	string s;
	for (int i = 0; i < SIZE; ++i) {
		for (int j = 0; j < SIZE; ++j)
			s += to_string(a[i][j]) + "-";
	}
	return s;
}

这样就可以用unordered_map<string, State*>的方式存储open表和close表。

但散列表是无序的,为了能够在节点插入open表时就根据fvalue进行排序,使用红黑树std::set的结构再建立一个有序Open表,并自定义排序方式

struct cmp {
	bool operator()(const State* a, const State* b) const
	{
		if (a->fvalue == b->fvalue)
			return a < b;
		return a->fvalue < b->fvalue;
	}
};
set<State*, cmp> open_set;

A*算法程序

完整代码可参见https://github.com/sby9981/A_project
并以如下结构安放文件
在这里插入图片描述
A*算法程序

State* process(State* new_node, unordered_map<string, State*>& open_map, set<State*, cmp>& open_set,
	unordered_map<string, State*>& close_map, const unordered_map<int, pos> target_map, double alpha)
/*	处理新结点,在open表和close表中搜寻新结点的状态是否存在
*	new_node:	新生成的结点
*	open_map:	存放全部节点字符串id的open表
*	open_set:	节点按估价函数fvalue排序后的open表
*	close_map:	存放全部节点字符串id的close表
*	target_map:	最终目标棋盘状态的数字和位置信息
*	alpha:		深度信息的权重fvalue = distance + depth * alpha;
*	return final_node:	若找到解,返回解的节点,若未找到,返回空
*/
{
	State* final_node = 0;
	unordered_map<string, State*>::iterator it_open = open_map.find(new_node->str_num),
											it_close = close_map.find(new_node->str_num);

	if (it_open != open_map.end()) {
		new_node->evaluate(target_map, alpha);
		if (it_open->second->fvalue > new_node->fvalue) {
			set<State*, cmp>::iterator it = open_set.find(it_open->second);
			open_set.erase(it);
			open_set.insert(new_node);
			it_open->second = new_node;
		}
	}
	else if (it_close != close_map.end()) {
		new_node->evaluate(target_map, alpha);
		if (it_close->second->fvalue > new_node->fvalue) {
			open_map.insert(pair<string, State*>(new_node->str_num, new_node));
			open_set.insert(new_node);
			close_map.erase(it_close);
		}
	}
	else {
		new_node->evaluate(target_map, alpha);
		if (new_node->distance == 0) {
			final_node = new_node;
		}
		open_map.insert(pair<string, State*>(new_node->str_num, new_node));
		open_set.insert(new_node);
	}

	return final_node;
}

deque<State*> A_solution(State* startnode, table target, double alpha)
/*	A*算法主程序
*	startnode:	初始状态的结点
*	target:		最终目标棋盘状态的数字和位置信息
*	return solution: 解的结点路径
*/
{
	State* final_node = 0;   // its data is target {1,2,3...15,0}
	deque<State*> solution;  // the node chain lead to final_node
	set<State*, cmp> open_set;  // it's sorted by fvalue
	unordered_map<int, pos> target_map = get_target_map(target);
	unordered_map<string, State*> open_map, close_map;
	int node_num = 0;  // the totle num of node <==> the step to the final solution
	int loop_num = 0;  // the loop num of while

	clock_t start1, end1;
	start1 = clock();

	startnode->evaluate(target_map, alpha);
	open_set.insert(startnode);
	open_map.insert(pair<string, State*>(startnode->get_str_num(), startnode));

	while (open_set.size() && loop_num < 1000000) {

		State* node = *open_set.begin();
		open_set.erase(open_set.begin());
		unordered_map<string, State*>::iterator node_it = open_map.find(node->str_num);
		open_map.erase(node_it);
		
		if (loop_num++ % 1000 == 0)
			cout << loop_num << ": " << node->fvalue << " " 
				<< node->distance<< " "<< node->depth << endl;
		
		if (node->zero_row > 0) {
			State* new_node = node->extend();
			new_node->move_up();
			if (final_node = process(new_node, open_map, open_set, close_map, target_map, alpha))
				break;
			else
				++node_num;
		}
		if (node->zero_row < SIZE - 1) {
			State* new_node = node->extend();
			new_node->move_down();
			if (final_node = process(new_node, open_map, open_set, close_map, target_map, alpha))
				break;
			else
				++node_num;
		}
		if (node->zero_col > 0) {
			State* new_node = node->extend();
			new_node->move_left();
			if (final_node = process(new_node, open_map, open_set, close_map, target_map, alpha))
				break;
			else
				++node_num;
		}
		if (node->zero_col < SIZE - 1) {
			State* new_node = node->extend();
			new_node->move_right();
			if (final_node = process(new_node, open_map, open_set, close_map, target_map, alpha))
				break;
			else
				++node_num;
		}
		close_map.insert(pair<string, State*>(node->str_num, node));
	}
	end1 = clock();
	
	cout << endl << "path: " << endl;
	while (final_node) {
		solution.push_front(final_node);
		final_node = final_node->father;
	}
	int j = 0;
	for (auto i = solution.begin(); i != solution.end(); ++i) {
		cout << j++ << endl;
		(*i)->show_table();
	}
	if (final_node)
		cout << "solution depth: " << final_node->depth << endl;
	std::cout << "node num: " << node_num << ", loop_num: " << loop_num << endl;
	cout << "total time(s): " << (double)(end1 - start1) / CLOCKS_PER_SEC << endl;
	return solution;
}

使用

int main()
{
	table target = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0 };
	table start = { 11,9,4,15,1,3,0,12,7,5,8,6,13,2,10,14 };
	
	State* node0 = new State(start, 0);

	deque<State*> solution = A_solution(node0, target, 0.5);
	cout << solution.size() - 1<< endl;

	return 0;
}

alpha为0.5的结果
在这里插入图片描述
经过0.786秒输出结果,走了43步。

结果讨论

通过调整alpha值的大小,程序运行的速度有着显著差异。其中alpha为0.5时,程序运行最快;alpha为0.8时得到的解与alpha为1时相同,可视为最优解,最终解的深度为41。详细结果表格所示。但由于在算法设计中使用了按地址排序,因此结果会有一定的随机性,但程序按启发函数大小排序的约束不变,对于求解最优解的影响较小。

alpha生成节点总数循环总数运行时间(s)解的深度
0.01220804115017.623121
0.167870228639.70891
0.21187693994316.99275
0.365107218099.36571
0.42638688373.85561
0.5531817720.77445
0.6677922680.98645
0.71519050652.2443
0.8717472390310.3541
0.92485728299753.61841
1.056993218941482.1241
1.11051689352964152.16841
1.22432141,812014350.4541
1.3循环次数超出100,0000
从表 中可以看到,生成的节点总数、循环总数与运行时间是正相关的。而当alpha从0.1增大时,运行时间先减小再增大,在alpha为0.5时,程序运行最快,当alpha大于0.7后,程序运行时间快速增大。而解的深度则随着alpha增大而减小。
可见,alpha越小,启发信息越强,同时程序也与趋向于局部最优,而陷入局部最优同样会浪费计算量,当alpha为0.5时达到了一个平衡,能以最快速度找到解。而随着alpha增大,算法越类似于广度优先搜索,越难找到解,计算量大大增加,同时找到解也越优。当alpha过大时,启发信息太弱,启发函数难以收敛,无法找到解。
  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值