九宫格(八数码)问题
1. 前言
智能控制基础的课后题,开始以为没什么,但是不写复杂的算法好久动手写起来还是漏洞百出gwickler/-inf.html)
主要参考了这篇博客的内容
英文描述此问题可参见[此处](http://www.aiai.ed.ac.uk/
2. 实现
2.1 定义一个图的节点类
class VertexOfGraph { //定义图的节点类
public:
int vertex[3][3];
int h,g;
int x,y;
string hashValue;
public:
VertexOfGraph() { //构造函数将成员变量初始化为0和空
x = 0;
y = 0;
h = 0;
g = 0;
hashValue = "";
};
bool isValid() { //判断当前状态是否有效,如果超出边界范围则无效
if(x >= 0 && x < 3 && y >= 0 && y < 3) {
return true;
}
return false;
}
void newStatus(int oldX, int oldY) { //将现在存储的空位位置x,y与oldX,oldY对应的位置交换数值,得到新状态
int temp = vertex[x][y];
vertex[x][y] = vertex[oldX][oldY];
vertex[oldX][oldY] = temp;
}
};
为了方便将类的成员变量都定义为public类型,省去了set、get方法,类中的h、g并没有用到,初始化都置0了,作为以后扩展用,主要是用于启发式搜索中的估价函数,搜索得到比较常用的启发式搜索方法有A*搜索,目前的算法都没有用到~
2.2 输入功能
输入功能必须要有,就控制台输入了, 可以把代码移到C#上,用Winform做一个GUI界面~
int origin[3][3],target[3][3];
定义3*3的数组表示九宫格的九个数字, origin分别为初始状态和目标状态, 循环读入即可. 至于为何不使用1*9的一维数组, 照顾要是考虑到后续搜索路径的问题, 3*3的路径比较容易设计, 其中一个数组下标变化1即可, 而对于1*9, 感觉有点复杂(后来发现其实并不是, 将下标分别+-3和+-1就得到了四个路径, 再判断新状态是否有效即可, 好像还比3*3简单点)
2.3 状态判重
对于每个状态必须判重,也要判断是否与目标状态相同,如果是将9个元素逐个比较,太耗时,肯定是得用到hash,hash方法http://www.cnblogs.com/goodness/archive/2010/05/04/1727141.html中的境界二有提到,但是我没看懂“逆序值”这个词写的不清楚,感觉应该是排列组合中的一个名词,但是搜出来的都不对,于是自己设计个不能称之为hash的hash函数, 因为这个方法的信息量没有压缩, 是可逆的不过有一个好处就是存储的信息相对而言多了, 对于最后输出所有路径有所帮助。
对于每种状态只需要唯一对应一个值即可,于是想着把每种状态的数字串成字符串当作hash值,这肯定不是好方法,至少本来只需要9!=362880个int型元素就能存下所有状态,而现在对于每种状态都是一个9位长的字符串
string getHash(int v[3][3]) { //将状态的数字排序转换为字符串,作为hash值
string hashString = "";
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
stringstream sstream;
string str;
sstream << v[i][j]; //注意sstream输入是<<而不是iostream的>>
sstream >> str;
hashString += str;
}
}
return hashString;
}
2.4 建立初始表结构
根据给定初始状态建立初始的表结构,找到0的位置即为可移动的空格位置并记录,找到得到hash值并记录
VertexOfGraph createGraph(int vertex[3][3]) {
VertexOfGraph returnGraph;
for(int i=0; i < 3; i++) {
for(int j = 0; j <3; j++) {
returnGraph.vertex[i][j] = vertex[i][j];
if(vertex[i][j] == 0) {
returnGraph.x = i;
returnGraph.y = j;
}
}
}
returnGraph.hashValue = getHash(vertex);
return returnGraph;
}
2.5 bfs广度搜索
搜索结果返回一个map表,将每一状态转换之后的状态作为key值,原状态作为value,一一对应保存起来,最后通过此表可得到实际路径
map<string,string> bfsSearch(VertexOfGraph graph,string targetHash,map<string,bool> visited) {
int direction[4][2] = { {-1,0}, {0,1}, {1,0}, {0,-1} };
queue<VertexOfGraph> que;
que.push(graph);
map<string,string> route;
while(!que.empty()) {
VertexOfGraph movingVertex = que.front();
//cout << movingVertex.hashValue << endl; //测试遍历结果
VertexOfGraph savingVertex = movingVertex;
que.pop();
for(int i = 0; i < 4; i++) {
movingVertex = savingVertex;
movingVertex.x = savingVertex.x + direction[i][0];
movingVertex.y = savingVertex.y + direction[i][1];
if(!movingVertex.isValid()) {
continue;
} else {
movingVertex.newStatus(savingVertex.x, savingVertex.y);
movingVertex.hashValue = getHash(movingVertex.vertex);
if(visited.find(movingVertex.hashValue)==visited.end()) {
visited[movingVertex.hashValue] = true;
route[movingVertex.hashValue] = savingVertex.hashValue;
que.push(movingVertex);
}
if(movingVertex.hashValue == targetHash) {
return route;
}
}
}
}
return route;
}
bfs的想法是,从第一个状态开始遍历可能的所有状态,对于当前状态,找出其可以转换的状态,将他们全部放入队列的尾部,保存route,并判断他们是否是目标状态,若是,返回route退出,否则不停取出队列的头元素,执行相同操作
2.6 找回实际路径
从route和目标hash值中找回实际路径,并输出
void display(map<string,string> searchRoute,string targetHash) {
stack<string> shortestRoute;
shortestRoute.push(targetHash);
string nextWay = targetHash;
while( searchRoute.find(nextWay) != searchRoute.end() ) {
nextWay = searchRoute[nextWay];
shortestRoute.push(nextWay);
}
cout << endl;
int counts = 0;
while( !shortestRoute.empty() ) {
string displayString = shortestRoute.top();
shortestRoute.pop();
if( (counts == 0) && (displayString == targetHash) ) { //如果堆栈内只有一个字符串元素,且等于targetHash,那么说明遍历了所有的状态也无法达到目标状态
cout << "Sorry, but you can not reach the target status, and you can try another status instead." << endl;
break;
}
cout << "Step " << counts << " :";
counts++;
for(unsigned int i = 0; i < displayString.length(); i++) {
if( i % 3 == 0 ) {
cout << endl;
}
cout << displayString.substr(i,1) << ' ';
}
cout << endl;
}
if( counts > 1 ) {
cout << "Totally, you used " << counts-1 << " steps to the target status." << endl;
}
}
源码
源码可以在这里看到