人工智能-A*启发式搜索算法解决八数码问题 Python实现

news2024/10/6 6:32:13

一.问题描述

        八数码问题也称为九宫问题。在 3×3 的棋盘,摆有八个棋子,每个棋子上标有 1 至 8 的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格(以数字 0 来表示),与空 格相邻的棋子可以移到空格中。 要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。

该问题可用A*启发式搜索算法求解。

A*算法的估价函数如下: 

        其中启发函数可选择w(n)和p(n)两种,本文以w(n)为例编写程序


二.算法实现【理论部分】

本文以如下情况为例进行分析:

         1.对问题进行抽象

                 ①.操作算子的选取

                若着眼在数码上,则相应的操作算子就是数码的移动,可知共有4(方向)*8(码的个数)=32 种算子,设计起来较为复杂。

                若着眼在空格上,则相应的操作算子就是空格的移动,在最理想的情况下【即空格在棋盘正中间时】,最多有4种算子【上移up,下移down,左移left,右移right】,设计较为简单。

                综上所述,本程序选择着眼于空格,使用上下左右四种操作算子进行程序编写

               ②.数码移动过程的抽象

                可将数码在3*3棋盘上的分布抽象为一维数组,这样每次空格的移动就等价于交换一维数组种两个元素的位置,如下图所示:

     

 至此,数码的移动问题转换成了寻找两个待交换元素在数组中的下标交换的问题

                2.实际执行过程

                        ①.搜索树

由上图的搜索树可知:

        树上的任意一个节点应该包含如下信息:

                ·该节点在树中的深度

                ·该节点的估价函数值 f(n)

                · 该节点的数码序列【抽象为一维数组】

                        ②.open表和closed表

由上表可知:

        open表中存放的是按估价函数值排序的节点,closed表存放的是每次从循环从open表中取出的第一个节点,直到取出节点的数码序列等于最终的数码序列,算法结束。 

          3.无解的情况

首先明确: 八数码问题是存在无解的情况的,在编写程序时,首先就需要判断初始序列和目标序列之间的变换是否是有解的,若将无解的两个序列执行算法,算法将入死循环

        两个状态有无解的判断,由两个序列的逆序数奇偶性决定,若两序列逆序数同为奇数或同为偶数,则两序列的变换有解,否则无解。

        何为逆序数? 如何求逆序数? 看这篇文章,这里不多赘述  逆序数文章

            4.操作算子的选择

                       ①.边界条件                   

                         ②.防止死循环

前一次执行UP操作,下一次迭代时应该禁用DOWN操作

前一次执行LEFT操作,下一次迭代应该禁用RIGHT操作 ,

反之亦然,目的是为了避免出现死循环

具体示例如下图:        

                总之,操作算子的选择在不考虑②中的约束条件时,其选取是的结果是十分固定的,例如0位置只能选择DOWN和RIGHT,5位置只能选择LEFT,UP和DOWN。在实际程序中可根据元素在数组中的位置+约束条件得到此次可选择的操作算子。

        5.数据结构的设计

        经过如上分析可知,要实现算法,关键要设计出搜索树中每个节点的数据结构,本次设计的结构如下:

class statusObject:
    def __init__(self):
        # 当前状态的序列
        self.array = []
        # 当前状态的估价函数值
        self.Fn = 0
        # cameFrom表示该状态由上一步由何种operation得到 
        # 目的是为了过滤 【死循环】
        # 0表示初始无状态 1表示up 2表示down 3表示left 4表示right
        self.cameFrom = 0
        # 第一次生成该节点时在图中的深度 计算估价函数使用
        self.Dn = 0 
        # 该节点的父亲节点,用于最终溯源最终解
        self.Father = statusObject

三.算法实现【代码部分】

         1.流程图:

                 2.程序源码

程序使用了numpy包,运行前请自行安装

另外在调试过程中使用了很多print语句查看结果,现已注释,若不需要请自行删除

import operator
import sys

import numpy as np


class statusObject:
    def __init__(self):
        # 当前状态的序列
        self.array = []
        # 当前状态的估价函数值
        self.Fn = 0
        # cameFrom表示该状态由上一步由何种operation得到
        # 目的是为了过滤 【死循环】
        # 0表示初始无状态 1表示up 2表示down 3表示left 4表示right
        self.cameFrom = 0
        # 第一次生成该节点时在图中的深度 计算估价函数使用
        self.Dn = 0
        self.Father = statusObject


