基于pythonA*算法两种搜索算法求解八数码问题

文章介绍了使用A类的迭代深入搜索和A*算法解决八数码问题的实现方法,包括节点结构、启发值计算以及两种算法的具体步骤。实验结果显示,A*算法搜索次数少但耗时较长,而迭代深入搜索搜索次数多但耗时短。两者在多数情况下都能找到最优路线,但A*算法的启发式搜索在复杂情况下的效率有待提高。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实验任务

这里我选择 A 类的迭代深入搜索A*算法两种搜索算法求解八数码问题的解,并比较两种方式。

八数码问题是:在 3 × 3 九宫棋盘上,放置数码为 1 - 88 个棋牌,剩下一个空格(用 0 代替),只能通过棋牌向空格的移动来改变棋盘的布局。要求找到一种从给定初始布局(即初始状态)到目标布局(即目标状态)的移动方法。

比如我们可以让初始状态为:

1

2

3

4

5

6

7

0

8

目标状态为:

1

2

3

4

0

6

7

5

8

只需要交换 50 ,即可实现。

解决方案

我们将一种可能的 3 × 3 九宫棋盘,用一个 Node 类(下面我都称之为节点)来表示:包含 9 个数字的排列方式(即 data ),到达这个节点的步数(即 step ),当前节点是从哪个节点来的(即 parent )和每个节点的启发值。

# 创建Node类 (包含当前数据内容,父节点,步数)
class Node:
    f_loss = -1  # 启发值
    step = 0  # 初始状态到当前状态的距离(步数)
    parent = None,  # 父节点

    # 用状态和步数构造节点对象
    def __init__(self, data, step, parent):
        self.data = data        # 当前状态数值,是一个数组
        self.step = step        # 当前的步数
        self.parent = parent    # 当前节点的父节点
        self.f_loss = cal_wcost(data) + step # 计算启发值
A*算法

属于启发式搜索的一种,意思是说每次走到一个节点,我们就计算一下这个节点的启发值,如果节点的启发值越低,我们就把它的优先级设置的越高。启发值计算公式如下: f(n) 表示初始节点到当前节点的耗费,h(n) 表示当前节点到目标节点的预测耗费。

f(n) = g(n) + h(n) 

对于此题,我们可以用当前节点和目标节点的不同的个数来当作预测值,如果 9 个数字各不相同则预测值为 9 。预测值计算函数如下:

def cal_wcost(num):
    '''计算此时的不同程度'''
    con = 0
    # 遍历整个九宫格
    for i in range(3):
        for j in range(3):
            tmp_num = num[i][j]
            compare_num = end_data[i][j]
            if tmp_num != 0:
                # 不等,则加一
                con += (tmp_num != compare_num)
    return con

接下来我们来看看 A* 算法的实现。类似 BFS 算法,我们用队列 opened 来存储入队的节点,用 closed 列表标记是否访问。队列不为空,那么出队,将 0 (空位)四周可以与之交换的与之交换,如果交换后的节点没有访问过,就入队。上面都是和 BFS 一样的操作。入队时,我们需要判断队列中是否有这个节点,有的话,选择启发值较小的更新队列。最后,排序队列,选择队列中启发值最小的出队。

def astar_method_a_function(opened, closed):
    '''AStar启发式搜索'''
    count = 0
    # 队列不为空
    while len(opened.queue) != 0:
        # 出队
        node = opened.get()
        # 如果出队节点是目标状态
        if (node.data == end_data).all():
            print(f'总共耗费{count}轮')
            return node
        # 将取出的点加入closed表中,标记已经访问
        # closed[data_to_int(node.data)] = 1
        closed.append(data_to_int(node.data))
        # 四种移动方法
        for action in ['left', 'right', 'up', 'down']:
            # 创建子节点
            child_node = Node(swap(node.data, action), node.step + 1, node)
            index = data_to_int(child_node.data)
            # 没有被访问
            if index not in closed:
                # 入队,但是要先判断是否在队列里
                refresh_open(child_node, opened)
        # 根据其中的f_loss值为open表进行排序
        # 保证先出队的都是启发值较小的
        sorte_by_floss(opened)
        count += 1
        # 达到搜索上界
        if count > 5000:
            print("没有搜索到可行解!")
            break
