Programming Challenges 习题8.6.2

本文探讨了经典的15数码拼图问题,采用启发式搜索算法A*解决难题。通过三种不同版本的A*算法实现,包括使用优先队列、开放列表和关闭列表的策略,以及状态集合的替代方案。详细解释了算法设计、优化和实现细节。

PC/UVa:110802/10181

15-Puzzle Problem

一道很难很难的题,写了3天。

经典的15数码问题,需要使用启发式搜索算法。试了3个版本的A*算法。

判断是否可解是从网上抄的,原理是个数学问题,我也没管。

代价函数H表示当前状态到目标状态的代价,这里使用曼哈顿距离,因为状态变换的唯一方法就是上下左右移动,所以曼哈顿距离没什么问题。

计算曼哈顿距离的过程中,不能计算空位置和目标位置的曼哈顿距离,否则就超时,为什么我也不知道。
不使用优先队列也会超时。

AStarVer1是根据网上流传最多的二维平面寻路算法写的。A*算法维护两个列表,一个打开列表和关闭列表,打开列表中保存待扩展的节点,关闭列表中保存已经访问过的节点。每次从打开列表中移除F值最小的节点,并加入到关闭列表中。如果最小节点是目标状态,则算法结束,否则扩展它:

  • 如果得到在关闭列表中的节点,则不进行任何操作;
  • 如果得到在打开列表中的节点,则比较F值(也是G值),取较小的放入打开列表中;
  • 如果扩展到的节点是一个新状态,则计算F值,加入入到打开列表中;

AStarVer2去掉了上一版本中的一些冗余,即使用OpenListheap保存了两份打开列表,但是这两个之间并不存在同步更新关系。打开列表需要选取最小值,使用二叉堆比较合适,因为取最小值是O(1)的操作,这是最快的。但是一般来说二叉堆并不支持替换操作,所以上述第二点用二叉堆无法实现,但幸运的是,即使将相同状态、不同F值的节点加入到二叉堆中,仍然可以保证优先取出F值最优的那个,所以再次从二叉堆中取出相同状态的节点时并不影响。再次取出时,关闭列表中已经存在该节点了,这里都采用了直接覆盖关闭列表中较优节点的操作,这是没有问题的。如果再次取出了该结点,说明上次取出后进行的搜索操作无法得到目标状态,那么这次取出也不会。

AStarVer3是根据《算法艺术与信息学竞赛》的介绍写的算法,和之前的区别就是不使用关闭列表,而使用状态集合。当扩展出新的节点后,如果是已经出现过的状态,则比较F值,并进行相应替换操作。本质上来说并没有什么区别,因为替换的只能是打开列表中的状态,而不是关闭列表中的状态,我个人感觉是因为关闭列表是之前已经走过的状态,再次得到关闭列表中的状态时,G值一定是增大的(或者一样大?),所以不会发生替换情况。

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <queue>
#include <cmath>

#define NO_SOLUTION "This puzzle is not solvable."
#define STR_FINAL_STATE "123456789ABCDEF0"
#define FINAL_STATE 0x123456789ABCDEF0
#define SIZE 4

#define ASTAR_SOLUTION

using namespace std;

bool solvable(vector<vector<int>> &Puzzle)
{
	vector<int> tiles;
	for (int i = 0; i < SIZE; i++)
	{
		for (int j = 0; j < SIZE; j++)
		{
			tiles.push_back(Puzzle[i][j]);
		}
	}
	int sum = 0, row;
	for (size_t i = 0; i < tiles.size(); i++)
	{
		int tile = tiles[i];
		if (tile == 0)
		{
			row = (i / SIZE + 1);
			continue;
		}

		for (size_t m = i; m < tiles.size(); m++)
		if (tiles[m] < tile && tiles[m] != 0)
			sum++;
	}

	return !((sum + row) % 2);
}

