A*搜索解决八数码问题【Python】

文章探讨了如何使用A*算法解决八数码问题,比较了曼哈顿距离和不在位数字数量作为启发函数对搜索时间的影响,强调了估价函数设计的重要性。
摘要由CSDN通过智能技术生成

一起来看我的shit代码 

实验题目

在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。要求使用A*算法。

问题分析

设计算子

该问题可以用状态空间表示,此时八数码的任何一种摆法就是一个状态,所有的摆法即为状态集S,总共包含9!个状态。接下来设计算子,如果定义为数字的移动,操作算子有4个方向*8个数字=32个。如果定义为空格的移动,操作算子只有4个方向上的移动,做到了算子的简化,当然这里要考虑边界情况,防止空格移出边界。

可解性判断

当左右移动空格时,逆序不变。当上下移动空格时,相当于将一个数字向前(或向后)移动两格,跳过的这两个数字要么都比它大(小),逆序±2;要么一个较大一个较小,逆序不变。所以可得结论:只要是相互可达的两个状态,它们的逆序奇偶性相同。所以先判断初始状态和目标状态的逆序对的奇偶性来保证问题的可解性。

算法设计

总体设计

步骤1  判断初始状态和目标状态逆序对的奇偶性,一致则有解,否则退出

步骤2  把初始结点S放入open表

步骤3  判断open表头元素和目标状态是否一致,一致则搜索成功,输出路径。若不一致,则从open表删除头元素,并按空格的四个方向拓展头元素的状态。

步骤4  若空格在边界外则舍弃,遍历有效子状态,计算子状态的估价值,若子状态不在open表也不在closed表,加入到open表;若在open表,更新最短路径;若在closed表,更新最短路径并移动到open表。

步骤5  头元素加到closed表

步骤6  open表按照估价值从小到大排序,转步骤3

详细讲解

导入numpy和time包,将以3x3的np矩阵来存储位置信息,time拿来计算耗时,观测性能。

定义Node类,matrix存储np矩阵,string存储其字符串展开形式,f存储该节点估价函数,g存储深度,h存储估计代价,blank存储空格的位置,father存储父亲节点。实现__lt__、__eq__、__hash__魔术方法,方便后面open表按照f值来排序。

定义估价函数,这里提供两种启发函数。diff函数,计算当前状态和目标状态的不在位数字的数量。manhattan函数,计算当前状态和目标状态的对应数字的曼哈顿距离。

定义逆序对计数函数,比较初始状态和目标状态逆序对的奇偶性,若一直则有解,再开始搜索,否则直接退出。

初始节点加入open表,取出open表头结点作为当前结点,判断当前结点和目标结点是否一致,若一致则搜索成功,递归输出当前结点的父节点作为路径,不一致则从open表删除该节点,并以空格的上下左右四个方向拓展当前结点。若空格在边界内,则交换两数并计算子节点的实际代价(父节点深度+1)和估计代价(diff或manhattan),取两者和作为该节点的估价值。

若子节点既不在open表也不在closed表,就加入到open表;若在open表并存在更短路径,则更新open表中相同节点的代价;若在close表并存在更短路径,则更新closed表中相同节点的代价并从closed表移动到open表。

把open表头结点加入到closed表。

对open表按f值从小到大排序。

运算结果

由于当前问题无解,为了演示程序正确性,故取教材5.17的题目作为输入。先用曼哈顿距离作为启发函数。

经过9步成功从初始状态变成目标状态。若将启发函数设置为diff函数,即不在位数字的数量。发现搜索时间大幅度减少。

实验总结

本次实验成功用A_star算法解决了八数码问题,并对比了两种不同的启发函数对搜索时间的影响,大多数情况下使用不在位数字数量作为启发函数比曼哈顿距离找到最优解的速度更快。由此可见设计一个好的估价函数是很重要的。此外,本实验所用估价函数没有考虑数码逆转的情况,理论上存在更优的估价函数。本次实验的代码也有不少可以改进的地方,比如本代码使用Python的list数据类型进行排序,当数码的层次过于庞大的时候,排序耗时会大幅增加,可以考虑使用set类型进行存储节点,可以有效减少排序耗时。

附录

import numpy as np
import time


