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

一.问题描述

        八数码问题也称为九宫问题。在 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.实际执行过程

                        ①.搜索树

✅2023/12/17修正:

下图搜索树中的A(4)应该修改为A(6)

因为此时序列中共有:

2,8,3,1,6,4,(0),7,5 六个元素(标红)不在【目标位置】

1,2,3,8,(0),4,7,6,5(目标序列)

由上图的搜索树可知:

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

                ·该节点在树中的深度

                ·该节点的估价函数值 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步

### 使用 Python 实现 A* 算法解决八数码问题 #### 一、背景介绍 八数码问题是经典的启发式搜索问题之一,目标是从初始状态移动到目标状态。A*算法一种基于代价评估的最优路径搜索方法,在求解此类问题时具有较高的效率。 #### 二、核心概念 1. **启发函数 (Heuristic Function)** 启发函数用于估算当前节点到达目标节点的成本。常用的启发函数有曼哈顿距离和欧几里得距离。对于八数码问题,通常采用曼哈顿距离作为启发值计算公式[^2]。 2. **优先队列 (Priority Queue)** A*算法依赖于优先队列存储待扩展节点,按照 \(f(n) = g(n) + h(n)\) 的总成本排序,其中: - \(g(n)\): 当前节点的实际代价(从起点到此节点的距离)。 - \(h(n)\): 启发估计值(从当前节点到终点的预计代价)。 3. **状态表示** 八数码的状态可以用二维数组或一维列表表示。例如,`[1, 2, 3, 4, 5, 6, 7, 8, 0]` 表示一个可能的棋盘布局,其中 `0` 是空白格子的位置。 --- #### 三、Python实现代码 以下是完整的 Python 实现: ```python import heapq def manhattan_distance(state): """ 计算曼哈顿距离 """ distance = 0 goal_state = [[1, 2, 3], [4, 5, 6], [7, 8, 0]] for i in range(3): for j in range(3): value = state[i][j] if value != 0: target_x, target_y = divmod(value - 1, 3) distance += abs(i - target_x) + abs(j - target_y) return distance class Node: def __init__(self, state, parent=None, move_cost=0): self.state = state self.parent = parent self.move_cost = move_cost self.heuristic = manhattan_distance(self.state) @property def total_cost(self): return self.move_cost + self.heuristic def find_zero_position(self): for i in range(3): for j in range(3): if self.state[i][j] == 0: return i, j def generate_children(self): children = [] zero_i, zero_j = self.find_zero_position() directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] # 右下左上 for di, dj in directions: new_i, new_j = zero_i + di, zero_j + dj if 0 <= new_i < 3 and 0 <= new_j < 3: new_state = [row[:] for row in self.state] new_state[zero_i][zero_j], new_state[new_i][new_j] = ( new_state[new_i][new_j], new_state[zero_i][zero_j], ) child_node = Node(new_state, parent=self, move_cost=self.move_cost + 1) children.append(child_node) return children def a_star(start_state, goal_state=[[1, 2, 3], [4, 5, 6], [7, 8, 0]]): start_node = Node(start_state) open_list = [] # 存储未访问节点的优先队列 closed_set = set() # 已经访问过的节点集合 heapq.heappush(open_list, (start_node.total_cost, id(start_node), start_node)) while open_list: _, _, current_node = heapq.heappop(open_list) if current_node.state == goal_state: path = [] node = current_node while node is not None: path.append(node.state) node = node.parent return path[::-1] closed_set.add(tuple(map(tuple, current_node.state))) for child in current_node.generate_children(): if tuple(map(tuple, child.state)) in closed_set: continue existing_index = next( (i for i, (_, _, n) in enumerate(open_list) if n.state == child.state), None, ) if existing_index is None or child.total_cost < open_list[existing_index][0]: heapq.heappush(open_list, (child.total_cost, id(child), child)) if __name__ == "__main__": initial_state = [ [2, 8, 3], [1, 6, 4], [7, 0, 5], ] solution_path = a_star(initial_state) for step, state in enumerate(solution_path): print(f"Step {step}:") for row in state: print(row) print("\n") ``` --- #### 四、代码解析 1. **曼哈顿距离计算**: 曼哈顿距离衡量的是两个位置之间的直线差值之和,适用于网格环境下的最短路径估测。 2. **Node类设计**: 节点对象封装了状态 (`state`)、父节点指针 (`parent`) 和实际移动开销 (`move_cost`)。通过属性装饰器动态计算总成本 (\(f(n)\))。 3. **优先队列管理**: 利用 `heapq` 库维护开放列表,确保每次取出最低成本的节点进行扩展。 4. **终止条件**: 如果找到的目标状态等于预设的目标矩阵,则回溯生成解决方案路径。 --- #### 五、性能分析 - 时间复杂度主要取决于启发函数的选择以及具体实例的空间分布情况。理论上,A*算法的时间复杂度接近 O(b^d),其中 b 是分支因子,d 是解的深度。 - 对于八数码问题而言,由于其规模较小,上述实现能够高效运行。 --- ###
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值