def selectOperation(i, cameFrom):
    # @SCY164759920
    # 根据下标和cameFromReverse来选择返回可选择的操作
    selectd = []
    if (i >= 3 and i <= 8 and cameFrom != 2):  # up操作
        selectd.append(1)
    if (i >= 0 and i <= 5 and cameFrom != 1):  # down操作
        selectd.append(2)
    if (i == 1 or i == 2 or i == 4 or i == 5 or i == 7 or i == 8):  # left操作
        if (cameFrom != 4):
            selectd.append(3)
    if (i == 0 or i == 1 or i == 3 or i == 4 or i == 6 or i == 7):  # right操作
        if (cameFrom != 3):
            selectd.append(4)
    return selectd


def up(i):
    return i - 3


def down(i):
    return i + 3


def left(i):
    return i - 1


def right(i):
    return i + 1

def setArrayByOperation(oldIndex, array, operation):
    # i为操作下标
    # 根据operation生成新状态
    if (operation == 1):  # up
        newIndex = up(oldIndex)  # 得到交换的下标
    if (operation == 2):  # down
        newIndex = down(oldIndex)
    if (operation == 3):  # left
        newIndex = left(oldIndex)
    if (operation == 4):  # right
        newIndex = right(oldIndex)
    # 对调元素的值
    temp = array[newIndex]
    array[newIndex] = array[oldIndex]
    array[oldIndex] = temp
    return array


def countNotInPosition(current, end):  # 判断不在最终位置的元素个数
    count = 0  # 统计个数
    current = np.array(current)
    end = np.array(end)
    for index, item in enumerate(current):
        if ((item != end[index]) and item != 0):
            count = count + 1
    return count


def computedLengthtoEndArray(value, current, end):  # 两元素的下标之差并去绝对值
    def getX(index):  # 获取当前index在第几行
        if 0 <= index <= 2:
            return 0
        if 3 <= index <= 5:
            return 1
        if 6 <= index <= 8:
            return 2

    def getY(index):  # 获取当前index在第几列
        if index % 3 == 0:
            return 0
        elif (index + 1) % 3 == 0:
            return 2
        else:
            return 1

    currentIndex = current.index(value)  # 获取当前下标
    currentX = getX(currentIndex)
    currentY = getY(currentIndex)
    endIndex = end.index(value)  # 获取终止下标
    endX = getX(endIndex)
    endY = getY(endIndex)
    length = abs(endX - currentX) + abs(endY - currentY)
    return length

def countTotalLength(current, end):
    # 根据current和end计算current每个棋子与目标位置之间的距离和【除0】
    count = 0
    for item in current:
        if item != 0:
            count = count + computedLengthtoEndArray(item, current, end)
    return count

def printArray(array):  # 控制打印格式
    print(str(array[0:3]) + '\n' + str(array[3:6]) + '\n' + str(array[6:9]) + '\n')

def getReverseNum(array):  # 得到指定数组的逆序数 包括0
    count = 0
    for i in range(len(array)):
        for j in range(i + 1, len(array)):
            if array[i] > array[j]:
                count = count + 1
    return count


openList = []  # open表  存放实例对象
closedList = []  # closed表
endArray = [1, 2, 3, 8, 0, 4, 7, 6, 5]  # 最终状态
countDn = 0  # 执行的次数

initObject = statusObject()  # 初始化状态
# initObject.array = [2, 8, 3, 1, 6, 4, 7, 0, 5]
initObject.array = [2, 8, 3, 1, 6, 4, 7, 0, 5]
# initObject.array = [2, 1, 6, 4, 0, 8, 7, 5, 3]
initObject.Fn = countDn + countNotInPosition(initObject.array, endArray)
# initObject.Fn = countDn + countTotalLength(initObject.array, endArray)
openList.append(initObject)
zeroIndex = openList[0].array.index(0)
# 先做逆序奇偶性判断  0位置不算
initRev = getReverseNum(initObject.array) - zeroIndex  # 起始序列的逆序数
print("起始序列逆序数", initRev)
endRev = getReverseNum(endArray) - endArray.index(0)  # 终止序列的逆序数
print("终止序列逆序数", endRev)
res = countTotalLength(initObject.array, endArray)
# print("距离之和为", res)
# @SCY164759920
# 若两逆序数的奇偶性不同,则该情况无解