#ifdef ASTAR_SOLUTION
class Node
{
private:
	void swap(size_t i, size_t j);
	string strState;
public:
	unsigned long long ullState, back;
	int F, G, H;
	char chDir;
	Node() = default;
	Node(const string &strState, const int G = 0);
	typedef Node(Node::*Move)();
	Node up();
	Node down();
	Node left();
	Node right();
	void calH(const vector<vector<int>> &ManhattanDistance);
};

bool operator<(const Node &n1, const Node& n2)
{
	//标准库优先队列是最大堆,这里的逻辑是反的
	//F越大,结点越小,在最大堆中越靠下
	//F值相同,G越大越接近目标状态,结点越大,越靠上
	if (n1.F > n2.F) return true;
	else if (n1.F < n2.F) return false;
	else{
		if (n1.G > n2.G) return false;
		else if (n1.G < n2.G) return true;
		else return n1.ullState < n2.ullState;
	}
}

void Node::swap(size_t i, size_t j)
{
	strState[i] ^= strState[j] ^= strState[i] ^= strState[j];
	ullState = stoull(strState, nullptr, 16);
}

void Node::calH(const vector<vector<int>> &ManhattanDistance)
{
	int idx;
	H = 0;
	for (size_t i = 0; i < strState.size(); i++)
	{
		if (strState[i] == '0'){
			continue;
			//不对空位置计算距离,超时的原因在这里
			//idx = 0;
		}
		else if (isdigit(strState[i])){
			idx = strState[i] - '0';
		}
		else idx = strState[i] - 'A' + 10;
		H += ManhattanDistance[idx][i];
	}
	//1.5倍H会WA,1.33倍H会TLE,好神奇
	F = G + H;
}

Node::Node(const string &strState, const int G)
: strState(strState), ullState(0), back(0), F(0), G(G), H(0), chDir('S')
{
	ullState = back = stoull(strState, nullptr, 16);
}

Node Node::up()
{
	Node node(strState, G + 1);
	string::size_type blank = node.strState.find('0');
	if (blank >= 4){//up
		node.back = ullState;
		node.chDir = 'U';
		node.swap(blank, blank - 4);
	}
	return node;
}

Node Node::down()
{
	Node node(strState, G + 1);
	string::size_type blank = node.strState.find('0');
	if (blank <= 11){//down
		node.back = ullState;
		node.chDir = 'D';
		node.swap(blank, blank + 4);
	}
	return node;
}

Node Node::left()
{
	Node node(strState, G + 1);
	string::size_type blank = node.strState.find('0');
	if (blank % 4 != 0){//left
		node.back = ullState;
		node.chDir = 'L';
		node.swap(blank, blank - 1);
	}
	return node;
}

Node Node::right()
{
	Node node(strState, G + 1);
	string::size_type blank = node.strState.find('0');
	if (blank % 4 != 3){//right
		node.back = ullState;
		node.chDir = 'R';
		node.swap(blank, blank + 1);
	}
	return node;
}

void calDistance(vector<vector<int>> &ManhattanDistance)
{
	for (int i = 1; i <= SIZE * SIZE; i++)
	{
		for (int pos = 0; pos < SIZE * SIZE; pos++)
		{
			ManhattanDistance[i % 16][pos] = abs((i - 1) / 4 - pos / 4) + abs((i - 1) % 4 - pos % 4);
		}
	}
}

void insertOpenList1(priority_queue<Node> &heap, map<unsigned long long, Node> &OpenList, const Node &node)
{
	map<unsigned long long, Node>::iterator iterOpen = OpenList.find(node.ullState);
	if (iterOpen != OpenList.end()){
		if (node.G < iterOpen->second.G){
			iterOpen->second = node;
			heap.push(node);
		}
	}
	else{
		OpenList[node.ullState] = node;
		heap.push(node);
	}
}

