一起来看我的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("初始状态和目标状态逆序奇偶性不一致,该问题无解")