本次实验的代码由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.2 描述实验过程
2.1.1 流程图
2.2.2 文字描述
- 首先我们输入初始状态与目标状态,并根据二者的逆序数是否相等,判断八数码问题是否有解。
- 初始open表和close表,open加入初始的状态S0,
- 开始核心算法的循环执行,循环的条件是open表非空,从open中弹出一个状态,判断该节点是否为目标节点,是的话程序结束,否则生成该状态的子状态。
① 子状态在close表中,说明该状态我们已经考察过了,跳过该子状态。
② 子状态不在close表中,我们根据子状态是否在open中,进行不同的操作
1)子状态在open表中,根据目前的子状态与open已有的子状态的g(x)函数的值进行判断(这里个g(x)是节点的深度),进行取舍。
2)子状态不在open表中,open直接加入该子状态 - 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("该八数码问题没有解,请重新选择!")