迭代深入搜索

迭代深入搜索( IDS ),本质上就是深度优先搜索, IDS 结合了 DFS 的空间优势与 BFS 的时间优势。对搜索的深度进行了限制,使得在搜索到限制深度后必须开始新的搜索路径。以至于看上去像是广搜(总是完成第 n 的所有节点搜索,再开始第 n + 1 的节点的搜索)。放在这里的话, n 指的就是当前的限制深度。

意思就是说我们本质上还是 DFS 算法。而且不需要维持一个队列了,可以减少空间消耗,也能像广度优先搜索一样得到最佳解。每次 DFS 的时候都有一个最大的搜索深度。由此,对于我们这个求解八数码问题我的想法是:

首先设定查找深度为 count ,然后进行深度优先搜索;没有找到的话, count + 2, 继续深度优先搜索。重复上述步骤,直到找到目标节点或者搜索深度达到设定的最大值。具体代码如下:

con = 0    # 全局变量,记录查找次数


def dfs(node, closed, depth, count):
    '''dfs搜索,最大深度为count'''
    # 达到最大深度了,就返回
    if depth == count:
        return []
    # 计数
    global con
    con += 1
    # 四个方向分别查找
    for action in ['left', 'right', 'up', 'down']:
        child_node = Node(swap(node.data, action), node.step + 1, node)
        # 如果查找到了,那么就返回
        if (child_node.data == end_data).all():
            print(f'总共耗费{con}轮')
            return child_node
        # 得到此时九宫棋盘对应的唯一数字
        index = data_to_int(child_node.data)
        # 如果没有标记过
        if index not in closed:
            # 标记
            closed.append(index)
            # dfs搜索下一个节点
            a = dfs(child_node, closed, depth + 1, count)
            # 如果返回值不是 [] ,说明找到了
            if a != []:
                return a
    return []


def ids_method_a_function(start_node):
    '''迭代深入搜索'''
    if (start_node.data == end_data).all():
        return start_node
    # count表示查找深度,这里我让查找深度从2增加到100,每次增加2
    for count in range(2, 101, 2):
        # 标记列表
        closed = []
        # 标记起始节点
        closed.append(data_to_int(start_node.data))
        # dfs搜索
        a = dfs(start_node, closed, 0, count)
        # 搜索到了
        if a != []:
            return a
    print("没有搜索到可行解!")
程序框架

对比分析

测试案例

这里初始输入为 1 2 3 4 0 6 7 5 8

目标状态设置为 1 2 3 4 0 5 6 7 8

A* 算法运行结果如下图:

总共耗费279轮
+------+-----------+--------+
| step |    data   | f_loss |
+------+-----------+--------+
|  0   |  [[1 2 3] |   3    |
|      |   [4 0 6] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  1   |  [[1 2 3] |   4    |
|      |   [4 5 6] |        |
|      |  [7 0 8]] |        |
| ---  |  -------- |  ---   |
|  2   |  [[1 2 3] |   4    |
|      |   [4 5 6] |        |
|      |  [0 7 8]] |        |
| ---  |  -------- |  ---   |
|  3   |  [[1 2 3] |   6    |
|      |   [0 5 6] |        |
|      |  [4 7 8]] |        |
| ---  |  -------- |  ---   |
|  4   |  [[1 2 3] |   7    |
|      |   [5 0 6] |        |
|      |  [4 7 8]] |        |
| ---  |  -------- |  ---   |
|  5   |  [[1 2 3] |   8    |
|      |   [5 6 0] |        |
|      |  [4 7 8]] |        |
| ---  |  -------- |  ---   |
|  6   |  [[1 2 3] |   10   |
|      |   [5 6 8] |        |
|      |  [4 7 0]] |        |
| ---  |  -------- |  ---   |
|  7   |  [[1 2 3] |   12   |
|      |   [5 6 8] |        |
|      |  [4 0 7]] |        |
| ---  |  -------- |  ---   |
|  8   |  [[1 2 3] |   13   |
|      |   [5 0 8] |        |
|      |  [4 6 7]] |        |
| ---  |  -------- |  ---   |
|  9   |  [[1 2 3] |   14   |
|      |   [0 5 8] |        |
|      |  [4 6 7]] |        |
| ---  |  -------- |  ---   |
|  10  |  [[1 2 3] |   14   |
|      |   [4 5 8] |        |
|      |  [0 6 7]] |        |
| ---  |  -------- |  ---   |
|  11  |  [[1 2 3] |   14   |
|      |   [4 5 8] |        |
|      |  [6 0 7]] |        |
| ---  |  -------- |  ---   |
|  12  |  [[1 2 3] |   14   |
|      |   [4 5 8] |        |
|      |  [6 7 0]] |        |
| ---  |  -------- |  ---   |
|  13  |  [[1 2 3] |   14   |
|      |   [4 5 0] |        |
|      |  [6 7 8]] |        |
| ---  |  -------- |  ---   |
|  14  |  [[1 2 3] |   14   |
|      |   [4 0 5] |        |
|      |  [6 7 8]] |        |
+------+-----------+--------+
A*算法耗时 1.5066275596618652 s

深入迭代搜索算法运行结果如下图(用的是和上面同样的输出函数,这里的启发值我们忽略即可):

总共耗费1501轮
+------+-----------+--------+
| step |    data   | f_loss |
+------+-----------+--------+
|  0   |  [[1 2 3] |   3    |
|      |   [4 0 6] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  1   |  [[1 2 3] |   4    |
|      |   [4 6 0] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  2   |  [[1 2 0] |   6    |
|      |   [4 6 3] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  3   |  [[1 0 2] |   8    |
|      |   [4 6 3] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  4   |  [[0 1 2] |   10   |
|      |   [4 6 3] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  5   |  [[4 1 2] |   12   |
|      |   [0 6 3] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  6   |  [[4 1 2] |   13   |
|      |   [6 0 3] |        |
|      |  [7 5 8]] |        |
| ---  |  -------- |  ---   |
|  7   |  [[4 1 2] |   14   |
|      |   [6 5 3] |        |
|  9   |  [[4 1 2] |   14   |
|      |   [0 5 3] |        |
|      |  [6 7 8]] |        |
| ---  |  -------- |  ---   |
|  10  |  [[0 1 2] |   14   |
|      |   [4 5 3] |        |
|      |  [6 7 8]] |        |
| ---  |  -------- |  ---   |
|  11  |  [[1 0 2] |   14   |
|      |   [4 5 3] |        |
|      |  [6 7 8]] |        |
| ---  |  -------- |  ---   |
|  12  |  [[1 2 0] |   14   |
|      |   [4 5 3] |        |
|      |  [6 7 8]] |        |
| ---  |  -------- |  ---   |
|  13  |  [[1 2 3] |   14   |
|      |   [4 5 0] |        |
|      |  [6 7 8]] |        |
| ---  |  -------- |  ---   |
|  14  |  [[1 2 3] |   14   |
|      |   [4 0 5] |        |
|      |  [6 7 8]] |        |
+------+-----------+--------+
迭代深入搜索算法耗时 0.2624633312225342 s
个人分析

我们可以看到, A* 算法的搜索次数为 279 轮,耗费的时间 1.5066275596618652 s;迭代深入算法搜索次数为 1501 轮,耗费的时间只有 0.2624633312225342 s

  1. 搜索次数分析:

我认为 A* 算法搜索次数较少的原因是运用了贪心的思想,每一步都是向着目标节点的方向前进,因此可以较快的得到结果;而迭代深入搜索算法没有优先级,属于穷举类型。并且,由于结合了广度优先搜索算法,第一次搜索深度为 2 ,第二次深度为 4 这样,就会造成深度较低的节点被重复访问,因此搜索次数多。

  1. 搜索时间分析:

A* 算法的耗时更长,我认为原因是 A* 算法元素入队时需要查找是否已经入队,而且入队后为了维持队列按照启发值顺序有需要排序,还有计算节点的启发值等一系列操作,耗时较长;而迭代深入搜索算法虽然搜索次数较多,但是没有上面排序查找等操作。而且很多节点的访问都是重复的, CPUcache 可以加速重复节点的访问,因此耗时较短。

  1. 内存消耗分析:

A* 算法需要一个额外的队列,增加了内存开销,迭代深入搜索主要的内存开销是迭代时调用函数栈。同时,二者都需要一个标记列表。

  1. 是否为最优路线

A* 算法运用贪心的思想,虽然不能保证是最优解,但是贪心可以保证局部最优;迭代深入搜索兼具 BFSDFS 的优点,我设置的每次搜索深度之差为 2 ,那么可以保证我们得到的路径与最优路径之间的误差在 2 以内。在我测试的几组数据中,二者得到的结果相同。

综合来看,A* 算法的搜索次数较少,但是耗时较长;迭代深入搜索搜索次数较长,但是耗时较短。一般来说,二者都能得到最优路线。

补充测试

增加两组测试:

输入:3 8 2 4 0 1 5 6 7
目标:1 8 7 2 0 6 3 4 5
迭代深入搜索算法耗费7090轮,耗时 1.7389905452728271 s
A*算法共耗费834轮,耗时 31.58454418182373 s
输入:3 0 2 8 1 4 7 6 5
目标:1 2 3 8 0 4 7 6 5
代深入搜索算法耗费6347轮,耗时 1.4747085571289062 s
A*算法耗费330轮,耗时 2.457353115081787 s
运行结果截图

限于篇幅,注释掉了路径的输出。

小结

选择这个题目我认为对我而言是一个很大的收获。首先是学习了 python 的相关操作,尤其的类的思想。其次学习了两种优秀的搜索算法。迭代深入搜索结合了深度优先算法和广度优先算法的优点,在书写代码时,不仅巩固了以往的知识,同样学到了一个性能更好也很常用的算法。对于 A* 算法,同样有了一个深入的了解,启发式搜索在有的时候,性能很好。同样,也有很多的不足,比如我的 A* 算法的启发值可能并没有想到一个最优的计算方法,导致对于一些较为复杂的输入,得不到解答。但总的来看,还是顺利完成了实验。

完整代码https://download.csdn.net/download/pythonyanyan/87394450

A*算法求解八数码问题 1、A*算法基本思想: 1)建立一个队列,计算初始结点的估价函数f,并将初始结点入队,设置队列头和尾指针。 2)取出队列头(队列头指针所指)的结点,如果该结点是目标结点,则输出路径,程序结束。否则对结点进行扩展。 3)检查扩展出的新结点是否与队列中的结点重复,若与不能再扩展的结点重复(位于队列头指针之前),则将它抛弃;若新结点与待扩展的结点重复(位于队列头指针之后),则比较两个结点的估价函数中g的大小,保留较小g值的结点。跳至第五步。 4)如果扩展出的新结点与队列中的结点不重复,则按照它的估价函数f大小将它插入队列中的头结点后待扩展结点的适当位置,使它们按从小到大的顺序排列,最后更新队列尾指针。 5)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,再返回第二步。 2、程序运行基本环境: 源程序所使用编程语言:C# 编译环境:VS2010,.net framework 4.0 运行环境:.net framework 4.0 3、程序运行界面 可使用程序中的test来随机生成源状态与目标状态 此停顿过程中按Enter即可使程序开始运行W(n)部分; 此停顿部分按Enter后程序退出; 4、无解问题运行情况 这里源程序中是先计算源状态与目标状态的逆序对的奇偶性是否一致来判断是否有解的。下面是无解时的运行画面: 输入无解的一组源状态到目标状态,例如: 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 8 7 0 运行画面如下: 5、性能比较 对于任一给定可解初始状态,状态空间有9!/2=181440个状态;当采用不在位棋子数作为启发函数时,深度超过20时,算法求解速度较慢; 其中启发函数P(n)与W(n)的含义如下: P(n): 任意节点与目标结点之间的距离; W(n): 不在位的将牌数; 源状态 目标状态 P(n) 生成节点数 W(n) 生成节点数 P(n) 扩展节点数 W(n) 扩展节点数 2 8 3 1 6 4 7 0 5 1 2 3 8 0 4 7 6 5 11 13 5 6 1 2 3 8 0 4 7 6 5 0 1 3 8 2 4 7 6 5 6 6 2 2 4 8 2 5 1 6 7 0 3 7 4 2 8 5 6 1 3 0 41 79 22 46 6 2 5 8 7 0 3 1 4 0 3 6 7 1 8 4 5 2 359 10530 220 6769 7 6 3 1 0 4 8 5 2 2 8 7 1 3 4 6 5 0 486 8138 312 5295 下图是解决随机生成的100中状态中,P(n)生成函数的生成节点与扩展节点统计图: 由上图可知,P(n)作为启发函数,平均生成节点数大约在1000左右,平均扩展节点数大约在600左右; 下图是解决随机生成的100中状态中,W(n)生成函数的生成节点与扩展节点统计图: 由上图可知,W (n)作为启发函数,平均生成节点数大约在15000左右,是P(n)作为启发函数时的平均生成节点的15倍;W (n)作为启发函数,平均扩展节点数大约在10000左右,是P(n)作为启发函数时的平均扩展节点的15倍; 下图是解决随机生成的100中状态中,两个生成函数的生成节点与扩展节点统计图: 由上述图表可以看到,将P(n)作为启发函数比将W(n)作为启发函数时,生成节点数与扩展节点数更稳定,相比较来说,采用P(n)作为启发函数的性能比采用W(n)作为启发函数的性能好。 6、源代码说明 1)AStar-EightDigital-Statistics文件夹:用来随机生成100个状态,并对这100个状态分别用P(n)与W(n)分别作为启发函数算出生成节点以及扩展节点,以供生成图表使用;运行界面如下: 2)Test文件夹:将0-8这9个数字随机排序,用来随机生成源状态以及目标状态的;运行界面如下: 3)AStar-EightDigital文件夹:输入源状态和目标状态,程序搜索出P(n)与W(n)分别作为启发函数时的生成节点数以及扩展节点数,并给出从源状态到目标状态的移动步骤;运行界面如下: 提高了运行速度的几处编码思想: 1、 在维护open以及close列表的同时,也维护一个类型为hashtable的open以及close列表,主要用来提高判断当前节点是否在open列表以及close列表中出现时的性能; 2、 对于每个状态,按照从左到右,从上到下,依次将数字拼接起来,形成一个唯一标识identify,通过该标识,可以直接判断两个状态是否是同一个状态,而不需要循环判断每个位置上的数字是否相等 3、 在生成每个状态的唯一标识identify时,同时计算了该状态的空格所在位置,通过空格所在位置,可以直接判断能否进行上移、下移、左移、右移等动作; 4、 只计算初始节点的h值,其它生成的节点的h值是根据当前状态的h值、移动的操作等计算后得出的,规则如下: a) 采用W(n)这种方式,不在位置的将牌数,共有以下3中情况: i. 该数字原不在最终位置上,移动后,在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值-1 ii. 该数字原在最终位置上,移动后,不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 +1 iii. 该数字原不在最终位置上,移动后,还是不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 iv. 该数字原在最终位置上,移动后,还在其最终位置 这种情况不存在 b) 采用P(n)这种方式,节点与目标距离,可通过下面3步完成 i. 首先计算在原位置时,与目标位置的距离,命名为Distance1 ii. 移动后,计算当前位置与目标位置的距离,命名为Distance2 iii. 计算子节点的h值: 子节点的h值 = 父节点的h值- Distance1+ Distance2 5、 在任意状态中的每个数字和目标状态中同一数字的相对距离就有9*9种,可以先将这些相对距离算出来,用一个矩阵存储,这样只要知道两个状态中同一个数字的位置,就可查出它们的相对距离,也就是该数字的偏移距离;例如在一个状态中,数字8的位置是3,在另一状态中位置是7,那么从矩阵的3行7列可找到2,它就是8在两个状态中的偏移距离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员奇奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值