void AStarVer1(const string &strInit, string &strSolution, vector<vector<int>> &ManhattanDistance)
{
	//网上流传最广的方法,UVa运行时间1.860
	priority_queue<Node> heap;
	map<unsigned long long, Node> OpenList;
	map<unsigned long long, Node> CloseList;
	map<unsigned long long, Node>::iterator iterClose;
	vector<Node::Move> MoveMethod;
	MoveMethod.push_back(&Node::up);
	MoveMethod.push_back(&Node::down);
	MoveMethod.push_back(&Node::left);
	MoveMethod.push_back(&Node::right);
	Node node(strInit, 0), curr;
	heap.push(node);
	OpenList[node.ullState] = node;
	while (!OpenList.empty()){
		curr = heap.top();
		if (curr.ullState == FINAL_STATE) break;
		if (curr.G >= 45) break;
		heap.pop();
		OpenList.erase(curr.ullState);
		CloseList[curr.ullState] = curr;
		for (size_t i = 0; i < MoveMethod.size(); i++)
		{
			node = (curr.*(MoveMethod[i]))();
			if (node.ullState == curr.ullState) continue;//无法进行该方向的移动
			node.calH(ManhattanDistance);
			iterClose = CloseList.find(node.ullState);
			if (iterClose != CloseList.end()) continue;//在关闭列表中则直接跳过
			insertOpenList1(heap, OpenList, node);
		}
	}
	if (!OpenList.empty()){
		if (curr.G >= 45) strSolution.assign(NO_SOLUTION);
		else{
			string strRev;
			while (curr.chDir != 'S'){
				strRev.push_back(curr.chDir);
				curr = CloseList.find(curr.back)->second;
			}
			strSolution.assign(strRev.rbegin(), strRev.rend());
		}
	}
}

void AStarVer2(const string &strInit, string &strSolution, vector<vector<int>> &ManhattanDistance)
{
	//来自优快云博客寂静山林,作者邱秋,略有修改,见下
	//修改前UVa运行时间1.190,修改后UVa运行时间1.200
	//可以去掉Ver1中的OpenList,因为和heap互相不影响
	//即使维护了OpenList中节点的唯一性,也无法保证heap中节点的唯一性
	//标准库提供的优先队列无法替代或者删除相同的节点,但是代价小的会被先取出
	priority_queue<Node> heap;
	map<unsigned long long, Node> CloseList;
	map<unsigned long long, Node>::iterator iterClose;
	vector<Node::Move> MoveMethod;
	MoveMethod.push_back(&Node::up);
	MoveMethod.push_back(&Node::down);
	MoveMethod.push_back(&Node::left);
	MoveMethod.push_back(&Node::right);
	Node node(strInit, 0), curr;
	heap.push(node);
	while (!heap.empty()){
		curr = heap.top();
		if (curr.ullState == FINAL_STATE) break;
		if (curr.G >= 45) break;
		heap.pop();
		CloseList[curr.ullState] = curr;
		for (size_t i = 0; i < MoveMethod.size(); i++)
		{
			node = (curr.*(MoveMethod[i]))();
			if (node.ullState == curr.ullState) continue;//无法进行该方向的移动
			node.calH(ManhattanDistance);
			iterClose = CloseList.find(node.ullState);
			if (iterClose != CloseList.end()){
				//注释掉的是原博客作者的写法
				//如果能以更少的步数到达一个走过的状态,则该状态需要重新扩展
				//直观上感觉这种情况应该是不会出现的
				//同时Ver1也证明了已经走过的状态可以直接跳过,UVa可AC
				/*if (node.G < iterClose->second.G){
					CloseList.erase(iterClose);
					heap.push(node);
				}*/
			}
			else{
				heap.push(node);
			}
		}
	}
	if (!heap.empty()){
		node = heap.top();
		if (node.G >= 45) strSolution.assign(NO_SOLUTION);
		else{
			string strRev;
			while (node.chDir != 'S'){
				strRev.push_back(node.chDir);
				node = CloseList.find(node.back)->second;
			}
			strSolution.assign(strRev.rbegin(), strRev.rend());
		}
	}
	else strSolution.assign(NO_SOLUTION);
}