if((initRev%2==0 and endRev%2==0) or (initRev%2!=0 and endRev%2!=0)):
    finalFlag = 0
    while(1):
        # 判断是否为end状态
        if(operator.eq(openList[0].array,endArray)):
            # 更新表,并退出
            deep = openList[0].Dn
            finalFlag = finalFlag +1
            closedList.append(openList[0])
            endList = []
            del openList[0]
            if(finalFlag == 1):
                father = closedList[-1].Father
                endList.append(endArray)
                print("最终状态为:")
                printArray(endArray)
                while(father.Dn >=1):
                    endList.append(father.array)
                    father = father.Father
                endList.append(initObject.array)
                print("【变换成功,共需要" + str(deep) +"次变换】")
                for item in reversed(endList):
                    printArray(item)
                sys.exit()
        else:
            countDn = countDn + 1
            # 找到选中的状态0下标
            zeroIndex = openList[0].array.index(0)
            # 获得该位置可select的operation
            operation = selectOperation(zeroIndex, openList[0].cameFrom)
            # print("0的下标", zeroIndex)
            # print("cameFrom的值", openList[0].cameFrom)
            # print("可进行的操作",operation)
            # # print("深度",openList[0].Dn)
            # print("选中的数组:")
            # printArray(openList[0].array)
            # 根据可选择的操作算出对应的序列
            tempStatusList = []
            for opeNum in operation:
                # 根据操作码返回改变后的数组
                copyArray = openList[0].array.copy()
                newArray = setArrayByOperation(zeroIndex, copyArray, opeNum)
                newStatusObj = statusObject()  # 构造新对象插入open表
                newStatusObj.array = newArray
                newStatusObj.Dn = openList[0].Dn + 1 # 更新dn 再计算fn
                newFn = newStatusObj.Dn + countNotInPosition(newArray, endArray)
                # newFn = newStatusObj.Dn + countTotalLength(newArray, endArray)
                newStatusObj.Fn = newFn
                newStatusObj.cameFrom = opeNum
                newStatusObj.Father = openList[0]
                tempStatusList.append(newStatusObj)
            # 将操作后的tempStatusList按Fn的大小排序
            tempStatusList.sort(key=lambda t: t.Fn)
            # 更新closed表
            closedList.append(openList[0])
            # 更新open表
            del openList[0]
            for item in tempStatusList:
                openList.append(item)
            # 根据Fn将open表进行排序
            openList.sort(key=lambda t: t.Fn)
            # print("第"+str(countDn) +"次的结果:")
            # print("open表")
            # for item in openList:
            #     print("Fn" + str(item.Fn))
            #     print("操作" + str(item.cameFrom))
            #     print("深度"+str(item.Dn))
            #     printArray(item.array)
            #      @SCY164759920
            # print("closed表")
            # for item2 in closedList:
            #     print("Fn" + str(item2.Fn))
            #     print("操作" + str(item2.cameFrom))
            #     print("深度" + str(item2.Dn))
            #     printArray(item2.array)
            # print("==================分割线======================")
else:
    print("该种情况无解")

2022.10.28 13:32更新: 

        经测试后发现,源代码的输出在某些情况下会BUG,现已修改,修改了原数据结构,在每个节点中添加了“Father”属性,用以存放每个节点的父亲节点。修改后,经测试已经能正常输出,若读者是在更新时间之后阅读本文,直接可忽略。

更新:

         原程序的启发函数只提供了w(n)的方法,现更新p(n)的实现:

        【p(n)即:节点n的每个棋子与目标位置之间的距离总和】

修改方法:

        将源程序中两处计算Fn的函数进行替换 并添加两个计算函数

第一处

 【原】:initObject.Fn = countDn + countNotInPosition(initObject.array, endArray)

【替换】:initObject.Fn = countDn + countTotalLength(initObject.array, endArray)

第二处:

【原】:newFn = newStatusObj.Dn + countNotInPosition(newArray, endArray)

【替换】:newFn = newStatusObj.Dn + countTotalLength(newArray, endArray)

添加两个计算函数:

def computedLengthtoEndArray(value, current, end):  # 两元素的下标之差并去绝对值
    def getX(index):  # 获取当前index在第几行
        if 0 <= index <= 2:
            return 0
        if 3 <= index <= 5:
            return 1
        if 6 <= index <= 8:
            return 2

    def getY(index):  # 获取当前index在第几列
        if index % 3 == 0:
            return 0
        elif (index + 1) % 3 == 0:
            return 2
        else:
            return 1

    currentIndex = current.index(value)  # 获取当前下标
    currentX = getX(currentIndex)
    currentY = getY(currentIndex)
    endIndex = end.index(value)  # 获取终止下标
    endX = getX(endIndex)
    endY = getY(endIndex)
    length = abs(endX - currentX) + abs(endY - currentY)
    return length

def countTotalLength(current, end):
    # 根据current和end计算current每个棋子与目标位置之间的距离和【除0】
    count = 0
    for item in current:
        if item != 0:
            count = count + computedLengthtoEndArray(item, current, end)
    return count

分别运行启发函数取w(n)p(n)发现,在本次选取的示例转换中:

        取p(n)时转换过程共需要5步

        取w(n)时转换过程共需要5步

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/683641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作系统——Linux 进程间通信

一&#xff1a;实验题目 Linux 进程间通信 二&#xff1a;实验目的 Linux 系统的进程通信机构&#xff08;IPC&#xff09;允许在任意进程间大批量地交换数据&#xff0c;通过本实验&#xff0c;理解 熟悉 Linux 支持的消息通信机制。 三&#xff1a;实验内容&#xff08;…

企业级微服务架构实战项目--xx优选-小程序安装篇4

一 搭建微信小程序前端环境 1.1 申请小程序测试号 申请地址&#xff1a;微信公众平台 使用微信扫描二维码进行申请&#xff0c;申请成功之后&#xff0c;进入界面&#xff0c;获取小程序ID和秘钥 。 AppID(小程序ID) wx62bfe844fea3eba8 AppSecret(小程序密钥) …

JavaScript ES9新特性

文章目录 Object spread syntax&#xff08;对象扩展语法&#xff09;Promise.prototype.finally()Asynchronous Iteration&#xff08;异步迭代&#xff09;&#xff1a;Rest/Spread Properties&#xff08;剩余和扩展属性&#xff09;&#xff1a;RegExp named capture group…

Apikit 自学日记:导入、导出文档

一、导入API文档 功能入口&#xff1a;API管理应用 / 选中某个项目 / API文档菜单 / 点击“ API”按钮旁的下拉按钮 / 选中“导入API” Eolink提供一键导入已有API文档的功能&#xff0c;支持多种产品格式。常用于新项目创建后&#xff0c;需要对旧项目或旧软件内数据进行迁移…

认识设计组件帮助测试,以提高产品用户体验

一、控制元素 1、活动指示器——应与背景想协调&#xff0c;用于持续时间不明的进程&#xff0c;单一元素不显示&#xff0c;大于1个显示 2、加载控件——同一个专区页面&#xff0c;加载样式统一 3、页码控制器——原点最好控制在5点内&#xff0c;左右滑动&#xff0c;点击原…

OpenCL编程指南-5.5图像读、写函数

读图像 OpenCL GPU设备有专用硬件来读、写图像。OpenCL C图像读、写函数允许开发人员充分利用这个专用硬件。OpenCL 中的图像支持是可选的。要了解一个设备是否支持图像&#xff0c;可以使用clGetDeviceInfo API查询CL_DEVICE_IMAGE_SUPPORT属性。 需要说明的是&#xff0…

c4d+AI+PS设计广告展示架/销售柜台/展示盒子的建议

1、首先做出我标识出来的样子&#xff0c;这里称作A面。&#xff08;可用软件&#xff1a;PS、AI、cdr等&#xff09; 2、制作用于展示盒A面PNG图片&#xff08;PS来掏空空白处用于描边&#xff09;。 操作&#xff1a;按需求缩小图片&#xff0c;载入选区&#xff0c;新建图层…

three.js标准网格材质(MeshStandardMaterial)光照、粗糙度、金属度、法线属性介绍

如上图&#xff0c;在前面的章节中我们通过设置物体的纹理和材质实现了一个3d的立体门框的效果 完整代码如下&#xff1a; import * as THREE from "three"; // 导入轨道控制器 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls&quo…

新媒体运营工作总结

新媒体运营工作总结篇1 一、大力提升新媒体平台的关注度。在当前掩盖全校60%的同学的基础上&#xff0c;进一步提高掩盖率&#xff0c;争取在未来一年中到达90%以上。为了增强平台与受众的互动与交流&#xff0c;吸引同学们的关注&#xff0c;可以展开更多同学们喜闻乐见的线上…

