广州大学人工智能原理实验二-八数码问题

广州大学学生实验报告

开课学院及实验室: 计算机科学与网络工程学院电子信息楼416A    ****

学院

计算机科学与网络工程学院

年级/专业/班

****

姓名

****

学号

*****

实验课程名称

人工智能原理实验

成绩

实验项目名称

八数码

指导老师

***

实验二  八数码问题

  • 实验目的

本实验课程是计算机、智能、物联网等专业学生的一门专业课程,通过实验,帮助学生更好地掌握人工智能相关概念、技术、原理、应用等;通过实验提高学生编写实验报告、总结实验结果的能力;使学生对智能程序、智能算法等有比较深入的认识。

  1. 掌握人工智能中涉及的相关概念、算法。
  2. 熟悉人工智能中的知识表示方法;
  3. 熟悉盲目搜索和启发式搜索算法的应用;
  4. 掌握问题表示、求解及编程实现。
  5. 掌握不同搜索策略的设计思想、步骤、性能。

二、基本要求

  1. 实验前,复习《人工智能原理》课程中的有关内容。
  2. 准备好实验数据。
  3. 编程要独立完成,程序应加适当的注释。
  4. 完成实验报告。

三、实验软件

使用C或C++(Visual studio)(不限制语言使用)。

四、实验内容:

(1)

  1. 在图1,3*3的方格棋盘上,摆放着1到8这八个数码,有1个方格是空。

图1

  1. 如图1所示,要求对空格执行空格左移、空格右移、空格上移和空格下移这四个操作使得棋盘从初始状态(图1左)到目标状态(图1右)。
  2. 可自行设计初始状态。目标状态为数字从小到大按顺时针排列。
  3. 分别用广度优先搜索策略、深度优先搜索策略和启发式搜索算法(A*算法)求解八数码问题;分析估价函数对启发式搜索算法的影响;探究各个搜索算法的特点。

(2)罗马尼亚问题

根据上图以Zerind为初始状态,Bucharest为目标状态实现搜索,分别以贪婪搜索(只考虑直线距离)和A*算法求解最短路径。 按顺序列出贪婪算法探索的扩展节点和其估价函数值,A*算法探索的扩展节点和其估计值。

选做部分:自行设计一个新的启发式函数,并分析该函数的可采纳性和优势(与启发式函数定义为“Zerind到Bucharest的直线距离”相比较)。

五、学生实验报告要求

1、实验报告需要包含以下几个部分

    1. 状态表示的数据结构
    2. 状态扩展规则的表示
    3. 搜索产生的状态空间图

    1. OPEN表和CLOSE表变化过程
    2. 程序清单
    3. 实验结果讨论

2、思考并解答以下问题

  1. 你所采用的估价函数f(n) = g(n) + h(n)中,g(n)和h(n)的主要作用是什么?
  2. 结合本实验举例说明不同启发策略对实验的效果有何影响?(可列出图表说明)
  3. 若问题的初始状态是随机产生的,你的实验程序应该如何改进?(如图形属性的设置、图形队列存入文件等)添加代码,根据实际需要添加其他辅助函数。

(4) 尝试使用一致代价(等代价)搜索, 迭代加深的深度优先搜索算法求解上述问题,并根据实验结果分析深度优先搜索,一致代价(等代价)搜索,迭代加深的深度优先搜索算法, A*搜索的时间和空间复杂度。

(5) 指出无信息搜索策略和有信息搜索策略的不同并比较其性能。

八数码

    1. 状态表示的数据结构

    1. 状态扩展规则的表示

答 :

x与上下左右四个方向交换位置 :

注意 : x可能在边缘, 因此它交换完之后不能不合法(因此需要对下标的合法性判断)

    1. 搜索产生的状态空间图

    1. OPEN表和CLOSE表变化过程
    1. 程序清单

广度优先搜索 : 完整代码

#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;
}

    1. 实验结果讨论

深度搜索

广度搜索

启发式搜索

结论 :

访问的状态数 : A* < BFS < DFS, A*算法的效率最高

  1. dfs : 是某一条分支深入到不能深入为止, 找到的解不一定是最优解, 八数码交换次数远大于bfs, A*算法求解出来的
  2. bfs : 按照层次逐步拓展搜索, 所以首次找到的路径是最短的, 获得的一定是最优解
  3. A*算法 :考虑节点代价和启发式函数, 然后对节点进行排序, 大大提高了效率.

罗马尼亚问题

  1. 状态表示的数据结构

状态:  使用字符串表示一个状态(城市)

  1. 状态扩展规则的表示

拓展规则 :两个城市之间有一条路径, 即两个城市之间是连通的

  1. 搜索产生的状态空间图

状态的转换 :

  1. OPEN表和CLOSE表变化过程

open表一开始是所有城市, 访问过一个城市之后, 加入close表, 并将从open表中移走.

  1. 程序清单

贪婪算法 : 完整代码

#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;
}

  1. 实验结果

贪婪算法

A *算法

结论 :  

  1. 贪婪搜索 : 只考虑当前点到达终点的代价, 即 F(n) = H(n), 每次选择最短的, 求解出来可能是局部最优解, 而不是全局最优解
  2. A*算法 : 考虑了到达某个节点的总路径, 也考虑这个节点到达终点的代价, 即F(n) = G(n) + H(n), 因此, 能够求解出全局最优解.

思考题

  1. 你所采用的估价函数f(n) = g(n) + h(n)中,g(n)和h(n)的主要作用是什么?

答 : g(n) : 从起点到当前节点的实际代价。 用于去报A*算法在搜索时候考虑实际的路径代价, 保证路径是最短路径

h(n) : 当前路径到达终点的启发式估计代价, 一般是欧氏距离或者曼哈顿距离。提供了对未知部分的估计, 能够更好地选择下一步。

  1. 结合本实验举例说明不同启发策略对实验的效果有何影响?(可列出图表说明)

答 : 启发式函数的不同选择会影响我们最终的结果。我们在计算启发式估计代价的时候, 可以选择曼哈顿距离, 欧几里得距离, 这影响了我们的下一步选择, 最终计算出来的结果也可能不同。

  1. 若问题的初始状态是随机产生的,你的实验程序应该如何改进?(如图形属性的设置、图形队列存入文件等)添加代码,根据实际需要添加其他辅助函数。

答 :

只需要修改start即可, 具体 : 使用随机数生成函数, 生成一个数字, 对应着一个状态, 将其代表的状态赋值给start即可

  1. 尝试使用一致代价(等代价)搜索, 迭代加深的深度优先搜索算法求解上述问题,并根据实验结果分析深度优先搜索,一致代价(等代价)搜索,迭代加深的深度优先搜索算法, A*搜索的时间和空间复杂度。

答 : 效率 : A* > 等代价搜索 > 迭代加深的深度优先搜索 > 深度优先搜索。

  1. 指出无信息搜索策略和有信息搜索策略的不同并比较其性能。

答 : 两者区别在于 : 是否使用启发式函数。无信息搜索(bfs, dfs)只依赖当前状态信息, 可能在搜索空间中盲目拓展, 导致求解出来的不是最佳的, 性能也比较差。而有信息搜索在此基础之上, 提供了对未知部分的估计(利用启发式函数对问题进行更智能的搜索), 更加高效, 性能更好。

无法运行代码的解决方法

1. 遇到编译错误问题的, 在Visual studio设置使用C++17标准(不到1分钟就能解决的事)

2. 使用Dev C++或者VS code的自行搜索设置编译器使用c++17及以上标准。

  • 33
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值