广州大学学生实验报告
开课学院及实验室: 计算机科学与网络工程学院电子信息楼416A ****
学院 | 计算机科学与网络工程学院 | 年级/专业/班 | **** | 姓名 | **** | 学号 | ***** |
实验课程名称 | 人工智能原理实验 | 成绩 | |||||
实验项目名称 | 八数码 | 指导老师 | *** |
实验二 八数码问题
- 实验目的
本实验课程是计算机、智能、物联网等专业学生的一门专业课程,通过实验,帮助学生更好地掌握人工智能相关概念、技术、原理、应用等;通过实验提高学生编写实验报告、总结实验结果的能力;使学生对智能程序、智能算法等有比较深入的认识。
- 掌握人工智能中涉及的相关概念、算法。
- 熟悉人工智能中的知识表示方法;
- 熟悉盲目搜索和启发式搜索算法的应用;
- 掌握问题表示、求解及编程实现。
- 掌握不同搜索策略的设计思想、步骤、性能。
二、基本要求
- 实验前,复习《人工智能原理》课程中的有关内容。
- 准备好实验数据。
- 编程要独立完成,程序应加适当的注释。
- 完成实验报告。
三、实验软件
使用C或C++(Visual studio)(不限制语言使用)。
四、实验内容:
(1)
- 在图1,3*3的方格棋盘上,摆放着1到8这八个数码,有1个方格是空。
图1
- 如图1所示,要求对空格执行空格左移、空格右移、空格上移和空格下移这四个操作使得棋盘从初始状态(图1左)到目标状态(图1右)。
- 可自行设计初始状态。目标状态为数字从小到大按顺时针排列。
- 分别用广度优先搜索策略、深度优先搜索策略和启发式搜索算法(A*算法)求解八数码问题;分析估价函数对启发式搜索算法的影响;探究各个搜索算法的特点。
(2)罗马尼亚问题
根据上图以Zerind为初始状态,Bucharest为目标状态实现搜索,分别以贪婪搜索(只考虑直线距离)和A*算法求解最短路径。 按顺序列出贪婪算法探索的扩展节点和其估价函数值,A*算法探索的扩展节点和其估计值。
选做部分:自行设计一个新的启发式函数,并分析该函数的可采纳性和优势(与启发式函数定义为“Zerind到Bucharest的直线距离”相比较)。
五、学生实验报告要求
1、实验报告需要包含以下几个部分
-
- 状态表示的数据结构
- 状态扩展规则的表示
- 搜索产生的状态空间图
-
- OPEN表和CLOSE表变化过程
- 程序清单
- 实验结果讨论
2、思考并解答以下问题
- 你所采用的估价函数f(n) = g(n) + h(n)中,g(n)和h(n)的主要作用是什么?
- 结合本实验举例说明不同启发策略对实验的效果有何影响?(可列出图表说明)
- 若问题的初始状态是随机产生的,你的实验程序应该如何改进?(如图形属性的设置、图形队列存入文件等)添加代码,根据实际需要添加其他辅助函数。
(4) 尝试使用一致代价(等代价)搜索, 迭代加深的深度优先搜索算法求解上述问题,并根据实验结果分析深度优先搜索,一致代价(等代价)搜索,迭代加深的深度优先搜索算法, A*搜索的时间和空间复杂度。
(5) 指出无信息搜索策略和有信息搜索策略的不同并比较其性能。
八数码
-
- 状态表示的数据结构
-
- 状态扩展规则的表示
答 :
x与上下左右四个方向交换位置 :
注意 : x可能在边缘, 因此它交换完之后不能不合法(因此需要对下标的合法性判断)
-
- 搜索产生的状态空间图
-
- OPEN表和CLOSE表变化过程
- OPEN表和CLOSE表变化过程
-
- 程序清单
广度优先搜索 : 完整代码
#include <iostream>
#include <queue>
#include <set>
#include <map>
#include <stack>
using namespace std;
string s; // 矩阵的初始状态
string ed = "1238x4765"; // 最终状态
map<string, string> path;
int dx[] = {0, 0, -1, 1};
int dy[] = {1, -1, 0, 0};
/**
* 输出路径
*/
void printPath() {
string curr = ed;
stack<string> stk;
while(path.count(curr)) {
stk.push(curr);
string pre = path[curr];
curr = pre;
}
stk.push(s);
while(stk.size()) {
string s = stk.top();
stk.pop();
for (int i = 0; i < 9; i ++) {
cout << s[i] << ' ';
if(i % 3 == 2) {
cout << endl;
}
}
cout << endl;
}
}
int main()
{
// string s = "87x465123"; // 不要重新定义变量, 已经有全局变量了
s = "87x465123";
cout << "广度优先搜索\n";
queue<string> q;
set<string> st;
q.push(s); // 入队
st.insert(s); // 防止重复访问
int cnt = 0;
while (q.size())
{
int sz = q.size();
// 层数
cnt++;
// 将这一层的元素全部出队处理
while (sz--)
{
string curr = q.front();
q.pop(); // 出队
// 找到x的位置, 和上下左右进行交换
// 如果当前的字符串是目标字符串, 那么就跳出循环
if(curr == ed) {
cout << "总共交换" << cnt - 1 << "次, 成功找到最终状态" << endl;
printPath();
return 0;
}
int idx = -1;
for (int i = 0; i < 9; i++)
{
if (curr[i] == 'x')
{
idx = i;
break;
}
}
// 一维转二维
int x = idx / 3;
int y = idx % 3;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i];
int b = y + dy[i];
if (a < 3 && a >= 0 && b < 3 && b >= 0)
{
int idx2 = a * 3 + b; // 交换的位置
string tmp = curr;
// 交换两个字母
swap(tmp[idx], tmp[idx2]);
// 如果产生了一个新的字符串, 就加入队列
if (!st.count(tmp))
{
st.insert(tmp);
q.push(tmp);
path[tmp] = curr; // 保存路径
}
}
}
}
}
cout << "转换失败" << endl;
return 0;
}
深度优先搜索 : 完整代码
#include <iostream>
#include <queue>
#include <set>
#include <map>
#include <stack>
using namespace std;
string s; // 矩阵的初始状态
string tmp;
string ed = "1238x4765"; // 最终状态
map<string, string> path; // 路径的跳转
set<string> st; // 防止重复访问
int dx[] = { 0, 0, -1, 1 };
int dy[] = { 1, -1, 0, 0 };
/**
* 输出路径
*/
void printPath()
{
cout << "printPath" << endl;
string curr = ed;
stack<string> stk;
while (path.count(curr))
{
stk.push(curr);
string pre = path[curr];
curr = pre;
}
stk.push(tmp);
while (stk.size())
{
string s = stk.top();
stk.pop();
for (int i = 0; i < 9; i++)
{
cout << s[i] << ' ';
if (i % 3 == 2)
{
cout << endl;
}
}
cout << endl;
}
exit(0); // 只输出一种方案
}
int cnt = 1;
void dfs(int u)
{
// cout << "test : " << s << endl;
if (s == ed)
{
cout << "************方案" << cnt << (cnt++) << "如下************" << endl;
printPath();
return;
}
if(u >= 2000) {
// cout << "搜索深度到达了边界" << endl;
return ;
}
// 找到x的位置
int idx = -1;
for (int i = 0; i < 9; i++)
{
if (s[i] == 'x')
{
idx = i;
break;
}
}
// 一维转二维
int x = idx / 3;
int y = idx % 3;
// 分支
for (int i = 0; i < 4; i++)
{
int a = x + dx[i];
int b = y + dy[i];
if (a < 3 && a >= 0 && b < 3 && b >= 0)
{
int idx2 = a * 3 + b; // 交换的位置
string pre = s; // 交换之前的值
swap(s[idx], s[idx2]);
if (!st.count(s)) // 防止重复访问
{
//cout << u << "path : " << s << ':' << pre << endl;
path[s] = pre;
st.insert(s);
dfs(u + 1); // 进行递归
path.erase(s); // 把记录删除掉
st.erase(s);
}
swap(s[idx], s[idx2]); // 恢复现场
}
}
}
int main()
{
// string s = "87x465123"; // 不要重新定义变量, 已经有全局变量了
s = "87x465123";
tmp = s;
// s = "123x84765";
cout << "深度优先搜索\n";
st.insert(s); // 已经访问过这个节点了
dfs(0);
cout << "转换失败" << endl;
return 0;
}
A *算法 : 完整代码
#include <iostream>
#include <queue>
#include <set>
#include <map>
#include <stack>
using namespace std;
string s; // 矩阵的初始状态
string ed = "1238x4765"; // 最终状态
map<string, string> path;
set<string> st; // 防止重复访问
bool success = false; // 标志成功
int dx[] = { 0, 0, -1, 1 };
int dy[] = { 1, -1, 0, 0 };
//数据结构 : 因为下一个节点都有代价值, 用于排序。 bfs不需要排序, 所有用一个结构体
struct Node {
string s; // 字符串
int f; // 代价值 f = g + h
int g; // 从st到当前节点的代价, 用于计算下一个f
// 构造函数
Node() {
}
Node(string s, int f, int g) {
this->s = s;
this->f = f;
this->g = g;
}
};
// 优先队列的排序规则
struct cmp {
bool operator() (Node *a, Node *b) {
return a->f > b->f; // 估值从小到大排序
}
};
/**
* 输出路径
*/
void printPath()
{
string curr = ed;
stack<string> stk;
while (path.count(curr))
{
stk.push(curr);
string pre = path[curr];
curr = pre;
}
stk.push(s);
int depth = stk.size();
while (stk.size())
{
string s = stk.top();
stk.pop();
for (int i = 0; i < 9; i++)
{
cout << s[i] << ' ';
if (i % 3 == 2)
{
cout << endl;
}
}
cout << endl;
}
cout << "总共交换" << depth - 1 << "次" << endl;
}
/*计算h : 到终点的代价, 曼哈顿距离*/
int H(string s) {
int sum = 0;
// 坐标(x, y)和(a, b)的差值之和
for (int i = 0; i < 9; i++) {
int a = i / 3;
int b = i % 3;
int idx = ed.find('x');
int x = idx / 3;
int y = idx % 3;
sum += abs(a - x) + abs(b - y);
}
return sum;
}
void Astar() {
priority_queue<Node*, vector<Node*>, cmp> q;
Node *start = new Node(s, H(s), 0);
q.push(start);
st.insert(s); // 记录, 防止重复访问
while (q.size()) {
// 出队
auto t = q.top();
q.pop();
// 到达终点
if (t->s == ed) {
success = true;
cout << "成功求解 : " << endl;
printPath();
return;
}
// 字符串,一维转二维
string s2 = t->s;
int idx = s2.find('x');
int a = idx / 3;
int b = idx % 3;
// 下一个节点
for (int i = 0; i < 4; i++) {
int x = a + dx[i];
int y = b + dy[i];
int idx2 = x * 3 + y;
string tmp = s2;
if (x < 3 && x >= 0 && y < 3 && y >= 0) {
swap(tmp[idx], tmp[idx2]);
if (!st.count(tmp)) {
st.insert(tmp);
Node* node = new Node(tmp, H(tmp) + t->g, t->g + 1); // 下一节点, 注意, 这里要h + g
q.push(node);
path[tmp] = s2;
}
}
}
}
}
int main()
{
// string s = "87x465123"; // 不要重新定义变量, 已经有全局变量了
s = "87x465123";
Astar();
if(!success) cout << "求解失败" << endl;
return 0;
}
-
- 实验结果讨论
深度搜索
广度搜索
启发式搜索
结论 :
访问的状态数 : A* < BFS < DFS, A*算法的效率最高
- dfs : 是某一条分支深入到不能深入为止, 找到的解不一定是最优解, 八数码交换次数远大于bfs, A*算法求解出来的
- bfs : 按照层次逐步拓展搜索, 所以首次找到的路径是最短的, 获得的一定是最优解
- A*算法 :考虑节点代价和启发式函数, 然后对节点进行排序, 大大提高了效率.
罗马尼亚问题
- 状态表示的数据结构
状态: 使用字符串表示一个状态(城市)
- 状态扩展规则的表示
拓展规则 :两个城市之间有一条路径, 即两个城市之间是连通的
- 搜索产生的状态空间图
状态的转换 :
- OPEN表和CLOSE表变化过程
open表一开始是所有城市, 访问过一个城市之后, 加入close表, 并将从open表中移走.
- 程序清单
贪婪算法 : 完整代码
#include <iostream>
#include <set>
#include <map>
#include <vector>
using namespace std;
int main() {
// 节点到Bucharest的直线距离(不是真实的距离)
map<string, int> straightDistance = {
{"Arad", 366},
{"Bucharest", 0},
{"Craiova", 160},
{"Drobeta", 242},
{"Eforie", 161},
{"Fagaras", 178},
{"Giurgiu", 77},
{"Hirsova", 151},
{"Iasi", 226},
{"Lugoj", 244},
{"Mehadia", 241},
{"Neamt", 234},
{"Oradea", 380},
{"Pitesti", 98},
{"Rimnicu Vilcea", 193},
{"Sibiu", 253},
{"Timisoara", 329},
{"Urziceni", 80},
{"Vaslui", 199},
{"Zerind", 374}
};
// 城市之间的距离
vector<tuple<string, string, int>> edges = {
{"Arad", "Zerind", 75},
{"Arad", "Sibiu", 140},
{"Arad", "Timisoara", 118},
{"Zerind", "Oradea", 71},
{"Oradea", "Sibiu", 151},
{"Timisoara", "Lugoj", 111},
{"Lugoj", "Mehadia", 70},
{"Mehadia", "Drobeta", 75},
{"Drobeta", "Craiova", 120},
{"Sibiu", "Fagaras", 99},
{"Sibiu", "Rimnicu Vilcea", 80},
{"Rimnicu Vilcea", "Craiova", 146},
{"Fagaras", "Bucharest", 211},
{"Rimnicu Vilcea", "Pitesti", 97},
{"Pitesti", "Bucharest", 101},
{"Bucharest", "Giurgiu", 90},
{"Bucharest", "Urziceni", 85},
{"Urziceni", "Hirsova", 98},
{"Urziceni", "Vaslui", 142},
{"Hirsova", "Eforie", 86},
{"Vaslui", "Iasi", 92},
{"Iasi", "Neamt", 87}
};
//
string start = "Arad"; // 初态
string ed = "Bucharest"; // 终态
vector<string> path; // 保存路径的跳转
path.push_back(start);
int distSum = 0; // 路径总和
while (1) {
// 获得当前的节点
int n = path.size();
string curr = path[n - 1];
string next = "";
int minDist = 0x3f3f3f3f;
int nextDist = 0x3f3f3f3f;
// 下一个节点 要离终点的直线距离最短
for (auto& edge : edges) {
auto [st, ed, dist] = edge;
if (curr == st && straightDistance[ed] < minDist) {
next = ed;
minDist = straightDistance[ed]; // 直线距离最短
nextDist = dist; // 下一跳的距离
}
}
// 加入这个选择
path.push_back(next);
//distSum += minDist; // 注意minDist是next->ed的直线距离, 而不是curr -> next的距离
distSum += nextDist;
// 到达终点
if (next == ed) {
break;
}
}
cout << "路径 :" << endl;
for (int i = 0; i < (int)path.size(); i++) {
cout << path[i];
if (i != (int)path.size() - 1) {
cout << "->";
}
}
cout << endl;
cout << "路径总和:" << distSum << endl;
return 0;
}
A*算法 : 完整代码
#include <iostream>
#include <set>
#include <map>
#include <vector>
using namespace std;
int main() {
// 节点到Bucharest的直线距离(不是真实的距离)
map<string, int> straightDistance = {
{"Arad", 366},
{"Bucharest", 0},
{"Craiova", 160},
{"Drobeta", 242},
{"Eforie", 161},
{"Fagaras", 178},
{"Giurgiu", 77},
{"Hirsova", 151},
{"Iasi", 226},
{"Lugoj", 244},
{"Mehadia", 241},
{"Neamt", 234},
{"Oradea", 380},
{"Pitesti", 98},
{"Rimnicu Vilcea", 193},
{"Sibiu", 253},
{"Timisoara", 329},
{"Urziceni", 80},
{"Vaslui", 199},
{"Zerind", 374}
};
// 城市之间的距离
vector<tuple<string, string, int>> edges = {
{"Arad", "Zerind", 75},
{"Arad", "Sibiu", 140},
{"Arad", "Timisoara", 118},
{"Zerind", "Oradea", 71},
{"Oradea", "Sibiu", 151},
{"Timisoara", "Lugoj", 111},
{"Lugoj", "Mehadia", 70},
{"Mehadia", "Drobeta", 75},
{"Drobeta", "Craiova", 120},
{"Sibiu", "Fagaras", 99},
{"Sibiu", "Rimnicu Vilcea", 80},
{"Rimnicu Vilcea", "Craiova", 146},
{"Fagaras", "Bucharest", 211},
{"Rimnicu Vilcea", "Pitesti", 97},
{"Pitesti", "Bucharest", 101},
{"Bucharest", "Giurgiu", 90},
{"Bucharest", "Urziceni", 85},
{"Urziceni", "Hirsova", 98},
{"Urziceni", "Vaslui", 142},
{"Hirsova", "Eforie", 86},
{"Vaslui", "Iasi", 92},
{"Iasi", "Neamt", 87}
};
//
string start = "Arad";
string ed = "Bucharest";
vector<string> path; // 保存路径的跳转
path.push_back(start);
int distSum = 0; // 路径总和
while (1) {
// 获得当前的节点
int n = path.size();
string curr = path[n - 1];
string next = "";
int minF = 0x3f3f3f3f;
int nextDist = 0x3f3f3f3f;
// 下一个节点 要离终点的直线距离最短
for (auto& edge : edges) {
auto [st, ed, dist] = edge;
if (curr == st) {
int H = straightDistance[ed]; // 到终点的距离
int G = distSum + dist; // 到当前点的距离
int f = H + G; // 估计函数
if (f < minF) {
next = ed;
minF = f; // 预估函数最小
nextDist = dist; // 下一跳的距离
}
}
}
// 加入这个选择
path.push_back(next);
//distSum += minDist; // 注意minDist是next->ed的直线距离, 而不是curr -> next的距离
distSum += nextDist;
// 到达终点
if (next == ed) {
break;
}
}
cout << "A*算法" << endl;
cout << "路径 :" << endl;
for (int i = 0; i < (int)path.size(); i++) {
cout << path[i];
if (i != (int)path.size() - 1) {
cout << "->";
}
}
cout << endl;
cout << "路径总和:" << distSum << endl;
return 0;
}
-
实验结果
贪婪算法
A *算法
结论 :
- 贪婪搜索 : 只考虑当前点到达终点的代价, 即 F(n) = H(n), 每次选择最短的, 求解出来可能是局部最优解, 而不是全局最优解
- A*算法 : 考虑了到达某个节点的总路径, 也考虑这个节点到达终点的代价, 即F(n) = G(n) + H(n), 因此, 能够求解出全局最优解.
思考题
- 你所采用的估价函数f(n) = g(n) + h(n)中,g(n)和h(n)的主要作用是什么?
答 : g(n) : 从起点到当前节点的实际代价。 用于去报A*算法在搜索时候考虑实际的路径代价, 保证路径是最短路径
h(n) : 当前路径到达终点的启发式估计代价, 一般是欧氏距离或者曼哈顿距离。提供了对未知部分的估计, 能够更好地选择下一步。
- 结合本实验举例说明不同启发策略对实验的效果有何影响?(可列出图表说明)
答 : 启发式函数的不同选择会影响我们最终的结果。我们在计算启发式估计代价的时候, 可以选择曼哈顿距离, 欧几里得距离, 这影响了我们的下一步选择, 最终计算出来的结果也可能不同。
- 若问题的初始状态是随机产生的,你的实验程序应该如何改进?(如图形属性的设置、图形队列存入文件等)添加代码,根据实际需要添加其他辅助函数。
答 :
只需要修改start即可, 具体 : 使用随机数生成函数, 生成一个数字, 对应着一个状态, 将其代表的状态赋值给start即可
- 尝试使用一致代价(等代价)搜索, 迭代加深的深度优先搜索算法求解上述问题,并根据实验结果分析深度优先搜索,一致代价(等代价)搜索,迭代加深的深度优先搜索算法, A*搜索的时间和空间复杂度。
答 : 效率 : A* > 等代价搜索 > 迭代加深的深度优先搜索 > 深度优先搜索。
- 指出无信息搜索策略和有信息搜索策略的不同并比较其性能。
答 : 两者区别在于 : 是否使用启发式函数。无信息搜索(bfs, dfs)只依赖当前状态信息, 可能在搜索空间中盲目拓展, 导致求解出来的不是最佳的, 性能也比较差。而有信息搜索在此基础之上, 提供了对未知部分的估计(利用启发式函数对问题进行更智能的搜索), 更加高效, 性能更好。
无法运行代码的解决方法
1. 遇到编译错误问题的, 在Visual studio设置使用C++17标准(不到1分钟就能解决的事)
2. 使用Dev C++或者VS code的自行搜索设置编译器使用c++17及以上标准。