void AStarVer3(const string &strInit, string &strSolution, vector<vector<int>> &ManhattanDistance)
{	
	//算法艺术与信息学竞赛中提供的写法,UVa运行时间1.480
	//CloseList更改为StateList,表示所有出现过的状态,实质上将打开列表和关闭列表合并了
	priority_queue<Node> heap;
	map<unsigned long long, Node> StateList;
	map<unsigned long long, Node>::iterator iterState;
	map<unsigned long long, Node>::iterator iter;
	vector<Node::Move> MoveMethod;
	MoveMethod.push_back(&Node::up);
	MoveMethod.push_back(&Node::down);
	MoveMethod.push_back(&Node::left);
	MoveMethod.push_back(&Node::right);
	Node node(strInit, 0), curr;
	heap.push(node);
	StateList[node.ullState] = node;//初始状态加入状态列表
	while (!heap.empty()){
		curr = heap.top();
		if (curr.ullState == FINAL_STATE) break;
		if (curr.G >= 45) break;
		heap.pop();
		//Ver1和Ver2中加入到关闭列表中的操作可以去掉
		//CloseList[curr.ullState] = curr;
		for (size_t i = 0; i < MoveMethod.size(); i++)
		{
			node = (curr.*(MoveMethod[i]))();
			if (node.ullState == curr.ullState) continue;//无法进行该方向的移动
			node.calH(ManhattanDistance);
			iterState = StateList.find(node.ullState);
			if (iterState != StateList.end()){
				if (node.G < iterState->second.G){
					//可能替换属于打开列表中的状态
					//也可能替换属于关闭列表中的状态,但是根据Ver1,这不太可能
					iterState->second = node;
					heap.push(node);
				}
			}
			else{
				//新状态
				StateList[node.ullState] = node;
				heap.push(node);
			}
		}
	}
	if (!heap.empty()){
		node = heap.top();
		if (node.G >= 45) strSolution.assign(NO_SOLUTION);
		else{
			string strRev;
			while (node.chDir != 'S'){
				strRev.push_back(node.chDir);
				node = StateList.find(node.back)->second;
			}
			strSolution.assign(strRev.rbegin(), strRev.rend());
		}
	}
	else strSolution.assign(NO_SOLUTION);
}
#endif

int main()
{
	int N = 0;
	cin >> N;
	vector<vector<int>> Puzzle(SIZE, vector<int>(SIZE, 0));
	vector<vector<int>> ManhattanDistance(16, vector<int>(16, 0));
	calDistance(ManhattanDistance);
	for (int n = 0; n < N; n++)
	{
		for (int i = 0; i < SIZE; i++)
		{
			for (int j = 0; j < SIZE; j++)
			{
				cin >> Puzzle[i][j];
			}
		}
#ifdef ASTAR_SOLUTION
		//A*算法中代价函数F = G + H
		//G表示从初始状态到当前状态的移动次数
		//H表示从当前状态到目标状态的代价函数,是一个估计值
		//因为15数码只能上下左右移动,所以代价函数可以取曼哈顿距离
		if (!solvable(Puzzle)){
			cout << NO_SOLUTION << endl;
			continue;
		}/**/
		string strInit, strSolution;
		for (int i = 0; i < SIZE; i++)
		{
			for (int j = 0; j < SIZE; j++)
			{
				if (Puzzle[i][j] < 10) strInit.push_back(Puzzle[i][j] + '0');
				else strInit.push_back(Puzzle[i][j] - 10 + 'A');
			}
		}
		AStarVer3(strInit, strSolution, ManhattanDistance);
		cout << strSolution << endl;
#endif
	}
	return 0;
}
/*
2
2 3 4 0
1 5 7 8
9 6 10 12
13 14 11 15
13 1 2 4
5 0 3 7
9 6 10 12
15 8 11 14
*/

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值