class Node:
    # 节点类
    def __init__(self, m):
        self.matrix = m  # 八数码状态矩阵
        self.string = m2s(m)  # 八数码状态字符串
        self.f = 0  # 估价函数
        self.g = 0  # 实际代价 这里取深度 初始第0层
        self.h = 0  # 估计代价
        self.blankindex = self.string.find("0")  # 字符串形式空格的下标
        self.blanki = (self.blankindex) // 3  # 空格在矩阵的第i行
        self.blankj = (self.blankindex) % 3  # 空格在矩阵的第j列
        self.father = None  # 父亲节点

    def __hash__(self):
        # 吧字符串作为哈希
        return hash(self.string)

    def __eq__(self, other):
        # 吧字符串形式作为相等依据
        return isinstance(other, Node) and self.string == other.string

    def __lt__(self, other):
        # 吧估价函数值作为排序依据
        return self.f < other.f


def m2s(matrix):
    # 矩阵转字符串
    return "".join(str(j) for i in matrix for j in i)


def s2m(string):
    # 字符串转矩阵
    return np.array([int(char) for char in string]).reshape(n, n)


def diff(node):
    # 统计不在位数字的个数
    return np.sum(node.matrix != end_node.matrix)


def manhattan(node):
    # 计算曼哈顿距离
    distance = 0
    for i in range(n):
        for j in range(n):
            if node.matrix[i][j] != 0:  # 不考虑空格
                goal_i, goal_j = np.where(end_matrix == node.matrix[i][j])
                distance += abs(i - goal_i) + abs(j - goal_i)
    return distance


def h(node, mode):
    # 提供两种方式计算启发函数
    ans = 0
    if mode == "diff":
        ans = diff(node)
    elif mode == "manhattan":
        ans = manhattan(node)
    return ans


def count_inversions(string):
    # 逆序数计数
    inversions = 0
    for i in range(len(string)):
        for j in range(i + 1, len(string)):
            if string[i] != '0' and string[j] != '0' and string[i] > string[j]:
                # 不考虑0
                inversions += 1
    return inversions


def A_star(s, e, mode):
    if count_inversions(s.string) % 2 != count_inversions(e.string) % 2:
        # 初始状态和目标状态逆序奇偶性不一致,该问题无解
        return None
    openlst = [s]
    closedlst = []
    path = []

    while openlst:
        head = openlst[0]
        if diff(head) == 0:  # 找到目标节点 搜索成功
            while head.father:  # 遍历父节点
                path.append(head.matrix)
                head = head.father
            return path[::-1]
        openlst.remove(head)
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]  # 上下左右
        for direction in directions:  # 遍历空格的方向
            new_i = head.blanki + direction[0]
            new_j = head.blankj + direction[1]
            if 0 <= new_i < n and 0 <= new_j < n:  # 边界检查 保证空格在矩阵内
                temp = head.matrix.copy()  # 复制head的矩阵
                temp[new_i][new_j], temp[head.blanki][head.blankj] = temp[head.blanki][head.blankj], temp[new_i][
                    new_j]  # 交换
                new_node = Node(temp)
                new_node.father = head  # 父节点
                new_node.g = head.g + 1  # 计算实际代价 深度=父节点深度+1
                new_node.h = h(new_node, mode)  # 计算估计代价
                new_node.f = new_node.g + new_node.h  # 估价函数

                if new_node not in openlst and new_node not in closedlst:
                    openlst.append(new_node)
                elif new_node in openlst:
                    for source in openlst:
                        # 找open表中一样的节点
                        if source.string == new_node.string:
                            if new_node.g < source.g:
                                # 若存在更短路径 更新代价
                                source.f = new_node.f
                                source.g = new_node.g
                                source.h = new_node.h
                                source.father = new_node.father
                            break
                elif new_node in closedlst:
                    for source in closedlst:
                        # 找open表中一样的节点
                        if source.string == new_node.string:
                            if new_node.g < source.g:
                                # 若存在更短路径 更新代价
                                source.f = new_node.f
                                source.g = new_node.g
                                source.h = new_node.h
                                source.father = new_node.father
                                openlst.append(source)  # 把该节点从closed表移到open表
                                closedlst.remove(source)
                            break
                closedlst.append(head)
                openlst.sort()


def data_input():
    start_matrix = s2m(input("初始状态是?"))
    end_matrix = s2m(input("目的状态是?"))
    return start_matrix, end_matrix


n = 3  # 矩阵维度

# # -----样例输入-----
# start_matrix = np.array([[1, 2, 3], [8, 6, 4], [7, 5, 0]])
# end_matrix = np.array([[1, 2, 3], [8, 0, 4], [7, 6, 5]])
# # ----------------

start_matrix, end_matrix = data_input()


start_node = Node(start_matrix)
end_node = Node(end_matrix)
print("初始状态")
print(start_matrix)
print("目的状态")
print(end_matrix)
print("----------开始搜索----------")

start_t = time.perf_counter()  # 计时
answer = A_star(start_node, end_node, "diff")
end_t = time.perf_counter()

