1、问题重述
3×3九宫棋盘,放置数码为1 -8的8个棋牌,剩下一个空格,通过数字向空格的移动来改变棋盘的布局。
要求:根据给定初始布局(即初始状态)和目标布局(即目标状态),如何移动棋牌才能从初始布局到达目标布局,找到合法的过程序列。
2、 问题分析:
(1)对于八数码问题的解决,每一个状态可认为是一个1×9的矩阵,问题即通过矩阵的变换,由数学知识可知,可计算这两个有序数列的逆序值,如果两者都是偶数或奇数,则可通过变换到达(前提不包含0),否则,这两个状态不可达。这样,就可以在具体解决问题之前判断出问题是否可解,从而可以避免不必要的搜索。
(2)常用的状态空间搜索有深度优先和广度优先。广度和深度优先搜索有一个很大的缺陷就是他们都是在一个给定的状态空间中穷举。效率实在太低,甚至不可完成。
(3)由于八数码问题状态空间共有9!个状态,对于八数码问题如果选定了初始状态和目标状态,有9!/2个状态要搜索,考虑到时间和空间的限制,在这里采用A*算法作为搜索策略,在这里就要用到启发式搜索。
3、(1)启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径,提高了效率。在启发式搜索中,对位置的 估价是 十分重要的。采用了不同的估价可以有不同的效果。
(2)启发中的估价是用估价函数表示的,如:f(n) = g(n) + h(n)
其中f(n) 是节点n的估价函数,g(n)是在状态空间中从初始节点到n节点的实际代价,h(n)是从n到目标节点最佳路径的估计代价。在这里主要是h(n)体现了搜索的启发信息,因为g(n)是已知的。如果说详细 点,g(n)代表了搜索的广度的优先趋势。但是当h(n) >> g(n)时,可以省略g(n),而提高效率。
4、A*算法原理:
在计算机科学中,A*算法作为Dijkstra算法的扩展,因其高效性而被广泛应用于寻路及图的遍历,在理解算法前,我们需要知道几个概念:
- 搜索区域(The Search Area):图中的搜索区域被划分为了简单的二维数组,数组每个元素对应一个小方格,当然我们也可以将区域等分成是五角星,矩形等,通常将一个单位的中心点称之为搜索区域节点(Node)。
- 开放列表(Open List):我们将路径规划过程中待检测的节点存放于Open List中,而已检测过的格子则存放于Close List中。
- 父节点(parent):在路径规划中用于回溯的节点,开发时可考虑为双向链表结构中的父结点指针。
- 路径排序(Path Sorting):具体往哪个节点移动由以下公式确定:F(n) = G + H 。G代表的是从初始位置A沿着已生成的路径到指定待检测格子的移动开销。H指定待测格子到目标节点B的估计移动开销。
- 启发函数(Heuristics Function):H为启发函数,也被认为是一种试探,由于在找到唯一路径前,我们不确定在前面会出现什么障碍物,因此用了一种计算H的算法,具体根据实际场景决定。在我们简化的模型中,H采用的是传统的曼哈顿距离(Manhattan Distance),也就是横纵向走的距离之和。
A*算法过程:
(1). 把起点加入 open list 。
(2). 重复如下过程:
a. 遍历open list ,查找F值最小的节点,把它作为当前要处理的节点,然后移到close list中
b. 对当前方格的相邻方格一一进行检查,如果它是不可抵达的或者它在close list中,忽略它。否则,做如下操作:
□ 如果它不在open list中,把它加入open list,并且把当前方格设置为它的父亲
□ 如果它已经在open list中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更近。如果更近,把它的父亲设置为当前方格,并重新计算它的G和F值。如果你的open list是按F值排序的话,改变后你 可能需要重新排序。
c. 遇到下面情况停止搜索:
□ 把终点加入到了 open list 中,此时路径已经找到了,或者
□ 查找终点失败,并且open list 是空的,此时没有路径。
(3). 从终点开始,每个方格沿着父节点移动直至起点,形成路径。
4、过程:
在八数码问题中,g(n)可以定义为从初始节点走到当前节点时走过的步数,h(n)可以定义为
1、不在位的将牌数,例如在上面的例子中,不在位的将牌数为7
2、当前节点与目标节点对应将牌的欧式距离(直线距离)
5、代码示例:
#include <iostream>
#include <time.h>
using namespace std;
int const MAXSIZE = 1000;
int const MAXSTEPS = 100;
struct Node
{
int status[9];//存放数字状态
int g;//从节点到指定节点的代价
int h;//从节点到指定节点的估算成本
int o;//消耗
int zero;//0所在的位置
int step;//得到当前节点的step(1上2下3左4右)
Node* parent;//存放父亲节点
};
//目标状态
int status0[9] = { 1, 2, 3, 8, 0, 4, 7, 6, 5 };
Node Open[MAXSIZE];//用于存放那些搜索图上的叶节点,也就是已经被生成出来,但是还没被扩展的节点,用于后面的搜索操作
Node Close[MAXSIZE];//用于存放那些搜索图上的叶节点,已经被扩展的节点
int o = 0;//表示数组最后一个下标
int c = 0;
Node* node;
/*
*
1、Open中的节点按照 o 值从小到大排列。每次从Open表中取出第一个元素 n 进行扩展(最小消耗)
如果是w目标节点(即h=0),则算法找到一个解,算法结束,否则继续扩展该节点;
2、对于 w的子节点 m ,如果 m 既不在Open也不在Close, 则将 m 加入Open;
如果 m 在Close, 如果新路径耗散值大,则保持不动
如果较小,则将 m 从Close中取出放入Open中 循环此过程,直到找到最优解
/*
* h 估算成本 (欧氏距离)
* 计算该节点与目标节点的相同数字位置的欧氏距离
*/
int CountH(int* status) {
int h = 0;
int i, j;
for (i = 0; i < 9; i++)
{
for (j = 0; j < 9; j++)
{
if (status0[j] == status[i])//两个数字相同的位置
break;
}
h = h + sqrt(pow((i % 3 - j % 3), 2) + pow((i / 3 - j / 3), 2));//欧式距离
}
return h;
}
/*
* 对新节点进行初始化赋值
* 返回该节点
*/
Node* initNode(int* status, int zero, int g, Node* parent, int step)
{
int i;
node = new Node;
for (i = 0; i < 9; i++)
{
node->status[i] = status[i];
}
node->zero = zero;
node->g = g;
node->h = CountH(node->status);
node->o = node->g + node->h;
node->parent = parent;
node->step = step;
return node;
}
/*
* 判断新生成的节点是否已经存在于FInd表或Close表中
* 0表示二者都不存在
* >0表示在Open表中
* <0表示在Close表中
* +1所在的位置
*/
int ExistOfList(Node* n)
{
int i, j;
int h = 0; //计数是否存在
int status[9];
node = new Node;
node = n;
for (i = 0; i < 9; i++)
{
status[i] = node->status[i];
}
//判断是否在Open表
for (i = 0; i <= o - 1; i++)
{
for (j = 0; j < 9; j++)
{
if (status[j] != Open[i].status[j])
{
h++;//不相同累计计数;
}
}
if (h == 0) //h=0证明在表中找到该节点
{
return i + 1; //如果在Open表中,返回i+1
}
h = 0; //扫描完一个节点后重置h,为下一趟遍历服务
}
//判断是否在Close表中
for (i = 0; i <= c - 1; i++)
{
for (j = 0; j < 9; j++)
{
if (status[j] != Close[i].status[j])
{
h++;
}
}
if (h == 0) //h=0证明在表中找到该节点
{
return -(i + 1);
}
h = 0; //扫描完一个节点后重置h
}
return 0; //存在于两表中;
}
/*
* 根据0的位置进行移动操作,更新该节点的状态
* 左右移动1
* 上下移动3
*/
int* LEFT(int* s, int index)
{
int temp, i;
static int status[9];
for (i = 0; i < 9; i++)
{
status[i] = s[i];
}
temp = status[index - 1];
status[index - 1] = 0;
status[index] = temp;
return status;
}
int* RIGHT(int* s, int index)
{
int temp, i;
static int status[9];
for (i = 0; i < 9; i++)
{
status[i] = s[i];
}
temp = status[index + 1];
status[index + 1] = 0;
status[index] = temp;
return status;
}
int* UP(int* s, int index)
{
int temp, i;
static int status[9];
for (i = 0; i < 9; i++)
{
status[i] = s[i];
}
temp = status[index - 3];
status[index - 3] = 0;
status[index] = temp;
return status;
}
int* DOWN(int* s, int index)
{
int temp, i;
static int status[9];
for (i = 0; i < 9; i++)
{
status[i] = s[i];
}
temp = status[index + 3];
status[index + 3] = 0;
status[index] = temp;
return status;
}
/*
*筛选新节点
* 对于新的子节点 m ,如果 m 既不在Open也不在Close, 则将 m 加入Open;
如果 m 在ExistOfList,说明从初始节点到 m 有路径, 如果新路径耗散值大,不进行任何操作;
如果较小,则将 m 从Close中取出放入Open中(删除原来在Close的,将新找到的放入Open)
*/
void opreate(Node* n)
{
int i;
int result;
node = new Node;
node = n;
//第一步操作,直接加入Open
if (node->g == 1)
{
Open[o] = *node;
o++;
return;
}
result = ExistOfList(node);
if (result == 0) //如果均不在两个表中,将节点加入Open表中
{
Open[o] = *node;
o++;
}
else if (result > 0) //存在于Open中
{
if (Open[result - 1].o > node->o) //若p.o小于当前位置的o,则将其替换
{
Open[result - 1] = *node;
}
}
else if (result < 0) //存在于Close
{
result = -result;
if (Close[result - 1].o > node->o) //若p.o小于当前位置的o,则将其放入Open中,并在Close中释放
{
Open[o] = *node;
o++;
}
for (i = result - 1; i <= c - 1; i++) //在Close种删除当前位置的节点
{
Close[i] = Close[i + 1];
}
c--;
}
}
void change(Node* t) {
int* status;
if ((t->zero) % 3 >= 1) //左移,则进行左移创造新结点
{
node = new Node; //创造新结点
status = LEFT(t->status, t->zero); //通过移动得到新的状态
node = initNode(status, t->zero - 1, (t->g) + 1, t, 1); //对新节点进行初始化,g为新的一步
opreate(node); //判断子节点是否在Open或Close中,并进行对应的操作
}
if ((t->zero) % 3 <= 1)
{
status = RIGHT(t->status, t->zero);
node = initNode(status, t->zero + 1, (t->g) + 1, t, 2);
opreate(node);
}
if (t->zero >= 3)
{
node = new Node;
status = UP(t->status, t->zero);
node = initNode(status, t->zero - 3, (t->g) + 1, t, 3);
opreate(node);
}
if (t->zero <= 5)
{
node = new Node;
status = DOWN(t->status, t->zero);
node = initNode(status, t->zero + 3, (t->g) + 1, t, 4);
opreate(node);
}
}
/*
* 根据Open找到最优解
* 每次取出Open最小值,判断该值的估算成本的欧式距离为0,进行移动操作,加入到表中,直到h=0,得到最优解
*/
Node* Search()
{
int i, j;
Node* t;
while (true)
{
if (o == 0) //如果o=0, 没有解
return NULL;
t = new Node;
//排序找到最小消耗
for (i = o - 1; i > 0; i--)
{
for (j = 0; j < i; j++)
{
if (Open[j].o > Open[j + 1].o)
{
*t = Open[j + 1];
Open[j + 1] = Open[j];
Open[j] = *t;
}
}
}
node = new Node;
//取最小消耗
*node = Open[0];
if (CountH(node->status) == 0) //找到最优解
{
break;
}
//不为最优解,将其扩展到Close表中,同时在Open中删除
t = node;
Close[c] = *node;
c++;
for (i = 0; i <= o - 1; i++)
{
Open[i] = Open[i + 1];
}
o--;
change(t);
}
return node;
}
/*
*显示移动过程
*/
void show_all(Node* node)
{
int status[100][9];//保存每个节点的状态;
int step = 0;
int i, j;
int total = node->g;
while (node)
{
for (i = 0; i < 9; i++)
{
status[step][i] = node->status[i];
}
step++;
node = node->parent;//向上输出
}
cout << "----------------------" << endl;
cout << "一共执行了:" << total << "步" << endl;
cout << "----------------------" << endl;
for (i = step - 1; i >= 0; i--)
{
for (j = 0; j < 9; j++)
{
cout << status[i][j];
if (j == 2 || j == 5 || j == 8)
cout << endl;
else
cout << " ";
}
cout << "----------------------" << endl;
}
}
//判断逆序数(不包含0)
int inverse_num(int a[], int n) {
int sum = 0;
int i, j;
for (i = 0;i < n;i++)
{
for (j = 0;j < i;j++)
{
if (a[i] != 0 && a[j] != 0 && a[j] > a[i])
{
sum = sum + 1;
}
}
}
return sum;
}
/*
主函数
*/
int main()
{
int status[9];
int i, beginTime, endTime;
Node* p;//初始状态
Node* q;
cout << "请输入初始状态:" << endl;
for (i = 0; i < 9; i++) //输入初始状态
{
cin >> status[i];
}
int s1 = inverse_num(status, 9);
int s2 = inverse_num(status0, 9);
cout << "初始状态逆序数:" << s1 << endl;
cout << "目标状态逆序数:" << s2 << endl;
if (!((s1 % 2) == (s2 % 2))) {//同奇或偶,才可达
cout << "逆序数不同奇或偶,无解" << endl;
return 0;
}
beginTime = clock();
for (i = 0; i < 9; i++) //判断0节点位置
{
if (status[i] == 0)
break;
}
p = initNode(status, i, 0, NULL, 0); //获得初始节点
Open[o] = *p; //将初始节点放入Open中
o++;
q = Search();
if (!q)
cout << "无解" << endl;
else
show_all(q);
endTime = clock();
cout << "Run Time:" << endTime - beginTime << "ms" << endl;
return 0;
}
结果:
请输入初始状态:
2
1
6
4
0
8
7
5
3
----------------------
一共执行了:18步
----------------------
2 1 6
4 0 8
7 5 3
----------------------
2 1 6
4 8 0
7 5 3
----------------------
2 1 0
4 8 6
7 5 3
----------------------
2 0 1
4 8 6
7 5 3
----------------------
2 8 1
4 0 6
7 5 3
----------------------
2 8 1
4 6 0
7 5 3
----------------------
2 8 1
4 6 3
7 5 0
----------------------
2 8 1
4 6 3
7 0 5
----------------------
2 8 1
4 0 3
7 6 5
----------------------
2 8 1
0 4 3
7 6 5
----------------------
0 8 1
2 4 3
7 6 5
----------------------
8 0 1
2 4 3
7 6 5
----------------------
8 1 0
2 4 3
7 6 5
----------------------
8 1 3
2 4 0
7 6 5
----------------------
8 1 3
2 0 4
7 6 5
----------------------
8 1 3
0 2 4
7 6 5
----------------------
0 1 3
8 2 4
7 6 5
----------------------
1 0 3
8 2 4
7 6 5
----------------------
1 2 3
8 0 4
7 6 5
----------------------