macOS FreeBSD 如何刷新 DNS 缓存

macOS FreeBSD 如何刷新 DNS 缓存 全文&#xff1a;如何刷新 DNS 缓存 (macOS, Linux, Windows) Unix Linux Windows 如何刷新 DNS 缓存 (macOS, FreeBSD, RHEL, CentOS, Debian, Ubuntu, Windows) 请访问原文链接&#xff1a;https://sysin.org/blog/how-to-flush-dns-cach…

(动态规划) 5. 最长回文子串 ——【Leetcode每日一题】

❓ 5. 最长回文子串 难度&#xff1a;中等 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s “babad” 输出&#xff1a;“bab” 解释&#xff1a…

Pytest+Webdriver+Alluer的UI自动化测试框架

作为web自动化的入门学习&#xff0c;搭建框架练习下 一、熟悉项目的测试框架的整体目录 二、 PIP安装完所需框架 1、编写main.py import pytestif __name__ __main__:#pytest.main() # 遍历相同目录下的所以test开头的用例#生成测试报告 #一次执行所有接口测试用例&…

【消费战略方法论】3W消费战略作业方法

3W消费战略 以消费者为核心导向的 品牌战略 品牌本质上属于消费者&#xff0c;消费者是品牌战略的核心。所有品牌工作都应把握一个标准&#xff0c;即是否围绕着消费者&#xff0c;是否对消费者有价值。一切的品牌战略、策略、创意、营销等工作都应该是为了“消费者”而设…

【Java|多线程与高并发】定时器(Timer)详解

文章目录 1. 前言2. 定时器的基本使用3. 实现定时器4. 优化上述的定时器代码5. 总结 1. 前言 在Java中&#xff0c;定时器Timer类是用于执行定时任务的工具类。它允许你安排一个任务在未来的某个时间点执行&#xff0c;或者以固定的时间间隔重复执行。 在服务器开发中,客户端向…

原型模式(Prototype)

定义 原型是一种创建型设计模式&#xff0c;使你能够复制已有对象&#xff0c;而又无需使代码依赖它们所属的类。 别名 克隆&#xff08;Clone&#xff09;。 前言 1. 问题 如果你有一个对象&#xff0c;并希望生成与其完全相同的一个复制品&#xff0c;你该如何实现呢&a…

基于工业智能网关的设备运维管理平台有何功能?

工业物联网平台作为监控工业设备和工业环境的智能应用&#xff0c;整合边缘和云端的数据优势&#xff0c;在制造业领域得到越来越丰富的应用。 在工业制造生产过程中&#xff0c;常常分为人、机、料、法、环等五大要素&#xff0c;其中机器设备的安全稳定运行时保证工厂生产效…

58同城AI Lab在WeNet中开源GPU热词增强功能

01 前言 端到端语音识别系统在足够多数据上训练后&#xff0c;往往能达到不错的识别效果&#xff0c;然而在实际应用场景中&#xff0c;对于不常见的专有名词&#xff0c;例如人名、产品名、小区名等&#xff0c;往往容易识别错误&#xff0c;此类问题需要快速修复&#xff0c…

DNS是什么?DNS的工作流程

79. DNS是什么&#xff1f; DNS&#xff08;Domain Name System&#xff09;是一种用于将域名解析为相应IP地址的分布式命名系统&#xff0c;了解DNS对于理解域名解析原理和优化网络请求非常重要。本篇文章将介绍DNS的概念、工作原理以及在前端开发中的应用&#xff0c;帮助前…

正则表达式-捕获组,命名捕获组,非捕获组

正则表达式的作用 测试目标字符串是否符合规则 返回true/false按照规则从目标字符串提取内容 返回匹配的数组 在线测试工具 regex101: build, test, and debug regexRegular expression tester with syntax highlighting, explanation, cheat sheet for PHP/PCRE, Python, …

java适配达梦数据库

目录 一、数据库安装 二、数据库可视化工具 三、数据迁移 四、工程适配 新增maven依赖 配置文件修改 基于flyway的数据库版本管理 五、注意事项 一、数据库安装 官方文档&#xff1a;安装前准备 | 达梦技术文档 这里有一个点需要注意&#xff0c;如果你之前的数据库或…