问题描述

3×3九宫棋盘,放置数码为1 -8的8个棋牌,剩下一个空格,只能通过棋牌向空格的移动来改变棋盘的布局。

要求:根据给定初始布局(即初始状态)和目标布局(即目标状态),如何移动棋牌才能从初始布局到达目标布局,找到合法的走步序列。

求解思路

八数码问题是一个典型的隐式图搜索问题。我们可以通过图建模将八数码转换为图。八数码每个状态可以用一个九维数组一一对应,我们把它作为图的节点,把空格的上下左右移动对应状态的转移,我们把他作为边。

每个状态可以用一个九维数组一一对应,State定义为:
typedef int State[9];

那么八数码问题就转换为一个图搜索问题。从开始状态寻找,逐步找到目标状态。这里我们重点实现选择扩展节点(启发式搜索)问题与避免节点重复扩展(hash)问题。

hash问题

我们注意到,八数码的每个状态由有序的九个数组成。共有9!=366880个。
我们固然可以将每个状态映射为一个整形数,其最大也不会超过1e10。但要注意,我们没有112233440这种状态,八数码中每个数是不能出现在两个位置上的。这样就产生了极大的空间浪费。
粗略估计的空间占用率为3668801e10\tfrac{366880}{1e10}1e10366880​,这是一个非常小的数。
为此,我们可以引入另外一个hash策略:将每个九元数看做一个字符串,对其按照字典序排列。
每个状态的hash值就是比它字典序要小的状态的个数。比如012345678,没有比它字典序最小的状态了,因此其hash值为0。876543210字典序最大,hash值为9!-1
而对于任意一种状态,我们引入hash函数如下:

