前一阵子上人工智能实验课做过八数码的题目,在博客中放上自己的实验过程
代码地址
实验环境
本次实验的编程语言为C++,运行环境为Windows10,使用的集成开发环境为VS2015
八数码游戏问题简介
九宫排字问题(又称八数码问题)是人工智能当中有名的难题之一。问题是在 3×3方格盘上,放有八个数码,剩下第九个为空,每一空格其上下左右的数码可移至空格。
问题给定初始位置和目标位置,要求通过一系列的数码移动,将初始位置转化为目标位置。
求解思路
对于八数码问题的求解,由于每次只将空格点与上、下、左、右四个位置的值进行交换,所以每个状态只需要对这四个点进行判断。
首先判断与空格相邻的点是否存在:
若存在,则根据启发式算法的思路f(x) = g(x) + h(x)
定义g(x)为某一数值从当前状态的坐标(与空格点相邻)直接到终止状态该数值坐标的移动距离,如图红线。
注:g(x)=0表示当前状态的点与终止状态对应点的值不变,即该值已经到达终止状态的目标点。
定义h(x)为某一数值经过空格点后到达终止状态该数值坐标的移动距离,如图黑线。
定义f(x) = g(x) – h(x) 若 f(x) < 0 则代表当前状态该数值经过空格点后移动到终点坐标的距离要比直接到达终点坐标的距离要多,这时不对该点进行操作,由于移动的方向只能是上、下、左、右,故不存在f(x) = 0(g(x) = h(x))的情况,否则(f(x) > 0)该点与空格点进行交换,表明通过该相邻点空格点的移动后距离终止状态该值的坐标更近。
例如:某一时刻状态如左图所示,终止位置如右图所示,圆圈表示空格相邻点与终止位置相邻点所处的位置,下面用图例来表明解决八数码问题的具体思路,对于中间某一状态的分析来解决下一次往哪一点移动。
对于中间状态左点坐标为(1,0),终止状态对应值坐标为(0,0),需移动1-0+0=1步到达,g(x) = 1,而从空格点(1,1)到达(0,0)需移动|1-0|+|1-0|=2步,h(x) = 2,g(x) < h(x),f(x) = -1<0不满足条件,表明左点 不与 空格点进行交换移动。(|a-b|表示a-b的绝对值)
对于中间状态上点坐标为(0,1),终止状态对应值坐标为(1,0),需移动|0-1|+|1-0|=2步到达,g(x) = 2,而从空格点(1,1)到达(1,0)需移动|1-1|+|1-0|=1步,h(x) = 1,f(x) =1> 0,满足条件,表面上点 可以 与空格点进行交换移动。
对于中间状态右点坐标为(1,2),终止状态对应值坐标为(1,2),需移动|1-1|+|2-2|=0步到达,g(x) = 0,而从空格点(1,1)到达(1,2)需移动|1-1|+|1-2|=1步,h(x) = 1,f(x) =-1< 0,不满足条件,表面右点 不与 空格点进行交换移动。
对于中间状态下点坐标为(2,1),终止状态对应值坐标为(2,1),需移动|2-2|+|1-1|=0步到达,g(x) = 0,而从空格点(1,1)到达(2,1)需移动|1-2|+|1-1|=1步,h(x) = 1,f(x) =-1< 0,不满足条件,表面下点 不与 空格点进行交换移动。
综上所述,空格点下一次要移动的位置是(0,1)
数据结构的设计
本次实验采用面向对象的思想,将八数码问题封装成一个模板类(实际上整形的类即可), 类视图如下:
该类的成员变量为:
current_point 当前点的坐标;
final_state 终止状态;
initial_state 初始状态;
move_point 待移动点坐标;
state 中间状态;
成员函数为:
void solve(); 整合其他函数,解决八数码问题的主函数;
bool have_answer();判断是否有解;
bool is_equal();判断中间状态和终止状态是否相等;
void exchange_point(int time); 输入为第几轮数,待移动点与当前点进行交换;
void get_next_point();得到待移动点坐标;
int is_close(int );输入为进行判断的坐标,对输入坐标和当前坐标的代价进行判定,f(x)<0,则返回-1,若f(x)>0,则返回h(x)。
思路及代码
首先重载一个含参的构造函数
template
Eight_figure::Eight_figure(vector> init, vector> fin) {
initial_state = init;
state = init;
final_state = fin;
for (int i = 0; i < 3; i++){
for (int j = 0; j < 3; j++) {
if (initial_state[i][j] == 0)
current_point = i*3 + j;
}
}
cout << "初始状态为:" << initial_state << endl;
cout << "终止状态为:" << final_state << endl;
cout << "当前空点的坐标为" << "(" << current_point / 3 << "," << current_point % 3 << ")" << endl;
}
输入为初始状态和终止状态,在main函数中定义两个数组后,调用构造函数进行输入;
接着把解决八数码问题的主要步骤表示出来,在solve()函数中,
首先判断问题是否有解,若无解,结束程序,
若有解,
1.判断初始状态与终止状态是否相等;
2.得到待移动点的坐标;
3.将空格与待移动点进行交换;
4.返回步骤1
同时对计算时间进行计时。
solve核心代码如下:
template
void Eight_figure::solve() {
if (!have_answer()) cout << "该变换无解" << endl;
else {
int time = 0;
clock_t start, finish;
double totaltime;
start = clock();
while (!is_equal()) {
get_next_point();
exchange_point(++time);
}
finish = clock();
totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "变换结束 用时" << totaltime << "秒!" << endl;
}
}
在have_answer()中,对初始状态和终止状态数组分别求解其逆序数,若前面存在大于该位的值,则计数值+1,对空格位置不进行计算。
int count1 = 0,count2 = 0;
for (int i = 0; i < 9; i++)//求得初始状态逆序数 用一维数0-8表示二维坐标 ( i/3,i%3 )
{
if (initial_state[i / 3][i % 3] == 0) continue;
for (int j = 0; j < i; j++) {
if (initial_state[j / 3][j % 3]> initial_state[i / 3][i % 3]) count1++;
}
}
for (int i = 0; i < 9; i++)//求得终止状态逆序数
{
if (final_state[i / 3][i % 3] == 0) continue;
for (int j = 0; j < i; j++) {
if (final_state[j / 3][j % 3]> final_state[i / 3][i % 3]) count2++;
}
}
求得后对两个逆序数进行奇偶性的判断,相同返回true,不同返回false
return count1 % 2 == count2 % 2;
在is_equal()中,判断中间状态state和终止状态final_state是否相等,判断方法为对应位判断,只要有一位不同,则返回false,否则返回true
bool Eight_figure::is_equal() {
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++) {
if (state[i][j] != final_state[i][j])
return false;
}
}
return true;
}
在is_close(int )中,输入为进行判断的坐标,该坐标是与空格点相邻的一个坐标,首先找到输入坐标上的值在终止状态下的位置,然后分别计算输入坐标和当前坐标到终止状态该值位置所需移动的步数,记为before和after(即g(x)和h(x)),若变换前所需步数少,则返回-1,代表不能进行交换,若变换后所需步数少,则返回变换后的步数。
template
int Eight_figure::is_close(int side) {
int after = 0, before = 0;
for (int i = 0; i < 9; i++)
{
if (final_state[i / 3][i % 3] == state[side / 3][side % 3])
{
before = abs(side / 3 - i / 3) + abs(side % 3 - i % 3);
after = abs(current_point / 3 - i / 3) + abs(current_point % 3 - i % 3);
if (after > before) return -1;
else
return after;
}
}
实现完is_close() 之后就可以实现get_next_point()了,这个函数依次计算上左右下四个位置的代价,若某一相邻点存在,调用is_close()对该点计算变换前后所需的步数,若步数为-1,则对下一个相邻点做判断,若步数为0,说明该空格点坐标在终止位置的值就是该相邻点位置的值,此时待移动点记为该相邻点,退出函数,否则记录该相邻点为待移动点后继续对其他相邻点做判断。
template
void Eight_figure::get_next_point() {
int up = current_point - 3, left = current_point - 1, right = current_point + 1, down = current_point + 3,step =0;
if (up >= 0) //判断上点上否存在
{
step = is_close(up);//取得若与上点进行交换 交换后的上点的数值到达终止状态目标点的步数
if (step != -1)//若交换后需要移动的步数变少
{
move_point = up;//移动点改为上点
if(step == 0)//若空点当前位置的值恰好是上点的值则返回上点
return;
}
}
if ((left+1) % 3 != 0) //对于左点
{
step = is_close(left);
if (step != -1){
move_point = left;
if(step == 0)
return;
}
}
if (right%3!= 0) //对于右点
{
step = is_close(right);
if (step != -1)
{
move_point = right;
if (step == 0)
return;
}
}
if (down <= 8) //对于下点
{ step = is_close(down);
if (step != -1)
{
move_point = down;
if (step == 0)
return;
}
}
}
在得到待移动点后,在中间状态的数组中将带移动状态与当前状态位置上的值交换,并将当前位置的坐标置为待移动点坐标,待移动点坐标变为-1,exchange_point(time)函数如下,输入为第几轮的轮数。
template
void Eight_figure::solve() {
if (!have_answer()) cout << "该变换无解" << endl;
else {
int time = 0;
clock_t start, finish;
double totaltime;
start = clock();
while (!is_equal()) {
get_next_point();
exchange_point(time);
time++;
}
finish = clock();
totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "变换结束 用时" << totaltime << "秒!" << endl;
}
}
最后在main函数中,定义一个该模板类的对象,将初始状态和终止状态作为构造函数的输入,调用该类的solve()函数即可。
int main(){
vector>init = { { 6,7,8 },{ 3,4,5 },{ 1,0,2 } };
vector>fin={ { 3,6,7 },{ 1,4,8 },{ 0,2,5 } };
Eight_figureef(init, fin);
ef.solve();
return 0;
}
实验结果
测试1:初始状态和终止状态如下图所示:
经过5轮变换即可完成从初始状态到终止状态的变换,分别是:
测试2:初始状态和终止状态如下图所示:该终止状态是由初始状态的外圈进行一次顺时针旋转形成的。
经过7轮变换即可完成从初始状态到终止状态的变换,分别是: