人工智能课程实践-A* 算法编程实践(python实现)

本次实验的代码由python编写完成,代码在附录中,需要请自取

1. 实验目的

(1)熟悉启发式搜索算法、A搜索算法的理论、实现方法;
(2)掌握A
搜索算法的核心算法实现过程;
(3)理解A* 搜索算法如何解决现实工程问题,学会分析现实问题蕴含的搜索求解思想;
(4)编写代码实现A* 搜索算法求解八数码问题;

2. 实验内容

2.1 详细说明实验内容

编写代码实现A* 搜索算法求解八数码问题,具体实验要求如下:
(1)在一个3×3的九宫格中有1-8额数字以及一个空格,它们随机摆放在9个格子中,将该九宫格调整到目标状态;
(2)规则:每次只能将与空格(上、下、左、右)相邻的一个数字和空格交换位置;请使用A* 搜索算法,编程解决这一问题。
(3)备注:为了程序的方便实现,可以用“0”代表空格;
(4)初始状态和目标状态:均由用户通过键盘手工输入或者从文件读入(不可写死在程序中);
(5)实验的测试数据如图2-1所示;
(6)自己可以增加测试数据,但必须移动数字至少5次;
图2-1实验测试用例

2.2 描述实验过程

2.1.1 流程图

在这里插入图片描述

2.2.2 文字描述

  1. 首先我们输入初始状态与目标状态,并根据二者的逆序数是否相等,判断八数码问题是否有解。
  2. 初始open表和close表,open加入初始的状态S0,
  3. 开始核心算法的循环执行,循环的条件是open表非空,从open中弹出一个状态,判断该节点是否为目标节点,是的话程序结束,否则生成该状态的子状态。
    ① 子状态在close表中,说明该状态我们已经考察过了,跳过该子状态。
    ② 子状态不在close表中,我们根据子状态是否在open中,进行不同的操作
    1)子状态在open表中,根据目前的子状态与open已有的子状态的g(x)函数的值进行判断(这里个g(x)是节点的深度),进行取舍。
    2)子状态不在open表中,open直接加入该子状态
  4. open表根据f(x)的值进行从小到大的排序,继续执行第3步

3. 实验程序

3.1 根据逆序数判断是否有解

def getStatus(numList):  # 逆序数:所有排列队列在当前位置之前比当前位置上的元素大的计数总和("0"不参与计数),用于判断奇数列,还是偶数列
    res = 0
    for i in range(1, len(numList)):
        if numList[i] == 0:
            continue
        for j in range(i):
            if numList[j] > numList[i]:
                res += 1
    return res

def existSolution(a_list, b_list):  # 判断是否存在解
    a_f = getStatus(a_list)
    b_f = getStatus(b_list)
    print(a_f, b_f)
    if (a_f % 2 == b_f % 2):  ## 逆序数相同,有解
        return True
    return False

描述:
这两个函数主要用于得到初始状态和目标状态的逆序数,并且根据逆序数是否同为奇同为偶判断八数码问题是否有解

3.2 h(s)启发函数

# 启发函数   这里我们采用的是“比对和目标状态的错位数”
def h(s):
    a = 0
    for i in range(len(s.node)):
        for j in range(len(s.node[i])):
            if s.node[i][j] != goal.node[i][j]:
                a = a + 1               # 不相同,加一
    return a

描述:
这里我们采用的h(s)的启发函数,是当前状态与目标状态的错位数,当然我们可以选择着其他更加有效的启发函数,这样我们的搜索的步数更少,效率更高。

3.3 A* 核心算法

def A_star(s):
    global openlist, closed ,counter  # 全局变量可以让open,closed表进行时时更新
    openlist = [s]  # s为初始状态
    while (openlist):  # 当open表不为空
        get = openlist.pop(0)  # 取出open表的首节点
        counter += 1
        closed.append(get)  # 考察结束后加入closed表中
        if (get.node == goal.node).all():  # 判断是否与目标节点一致 all()对状态矩阵的所有元素进行与运算
            return get
        # 判断此时状态的空格位置(a,b)
        for a in range(len(get.node)):
            for b in range(len(get.node[a])):
                if get.node[a][b] == 0:
                    break
            if get.node[a][b] == 0:
                break
        # 开始移动
        row, col = len(get.node), len(get.node[0])  # 矩阵的长宽
        for dir in direction:
            i = a + dir[0]
            j = b + dir[1]
            if 0 <= i < row and 0 <= j < col:  # 没有超出边界的范围
                c = get.node.copy()
                c[a][b] = c[i][j]
                c[i][j] = 0  # 交换当前的0与该点的位置
                if (get.father !=None and (c == get.father.node).all()):   # 保证当前n的子节点不和其父节点相同,即不走回头路
                    continue
                new = State(c)
                new.father = get  # 此时取出的get节点成为新节点的父亲节点
                new.g = get.g + 1  # 新节点与父亲节点的距离
                if (isContained(closed, new) == -1):  # 不在closed中的才是我们需要的,因为我们不走回头路
                    position = isContained(openlist, new)
                    if position != -1:  # 如果在open表中,比较深度的大小来决定是否更新节点信息
                        if new.g < openlist[position].g:
                            openlist.pop(position)
                            new.h = h(new)  # 新节点的启发函数值
                            new.f = new.g + new.h  # 新节点的估价函数值
                            openlist.append(new)  # 加入open表中

                    else:
                        new.h = h(new)  # 新节点的启发函数值
                        new.f = new.g + new.h  # 新节点的估价函数值
                        openlist.append(new)  # 加入open表中
        list_sort(openlist)  # 根据f(s)的值从小到大的排序

描述:
开始核心算法的循环执行,循环的条件是open表非空,从open中弹出一个状态,判断该节点是否为目标节点,是的话程序结束,否则生成该状态的子状态。
③ 子状态在close表中,说明该状态我们已经考察过了,跳过该子状态。
④ 子状态不在close表中,我们根据子状态是否在open中,进行不同的操作
1)子状态在open表中,根据目前的子状态与open已有的子状态的g(x)函数的值进行判断(这里个g(x)是节点的深度),进行取舍。
2)子状态不在open表中,open直接加入该子状态
open表根据f(x)的值进行从小到大的排序,继续执行执行循环

4. 实验结果

4.1 测试一

4.1.1 测试数据

在这里插入图片描述

4.1.2 测试结果

在这里插入图片描述
………………………………
在这里插入图片描述

4.2 测试二

4.2.1 测试数据

在这里插入图片描述

4.2.2 测试结果

在这里插入图片描述
………………………………
在这里插入图片描述

5. 实验结果分析

1.详细分析实验结果是否符合预期。
我们可以看到实验结果可以在较少的步数中解决实际的问题,符合我们的最初的预期。其实算法的核心在于生成树的剪枝,也就是我们的启发函数,好的启发函数可以更加有效地解决问题,极大地提高算法的效率。

6. 实验思考

通过这个实验,让我对树和图的搜索算法有了更加深刻的了解,我们这个8数码的问题,可以采用很多种的搜索算法,比如BFS,DFS以及A算法,但是BFS和DFS没有启发函数的剪枝,它俩的时间复杂度和空间复杂度都是比较高的,我们采用的A算法可以通过F(x)启发函数进行树搜索算法的剪枝,我们优先考虑可能性大的节点,这样可以大大提高我们的效率,其实里面有一种“贪心算法”的策略,我们每次都贪心选择F(x)小的节点进行搜索。
还有就是我对剪枝的策略有了更加深刻的了解,我最初使用的剪枝策略是当前考察节点的子节点不和待考察节点的父节点相同,当时这样任然会造成“走回头路”,比如在之前就从open表中弹出一个节点,但是之后我们有加入了一个和之前这个节点相同的节点,这样就会“走回头路”,所以close表就非常有必要了,这样子防止我们走之前走过的路径。

附录(代码以及测试样例)

测试数据1

3
1,5,3,2,4,6,7,0,8
1,2,3,4,5,6,7,8,0

测试数据2

3
2,5,8,3,4,6,1,7,0
2,8,6,3,5,0,1,4,7

测试数据3

3
2,8,3,1,6,4,7,0,5
1,2,3,8,0,4,7,6,5

测试数据4

3
2,0,3,1,8,4,7,6,5
1,2,3,8,0,4,7,6,5

测试数据5

3
1,2,3,4,5,0,7,8,6
1,2,3,4,5,6,7,8,0

测试数据6

3
2,1,6,4,0,8,7,5,3
1,2,3,8,0,4,7,6,5

完整程序

import numpy as np
import operator
from time import time

direction = [(-1, 0), (1, 0), (0, -1), (0, 1)]  # 定义方向数组
steps = []  # 在结果的回溯时用于保存路径
openlist = []  # open表
closed = []  # closed表


class State:
    def __init__(self, m):
        self.node = m  # 数码的状态矩阵
        self.f = 0  # f(n)=g(n)+h(n)
        self.g = 0  # g(n)  深度
        self.h = 0  # h(n)  到目标的费用估计
        self.father = None  # 节点的父亲节点


def isContained(num_list, num):  # 找到列表中对应元素的下标,如果不存在返回值“-1”
    for i in range(len(num_list)):
        if (num.node == num_list[i].node).all():
            return i
    return -1


def getStatus(numList):  # 逆序数:所有排列队列在当前位置之前比当前位置上的元素大的计数总和("0"不参与计数),用于判断奇数列,还是偶数列
    res = 0
    for i in range(1, len(numList)):
        if numList[i] == 0:
            continue
        for j in range(i):
            if numList[j] > numList[i]:
                res += 1
    return res


def existSolution(a_list, b_list):  # 判断是否存在解
    a_f = getStatus(a_list)
    b_f = getStatus(b_list)
    print(f"状态1逆序数:{a_f}", f"状态2逆序数:{b_f}")
    if (a_f % 2 == b_f % 2):  ## 逆序数相同,有解
        return True
    return False


# 启发函数   这里我们采用的是“比对和目标状态的错位数”
def h(s):
    a = 0
    for i in range(len(s.node)):
        for j in range(len(s.node[i])):
            if s.node[i][j] != goal.node[i][j]:
                a = a + 1               # 不相同,加一
    return a


# 对节点列表按照估价函数的值的规则排序
def list_sort(l):
    cmp = operator.attrgetter('f')
    l.sort(key=cmp)

counter = 0  # open表弹出节点的统计
counter_all = 1 # 树规模大小的统计,初始只要根节点
# A*算法
def A_star(s):
    global openlist, closed ,counter ,counter_all # 全局变量可以让open,closed表进行时时更新
    openlist = [s]  # s为初始状态
    while (openlist):  # 当open表不为空
        get = openlist.pop(0)  # 取出open表的首节点
        counter += 1         # open表弹出节点的统计
        closed.append(get)  # 考察结束后加入closed表中
        if (get.node == goal.node).all():  # 判断是否与目标节点一致 all()对状态矩阵的所有元素进行与运算
            return get
        # 判断此时状态的空格位置(a,b)
        for a in range(len(get.node)):
            for b in range(len(get.node[a])):
                if get.node[a][b] == 0:
                    break
            if get.node[a][b] == 0:
                break
        # 开始移动
        row, col = len(get.node), len(get.node[0])  # 矩阵的长宽
        for dir in direction:
            i = a + dir[0]
            j = b + dir[1]
            if 0 <= i < row and 0 <= j < col:  # 没有超出边界的范围
                counter_all += 1                 # 树上已生成节点的统计
                c = get.node.copy()
                c[a][b] = c[i][j]
                c[i][j] = 0  # 交换当前的0与该点的位置
                if (get.father !=None and (c == get.father.node).all()):   # 保证当前n的子节点不和其父节点相同,即不走回头路
                    continue
                new = State(c)
                new.father = get  # 此时取出的get节点成为新节点的父亲节点
                new.g = get.g + 1  # 新节点与父亲节点的距离
                if (isContained(closed, new) == -1):  # 不在closed中的才是我们需要的,因为我们不走回头路
                    position = isContained(openlist, new)
                    if position != -1:  # 如果在open表中,比较深度的大小来决定是否更新节点信息
                        if new.g < openlist[position].g:
                            openlist.pop(position)
                            new.h = h(new)  # 新节点的启发函数值
                            new.f = new.g + new.h  # 新节点的估价函数值
                            openlist.append(new)  # 加入open表中

                    else:
                        new.h = h(new)  # 新节点的启发函数值
                        new.f = new.g + new.h  # 新节点的估价函数值
                        openlist.append(new)  # 加入open表中
        list_sort(openlist)  # 根据f(s)的值从小到大的排序


# 递归打印路径
def getpath(f):
    global steps
    if f is None:
        return
    # 注意添加语句放在递归调用前和递归调用后的区别。放在后实现了倒叙添加
    getpath(f.father)
    steps.append(f.node)


## 输入数据,并进行是否有解的初步判断,根据“偶排列与奇排列”
O, A, B = None, None, None
with open("input.txt", mode="r", encoding='utf-8') as f:
    O = int(f.readline().strip())
    A = [int(i) for i in f.readline().strip().split(",")]
    B = [int(i) for i in f.readline().strip().split(",")]
print(f"矩阵的规模:{O}",f"初始状态{A}", f"目标状态{B}")
# O=int(input(("请输入方阵的行/列数:")))
# A=list(input("请输入初始状态:").split(","))
# B=list(input("请输入目标状态:").split(","))

if existSolution(A, B):  # 初步判断有解
    # 初始化过程 :定义初始和结束状态
    z = 0
    M = np.zeros((O, O))
    N = np.zeros((O, O))
    for i in range(O):
        for j in range(O):
            M[i][j] = A[z]
            N[i][j] = B[z]
            z = z + 1

    init = State(M)  # 初始状态
    goal = State(N)  # 目标状态
    time1 = time()
    final = A_star(init)
    if final:
        print("有解,解为:")
        getpath(final)
        for i, item in enumerate(steps):
            print(f"*****第{i}步*****")
            print(item)
        print("总共考察了%d个节点"%counter)
        print(f"该树已生成的节点数为{counter_all}")
        print(f"总用时为{time()-time1}s")
    else:
        print("无解")


else:
    print("该八数码问题没有解,请重新选择!")

A*算法是一种启发式搜索算法,常用于求解路径问题。下面以8数码和迷宫问题为例,来介绍A*算法的过程。 1. 8数码问题 1.1 状态表示 8数码问题是指在3×3的九宫格上,摆放了1-8这8个数字,其中有一个格子是空的,求解将这8个数字移动到目标状态的过程。我们可以用一个3×3的矩阵来表示当前状态,其中空格用0表示。 1.2 操作集 对于8数码问题,每次只能将空格与上下左右四个方向之一的数字进行交换,因此操作集可以表示为{Up, Down, Left, Right}。 1.3 估价函数 估价函数用于评估当前状态到达目标状态的距离,一般情况下,估价函数越小,搜索的效率越高。在8数码问题中,我们可以使用曼哈顿距离作为估价函数。曼哈顿距离是指对于一个点(x,y),到目标点(x',y')的距离为|x-x'| + |y-y'|。因此,对于当前状态s,到达目标状态的估价函数值可以表示为: h(s) = Σi=1^8 ManhattanDistance(s[i], goal[i]) 其中,s[i]表示当前状态中数字i的位置,goal[i]表示目标状态中数字i的位置。 1.4 OPEN表和CLOSED表 OPEN表是用来存储待扩展的状态的集合,CLOSED表是用来存储已经扩展过的状态的集合。在每次扩展状态时,如果该状态已经存在于CLOSED表中,则可以直接跳过;否则,将该状态加入OPEN表中。 1.5 A*算法步骤 1. 初始化OPEN表和CLOSED表,将初始状态加入OPEN表中。 2. 从OPEN表中选择f值最小的状态s,如果该状态为目标状态,则搜索结束;否则,将该状态从OPEN表中移除,并将其加入CLOSED表中。 3. 对于状态s,枚举所有可能的操作,得到所有可能的后继状态。 4. 对于每个后继状态s',计算其估价函数值f(s') = g(s) + h(s'),其中g(s)表示从初始状态到状态s的实际距离。如果状态s'已经存在于OPEN表或CLOSED表中,则比较其f值和原来的f值,选择较小的一个;否则,将状态s'加入OPEN表中。 5. 重复步骤2-4,直到找到目标状态或OPEN表为空。 2. 迷宫问题 2.1 状态表示 迷宫问题是指在一个矩形网格中,从起点出发,到达终点的最短路径。我们可以用一个二维数组来表示迷宫,其中0表示可通过的空格,1表示不可通过的障碍物。 2.2 操作集 对于迷宫问题,每次只能向上下左右四个方向之一移动一格,因此操作集可以表示为{Up, Down, Left, Right}。 2.3 估价函数 估价函数用于评估当前状态到达目标状态的距离,一般情况下,估价函数越小,搜索的效率越高。在迷宫问题中,我们可以使用欧式距离作为估价函数。欧式距离是指对于两个点(x1,y1)和(x2,y2),它们之间的距离为√((x1-x2)^2 + (y1-y2)^2)。因此,对于当前状态s,到达目标状态的估价函数值可以表示为: h(s) = EuclideanDistance(s, goal) 其中,s表示当前状态的位置,goal表示目标状态的位置。 2.4 OPEN表和CLOSED表 OPEN表和CLOSED表的作用同8数码问题中的作用相同。 2.5 A*算法步骤 1. 初始化OPEN表和CLOSED表,将起点加入OPEN表中。 2. 从OPEN表中选择f值最小的状态s,如果该状态为终点,则搜索结束;否则,将该状态从OPEN表中移除,并将其加入CLOSED表中。 3. 对于状态s,枚举所有可能的操作,得到所有可能的后继状态。 4. 对于每个后继状态s',计算其估价函数值f(s') = g(s) + h(s'),其中g(s)表示从起点到状态s的实际距离。如果状态s'已经存在于OPEN表或CLOSED表中,则比较其f值和原来的f值,选择较小的一个;否则,将状态s'加入OPEN表中。 5. 重复步骤2-4,直到找到终点或OPEN表为空。 以上就是A*算法求解8数码、迷宫问题的过程,希望能对你有所帮助。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

RockLis

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

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

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

打赏作者

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

抵扣说明:

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

余额充值