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

问题描述

3×3九宫棋盘,放置数码为1 -8的8个棋牌,剩下一个空格,只能通过棋牌向空格的移动来改变棋盘的布局。
八数码图片示例
要求:根据给定初始布局(即初始状态)和目标布局(即目标状态),如何移动棋牌才能从初始布局到达目标布局,找到合法的走步序列。

求解思路

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

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

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

hash问题

我们注意到,八数码的每个状态由有序的九个数组成。共有9!=366880个。
我们固然可以将每个状态映射为一个整形数,其最大也不会超过1e10。但要注意,我们没有112233440这种状态,八数码中每个数是不能出现在两个位置上的。这样就产生了极大的空间浪费。
粗略估计的空间占用率为 366880 1 e 10 \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的全部排列个数为 ( n 1 + n 2 + n 3 ) ! n 1 ! ∗ n 2 ! ∗ n 3 ! \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

扩展策略扩展节点总数
盲目bfs181441
启发函数A145323
启发函数B16596

启发函数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;
}
  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值