数码电脑
人工智能作业 八数码启发式搜索与bfs比较
2023-12-18 13:00  

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

问题描述

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

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

求解思路

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

每个状态可以用一个九维数组一一对应,State定义为:

typedef int State[9];

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

hash问题

我们注意到,八数码的每个状态由有序的九个数组成。共有9!=366880个。

我们固然可以将每个状态映射为一个整形数,其最大也不会超过1e10。但要注意,我们没有这种状态,八数码中每个数是不能出现在两个位置上的。这样就产生了极大的空间浪费。

粗略估计的空间占用率为\tfrac{366880}{1e10}​,这是一个非常小的数。

为此,我们可以引入另外一个hash策略:将每个九元数看做一个字符串用c语言实现八数码问题的宽度优先搜索,对其按照字典序排列。

每个状态的hash值就是比它字典序要小的状态的个数。比如1688黄页,没有比它字典序最小的状态了,因此其hash值为0。字典序最大,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。评价函数是用来评价待扩展的好坏的。我们每次扩展节点总是选择评价最好的节点做扩展。

评价函数F = dis + v,对应每一个节点。dis代表从原节点到当前节点所用步数,v代表启发函数值。dis用来描述从开始节点扩展到当前节点付出的代价,v用来描述从当前节点到目标节点的代价的估计。open表:里面放待扩展节点,每次选择评价函数最小的节点做扩展。我们使用bfs策略扩展节点。close表,存放已经扩展了的节点。注意到我们要在close表中记录该状态对应的最优F值。

如果我们又扩展到了已经在close表中的节点,那么我们就要比较二者的F值,若这次扩展的F值比close表中要小,不予理会。否则要将其放入open表中并更新close表。在《人工智能 模型与算法》书中,对启发函数v的选取有一定要求。大抵是图搜索需要满足可容性与一致性,树搜索需要满足可容性。我们解决八数码应该是用树搜索的形式做一个图搜索。这里了解一下就好。 数据结构的选择 open表

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

这里选用(最大堆)做open表。

close表

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

节点

如前所述,八数码的每个状态用typedef int State[9]表示。

dis用来描述从开始节点扩展到当前节点付出的代价用c语言实现八数码问题的宽度优先搜索人工智能作业 八数码启发式搜索与bfs比较,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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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 
#include 
#include 
#include 
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;
}

【本文来源于互联网转载,如侵犯您的权益或不适传播,请邮件通知我们删除】

发表评论
0评