//得到对应的编码
int get_code(State st)
{int code = 0;for (int i = 0; i < 9; i++){int cnt = 0;for (int j = i + 1; j < 9; j++)//求每个位置i后面比它小的元素的个数cnt{if (st[j] < st[i])cnt++;}//位置i的数可以重写为cnt个其他数,后面的数任意code += fact[8 - i] * cnt;}return code;
}

这里利用到了排列组合的一个公式,对于n1个a,n2个b,n3个c。 abc的全部排列个数为(n1+n2+n3)!n1!∗n2!∗n3!\tfrac{(n1 + n2 + n3)!}{n1! * n2! *n3!}n1!∗n2!∗n3!(n1+n2+n3)!​

启发式搜索问题

搜索算法可分为两大类:盲目搜索和启发式搜索。
盲目搜索又称无信息的搜索,广度优先搜索、深度优先搜索都是典型的盲目搜索。我们说DFS和BFS都是蛮力搜索,因为它们扩展节点是盲目的。对于同一个节点扩展出的多个子节点,它并不考虑这些子节点对解决问题的好坏之分,而是盲目的扩展。
在有些情况下,我们还需要一些启发式信息以便快速找到目标节点。在这里我们引入评价函数F。评价函数是用来评价待扩展的好坏的。我们每次扩展节点总是选择评价最好的节点做扩展。

  1. 评价函数F = dis + v,对应每一个节点。dis代表从原节点到当前节点所用步数,v代表启发函数值。dis用来描述从开始节点扩展到当前节点付出的代价,v用来描述从当前节点到目标节点的代价的估计。
  2. open表:里面放待扩展节点,每次选择评价函数最小的节点做扩展。我们使用bfs策略扩展节点。
  3. close表,存放已经扩展了的节点。注意到我们要在close表中记录该状态对应的最优F值
    如果我们又扩展到了已经在close表中的节点,那么我们就要比较二者的F值,若这次扩展的F值比close表中要小,不予理会。否则要将其放入open表中并更新close表。
  4. 在《人工智能 模型与算法》书中,对启发函数v的选取有一定要求。大抵是图搜索需要满足可容性与一致性,树搜索需要满足可容性。我们解决八数码应该是用树搜索的形式做一个图搜索。这里了解一下就好。

数据结构的选择

open表

open表每次选择评价函数最小的节点做扩展。一些作者选择用map来实现,但我认为这是冗余的,我们不需要对整个open表排序,只需要每次都能查找评价函数最小的节点即可。
这里选用priority_queue(最大堆)做open表。

close表

利用前面的hash策略,我们已经能够保证用9!大小的数组存放所有状态。close表用数组表示即可。

节点

如前所述,八数码的每个状态用typedef int State[9]表示。
dis用来描述从开始节点扩展到当前节点付出的代价,v用来描述从当前节点到目标节点的代价的估计。二者组成评价函数F。
运算符重载配合最大堆实现。

class Node
{friend class Solution;
public:Node(State s, int dd, int vv);Node(const Node& rhs);bool operator<(const Node& rhs) const;friend ostream& operator<<(ostream& os, const Node& t);private:State st;int v, dis;
};

启发函数

我们在这里提出了两种启发函数,二者的性能还是有很大差异的,我们在下面的测试中会给出结果

//利用棋盘相同位置不同元素的数目做启发int Solution::get_v(State s)
{int cnt = 0;for (int i = 0; i < 9; i++){if (s[i] != goal[i])cnt++;}return cnt;
}
//利用每个数字在当前节点与目标节点之间的曼哈顿距离做启发
int Solution::get_v(State s)
{int ans = 0;for (int i = 0; i < 3; i++){for (int j = 0; j < 3; j++){int key = s[i * 3 + j];int z = 0;for (z = 0; z < 9; z++){if (goal[z] == key)break;}int zx = z / 3;int zy = z % 3;ans += abs(i - zx) + abs(j - zy);}}return ans;
}

测试

衡量启发式搜索策略优劣的重要指标是总共扩展的节点数目。
测试用例:
初始状态:2 6 4 1 3 7 0 5 8
目标状态:8 1 5 7 3 6 4 0 2

扩展策略 扩展节点总数
盲目bfs 181441
启发函数A 145323
启发函数B 16596

启发函数B(使用每个数字的曼哈顿距离)的效果比是最好的。在设计启发式搜索时,启发函数的设计影响很大。

代码

启发式函数版本,使用c++面向对象书写

#include <iostream>
#include <algorithm>
#include <set>
#include <cstring>
#include <vector>
#include <queue>
#include <fstream>
#include <memory>
#include <cmath>using namespace std;typedef int State[9];class Solution;
class Node
{friend class Solution;
public:Node(State s, int dd, int vv);Node(const Node& rhs);bool operator<(const Node& rhs) const;friend ostream& operator<<(ostream& os, const Node& t);private:State st;int v, dis;
};
class Solution
{friend class Node;
public:Solution();void Result();private:int get_code(State);int get_v(State);int bfs();const int maxstate = 1e6;State start, goal;int fact[10];priority_queue<Node> open_que;  //open表,记录待扩展节点unique_ptr<int[]> vis;  //作为close表,保存每一个状态的当下的最好情况int cnt = 0;           //记录总共扩展的节点个数const int dx[4] = { -1, 1, 0, 0 };const int dy[4] = { 0, 0, -1, 1 };
};
int main()
{Solution t;t.Result();return 0;
}Solution::Solution() :vis(new int[maxstate])
{memset(vis.get(), 0x3f, sizeof(int) * maxstate);fact[0] = 1;for (int i = 1; i < 10; i++)fact[i] = fact[i - 1] * i;//cout << "请输入初始状态表" << endl;for (auto& it : start)cin >> it;//cout << "请输入目标状态表" << endl;for (auto& it : goal)cin >> it;//cout << endl;
}//得到对应的编码
int Solution::get_code(State st)
{int code = 0;for (int i = 0; i < 9; i++){int cnt = 0;for (int j = i + 1; j < 9; j++){if (st[j] < st[i])cnt++;}code += fact[8 - i] * cnt;}return code;
}
//利用每个数字的曼哈顿距离做启发
int Solution::get_v(State s)
{int ans = 0;for (int i = 0; i < 3; i++){for (int j = 0; j < 3; j++){int key = s[i * 3 + j];int z = 0;for (z = 0; z < 9; z++){if (goal[z] == key)break;}int zx = z / 3;int zy = z % 3;ans += abs(i - zx) + abs(j - zy);}}return ans;
}
//利用棋盘相同位置不同元素的数目做启发
// int Solution::get_v(State s)
// {//     int cnt = 0;
//     for (int i = 0; i < 9; i++)
//     {//         if (s[i] != goal[i])
//             cnt++;
//     }
//     return cnt;
// }//返回目标状态在st数组下标
int Solution::bfs()
{//把第一个节点放入并标记int code = get_code(start);vis[code] = get_v(start);open_que.push({ start, 0, vis[code] });cnt = 1;while (!open_que.empty()){Node now = open_que.top();open_que.pop();//如果到达目标状态if (memcmp(goal, now.st, sizeof(now.st)) == 0)return now.dis + now.v;//进行宽度优先搜索State& s = now.st;int z;for (z = 0; z < 9; z++){if (s[z] == 0) //找0的位置break;}int x = z / 3, y = z % 3; //获取行列编号for (int d = 0; d < 4; d++){int newx = x + dx[d];int newy = y + dy[d];int newz = newx * 3 + newy;                         //0的新位置if (newx >= 0 && newx < 3 && newy >= 0 && newy < 3) //如果移动合法{State tmp; //tmp为转移到的新状态memcpy(&tmp, &s, sizeof(s));std::swap(tmp[newz], tmp[z]);int value = get_v(tmp);int dis = now.dis + 1;int code = get_code(tmp);//做更新if (value + dis < vis[code]){cnt++;open_que.push({ tmp, dis, value });vis[code] = value + dis;}}}}return -1;
}
void Solution::Result()
{cout << "最少需要多少步:" << bfs() << endl;cout << "共扩展节点数:" << cnt << endl;
}Node::Node(State s, int dd, int vv) : dis(dd), v(vv)
{memcpy(st, s, sizeof(st));
}
Node::Node(const Node& rhs) : dis(rhs.dis), v(rhs.v)
{memcpy(st, rhs.st, sizeof(st));
}
bool Node::operator<(const Node& rhs) const
{return (v + dis) > (rhs.v + rhs.dis);
}
ostream& operator<<(ostream& os, const Node& t)
{for (auto it : t.st)os << it << ' ';os << endl;os << "v: " << t.v << ", dis:" << t.dis << endl;return os;
}

面向过程

#include <iostream>
#include <algorithm>
#include <set>
#include <cstring>using namespace std;typedef int State[9];
const int maxstate = 1e6;
State st[maxstate], goal;
int dis[maxstate];const int dx[] = {-1, 1, 0, 0};
const int dy[] = {0, 0, -1, 1};
bool vis[362880];
int fact[10];
int cnt;
void init_lookup_table()
{fact[0] = 1;for (int i = 1; i < 10; i++)fact[i] = fact[i - 1] * i;
}
int try_to_insert(int s)
{int code = 0;for (int i = 0; i < 9; i++){int cnt = 0;for (int j = i + 1; j < 9; j++){if (st[s][j] < st[s][i])cnt++;}code += fact[8 - i] * cnt;}if (vis[code])return 0;return vis[code] = 1;
}//返回目标状态在st数组下标
int bfs()
{init_lookup_table();int front = 1, rear = 2;cnt = 1;while(front < rear){State& s = st[front];if(memcmp(goal, s, sizeof(s)) == 0)return front;int z;for (z = 0; z < 9; z++){if (!s[z])  //找0的位置break;}int x = z / 3, y = z % 3;   //获取行列编号for (int d = 0; d < 4; d++){int newx = x + dx[d];int newy = y + dy[d];int newz = newx * 3 + newy;     //0的新位置if (newx >= 0 && newx < 3 && newy >= 0 && newy < 3)     //如果移动合法{State& t = st[rear];memcpy(&t, &s, sizeof(s));t[newz] = s[z];t[z] = s[newz];dis[rear] = dis[front] + 1;if(try_to_insert(rear)){rear++;cnt++;}}}front++;}return 0;
}
int main()
{freopen("in.txt", "r", stdin);for (int i = 0; i < 9; i++)cin >> st[1][i];for (int i = 0; i < 9; i++)cin >> goal[i];int ans = bfs();cout << dis[ans] << endl;cout << cnt << endl;return 0;
}

人工智能作业 八数码启发式搜索与bfs比较相关推荐

  1. hdu1043 经典的八数码问题 逆向bfs打表 + 逆序数

    题意: 题意就是八数码,给了一个3 * 3 的矩阵,上面有八个数字,有一个位置是空的,每次空的位置可以和他相邻的数字换位置,给你一些起始状态 ,给了一个最终状态,让你输出怎么变换才能达到目的. 思路: ...

  2. 移动拼图游戏(八数码问题) BFS版

    小时候玩过的移动拼图游戏.有一个3*3的棋盘,其中有0-8这9个数字,0表示空格,每次移动只能把空格旁边的数字移到空格,即与0相邻的数字可以和0交换位置. 求从初始状态 2 3 0 7 1 6 5 8 ...

  3. 【人工智能】八数码问题:广度搜索、深度搜索

    应用广度搜索BFS和深度搜索DFS解决八数码问题,广度搜索和深度搜索都是盲目搜索,相关理论知识,算法过程:问题求解:状态空间图和盲目搜索. 参考:7种方法求解八数码问题 Python实现A*算法解决N ...

  4. 【八数码问题】基于状态空间法的知识表示与状态搜索:无信息搜索(BFS/DFS) 启发式搜索(A*)

    前言 一.问题引入 二.状态空间法 1. 知识及其表示 2. 状态空间法定义 3. 问题求解 三.基于状态空间搜索法解决八数码问题 1. 八数码问题的知识表示 2. 状态空间图搜索 1. 无信息搜索 ...

  5. 人工智能----八数码问题(启发式搜索)

    启发式搜索 一. 实验目的 理解人工智能系统中搜索策略的含义. 熟悉盲目搜索和启发式搜索算法的实际应用. 结合八数码问题的建模.求解及编程语言的应用,掌握启发式搜索算法的应用. 二. 实验原理 启发式 ...

  6. 人工智能作业 - A*算法程序求解八数码

    文章目录 A*算法 简介 思路 A*伪代码 八数码问题 题目描述 注意事项 实验过程 解决思路 伪代码 二维压缩为一维 检查是否有解 其他 代码实现 h1和h2的对比 关于曼哈顿距离 参考链接 A*算 ...

  7. A*算法解决八数码问题 人工智能原理实验报告 启发式搜索 python

    目录 一.实验主要步骤 ①.设计界面输入规则 ②.判断是否有解 ③.求解 二.实验结果展示 三.附录 完整实验程序代码: 一.实验主要步骤 ①.设计界面输入规则 有且仅有9位数字代表数码和空格,从左到 ...

  8. 《人工智能》实验二——搜索技术(八数码问题)

    必须记住下一步还可以走哪些点--OPEN表(记录还没有扩展的点) 必须记住哪些点走过了--CLOSED表(记录已经扩展的点 广度优先搜索 在应用BFS算法进行八数码问题搜索时需要open和closed ...

  9. 习题:八数码难题(双向BFS)

    八数码难题(wikioi1225) [题目描述] 在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字.棋盘中留有一个空格,空格用0来表示.空格周围的棋子可以移到空格中.要求解的问题是:给出 ...

最新文章

  1. 什么?iPhone 8发布会当晚,杭州竟然有土豪摔手机砸场子!
  2. 用一句sql语句更新两个表并可更新对应的字段的值
  3. WEB 进程的查看和关闭(kill)
  4. C语言学习之有一个分数序列2/1、3/2、5/3、8/5、13/8、21/13,……求出这个数列的前20项之和。
  5. 【STM32】 keil软件介绍--工具栏
  6. 关于eclipse中web项目tomcat报错Server Tomcat v9.0 Server at localhost failed to start问题解决
  7. java流读取xml_使用FileInputStream(用于Java)读取XML文件?
  8. go语言io reader_go语言之IO操作(待补充)
  9. Codeforces Round #709 (Div. 1) C. Skyline Photo dp + 单调栈优化
  10. POJ--3974 Palindrome(回文串,hash)
  11. Linux命令解释之rpm
  12. 邹城的关于机器人教育_【喜报】我校机器人队问鼎全国大学生机器人大赛冠军!...
  13. JUC 常用 4 大并发工具类:CountDownLatch、CyclicBarrier、Semaphore、Exchanger
  14. 佳能430二代_简单好用 佳能430EX III-RT闪光灯评测
  15. 入学吉他选购,有哪些适合初学者入门的吉他品牌推荐?
  16. layui多选下拉框,多选
  17. B站探索日历-推荐几个UP主
  18. 安卓app开机自启动的几种方式
  19. 邮储社招Java笔试题_2019年及历年中国邮政储蓄银行社招笔试题和参考答案6套
  20. 装修鸿蒙瓷砖选择,我家110平新房,简单装修花16万,还是地板的瓷砖最有档次!...

热门文章

  1. 基于Arduino控制步进电机 编码器普菲特TB6600控制
  2. 成都电子科技大学深圳校区计算机学院导师简介
  3. Gradle sync failed: No variants found for ':app'. Check build files to ensure at least one varian...
  4. OSS服务和自建服务器存储对比
  5. 指纹特征提取及描述(附Python代码)
  6. 大多数人不知道的企业数据分析能力金字塔,你处在哪一级?
  7. k8s核心资源之service四层负载均衡器代理(六)
  8. ad怎么修改栅格_AD 10怎么设置栅格?
  9. 面试官:SPA(单页应用)首屏加载速度慢怎么解决?
  10. [实验吧刷题]密码学部分