if answer:
    cnt = 0
    for i in answer:
        cnt += 1
        print("第{}步".format(cnt))
        print(i)
        print("------------------------")
    print("经过{}步到达目的状态,耗时{:.16f}秒".format(cnt, end_t - start_t))
else:
    print("初始状态和目标状态逆序奇偶性不一致,该问题无解")

### 回答1: a*算法是一种启发式搜索算法,可以用于解决八数码问题八数码问题是一种经典的游戏,目标是将一个3*3的九宫格中的数字按照特定顺序排列,其中一个格子为空。每次操作可以将与空格相邻的一个数字移动到空格中,直到达到目标状态。 使用a*算法解决八数码问题主要有以下几个步骤: 1. 定义状态空间:将九宫格中的数字排列看作状态,即每个状态由一个长度为9的数组来表示。 2. 定义启发函数:启发函数用来评估当前状态距离目标状态的距离,通常使用曼哈顿距离或欧几里得距离。在八数码问题中,使用曼哈顿距离计算两个状态之间的距离。 3. 定义操作:定义将一个数字移动到空格中的操作,以及对应的代价。在八数码问题中,每次操作的代价都为1。 4. 使用优先队列存储状态:使用优先队列存储每个状态以及与起始状态的距离 + 启发函数值。 5. 开始搜索:从初始状态开始,每次取出距离 + 启发函数值最小的状态,并对其进行操作,得到一系列可能的下一个状态。将这些状态加入优先队列,并继续搜索,直到找到目标状态。 6. 输出解:当找到目标状态后,可以通过反向遍历得到从目标状态到初始状态的一条路径,即为解。将路径输出即可。 使用Python实现a*算法解决八数码问题具体实现可以参考相关教程或代码库。 ### 回答2: 在八数码问题中,有一个3x3的矩阵,其中包含1-8号数字,以及一个空位。基本目标是将矩阵重排、使得排列成指定的形式。 a*算法,是一种基于启发式搜索的算法,它可以在有较大状态空间的问题中找到最优解。在求解八数码问题时,a*算法可以被用来搜索空位所处位置的不同状态,并采用估价函数来判断哪些状态更有可能走向正确的解决方案。 基于估价函数,a*算法被用来搜索状态时已经做好了最小化搜索路径长度的准备,也就是说,它可以尽可能快地找到最优解。 实现a*算法解决八数码问题Python代码,可以分多层解决。首先,需要定义一个函数,用于获取空格的位置。通过该函数,可以确定出当前状况空格往四个方向所能到达的状态。 下一步,需要判断每一个移动后的状态是否合法。移动状态需要计算出一个估价函数的值,来评估该状态是否最有可能走向目标正确状态。 用Python实现时,估价函数可以定义为当前状态离目标状态越远,则评估函数值越大。估价函数的实现可以使用曼哈顿距离来计算两个状态之间的距离。 接下来,通过a*算法进行搜索,直到找到最优解。在算法中,首先通过一个优先级队列(priority queue)来对状态进行排序和筛选。在每一个移动后的状态中,选择估价函数值最小的状态进行搜索。最终,可以找到最优的解决方案。 ### 回答3: A*算法是一种用于路径规划的算法,它能够有效地解决八数码问题八数码问题是指在 3×3 的九宫格中,一个初始状态可以移到目标状态的谜题。在八数码问题中,每个格子可以放置数字1-8或空格,规则是只能上下左右移动,将空格移到目标状态,同时保证空格移动路径最短。 在Python中,构建A*算法解决八数码问题的步骤如下: 1.构建初始的状态和目标状态 定义一个 3 * 3 的列表,用0表示空格,用1-8表示数字。例如,一个样例状态为:[1,2,3,4,5,6,0,7,8]。 2.计算需要移动的步数 通过计算当前状态和目标状态之间不同的数字的个数,即曼哈顿距离(Manhattan distance),来计算出当前状态的评估函数(f(n))。 3.确定移动的方向 向当前空格的周围四个方向依次移动,计算移动后的状态的评估函数f(n)。 4.加入已探索列表 将移动后的状态加入已探索的状态列表中。 5.重复步骤2-4,直到找到目标状态。 如果当前状态和目标状态一致,则搜索结束。否则,重复步骤2-4直到找到目标状态。此时,需要返回最短路径。 6.输出最终答案 输出从初始状态到目标状态的路径。 总体来说,A*算法是一种有效的搜索算法,在处理八数码问题中有着不错的应用效果。在实现A*算法时,要注意选择正确的数据结构和算法实现方法,并严格控制代码的时间复杂度,以提高算法的效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值