问题描述:
八数码问题也称为九宫问题。在3×3的棋盘上摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格(数字表为0),与空格相邻的棋子可以移到空格中。
需要解决的问题:给出一个初始状态和一个目标状态,求出从初始状态转变成目标状态的移动棋子的最少步数。
原理:启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径,提高了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。
估价函数:
f ∗ ( n ) = h ∗ ( n ) + g ∗ ( n ) f^*(n) = h^*(n) + g^*(n) f∗(n)=h∗(n)+g∗(n)
其中,在任意节点 n n n上定义, g ∗ ( n ) g^*(n) g∗(n)为从初始节点 S S S到节点 n n n的代价; h ∗ ( n ) h^*(n) h∗(n)为从节点 n n n到目标节点 E E E的代价;则 f ∗ ( n ) f^*(n) f∗(n)定义为从初始节点S经过节点 n n n到达目标节点 E E E的最佳路径代价。
定义 f ∗ ( n ) f^*(n) f∗(n) 的估计函数 f ( n ) f(n) f(n): f ( n ) = h ( n ) + g ( n ) f(n) = h(n) + g(n) f(n)=h(n)+g(n)
其中, g ( n ) g(n) g(n)是 g ∗ ( n ) g^*(n) g∗(n)的估计函数, f ( n ) f(n) f(n)是 f ∗ ( n ) f^*(n) f∗(n)的估计函数。这样定义的估价函数为 A 算 A算 A算法
启发式 A A A算法:
在 A A A算法中,当所有节点对于 n n n, 0 < = h ( n ) < = h ∗ ( n ) 0<=h(n)<=h^*(n) 0<=h(n)<=h∗(n)时(保证了算法的可采纳性), A A A算法成为 A ∗ A^* A∗算法。
代码实现时,笔者用State类表示搜索状态,数位digits用长度为9的一维列表表示。 g ( n ) g(n) g(n)为运行深度, h ( n ) h(n) h(n)为可变函数(可以类外更改)。 A ∗ A^* A∗算法按照算法描述实现。(代码可进一步优化,例如找最小 f f f值时,笔者偷懒 采用扫描查找,可以通过后续扩展节点插入OPEN表时采用二分插入排序的方法进行优化)
代码中提供了两个可使用的 h ( n ) h(n) h(n)函数(默认恒为0,原算法即为BFS搜索),其中第一个函数的代价定义为每一位数码与其目标位置之间的距离的总和;第二个函数的代价定义为数码错放的数量。
八数码的可解性问题,详细证明自行搜索,这里直接提供结论:把数字排列组成数列,如果初始状态和目标状态的数列的逆序数的奇偶性相同,则可由初始状态移动至目标状态,反之不可。
代码写的垃圾,勿喷
# 状态类
class State:
# h(n) 将付出的代价
h = lambda self: 0
def __init__(self, digits, depth, pa):
# 数位
self.digits = digits
# 父状态
self.pa = pa
# 深度 g(n)已付出的代价
self.depth = depth
# 估价函数
self.f = self.__f()
# 交换zero位和next位的数
def get_digits(self, zero, next):
digits = list(self.digits)
digits[zero] = digits[next]
digits[next] = 0
return digits
# 获取可移动的下一个digits
def next_digits(self):
zero = [i for i, v in enumerate(self.digits) if v == 0][0]
if int(zero / 3) != 0:
yield self.get_digits(zero, zero-3)
if int(zero / 3) != 2:
yield self.get_digits(zero, zero+3)
if zero % 3 != 0:
yield self.get_digits(zero, zero-1)
if zero % 3 != 2:
yield self.get_digits(zero, zero+1)
# 获取下一个可转换状态(父状态不在内)
def next_state(self):
for d in self.next_digits():
if not self.pa or d != self.pa.digits:
yield State(d, self.depth + 1, self)
# 计算估价函数f(n) = g(n) + h(n)
def __f(self):
return self.depth + State.h(self) * 2
# 更换h(x)
@staticmethod
def change_h(h):
State.h = h
pass
def __eq__(self, other):
return self.digits == other.digits
def __str__(self):
res = 'depth = %d, f(x) = %d\n' % (self.depth, self.f)
for i in range(3):
res += str(self.digits[i*3: (i+1)*3]) + '\n'
return res
class EightDigits:
def __init__(self, s, e = [1, 2, 3, 8, 0, 4, 7, 6, 5]):
# 初始状态
self.s = State(s, 0, None)
# 目标状态
self.e = e
self.open = []
self.close = []
self.open.append(self.s)
# 判断奇偶排列
def isOdd(self, digits):
num = 0
for i in range(9):
if digits[i] == 0:
continue
for j in range(i + 1, 9):
if digits[j] == 0:
continue
if digits[i] > digits[j]:
num += 1
return num % 2 == 1
# A*算法
def A(self):
# 判断可不可解
if self.isOdd(self.s.digits) != self.isOdd(self.e):
return None
num = 0
while self.open:
front = min(self.open, key=lambda x: x.f)
self.open.remove(front)
num += 1
print(front)
if front.digits == self.e:
# 搜索次数
print("-" * 10 + str(num) + "-" * 10 + '\n')
return front
self.close.append(front)
for next in front.next_state():
if next in self.open:
for i, o in enumerate(self.open):
if next == o:
if next.f < o.f:
self.open[i] = next
break
elif next in self.close:
for c in self.close:
if next == c:
if next.f < c.f:
self.close.remove(c)
self.open.append(next)
break
else:
self.open.append(next)
return None
@staticmethod
def show(state):
if state.pa:
EightDigits.show(state.pa)
print(state)
# h函数1 代价为所有数恢复原有状态所移动距离的代价总和
def p(s):
e = [1, 2, 3, 8, 0, 4, 7, 6, 5]
digits = s.digits
res = 0
for i in range(9):
for j, v in enumerate(digits):
if v == e[i]:
t = abs(i - j)
res += int(t / 3) + t % 3
break
return res
# h函数2 代价为不在正确位置的数的数量
def w(s):
e = [1, 2, 3, 8, 0, 4, 7, 6, 5]
digits = s.digits
return len([i for i in range(9) if e[i] != digits[i]])
if __name__ == '__main__':
State.change_h(p) # 采用h函数1
eight_digits = EightDigits([2, 1, 3, 0, 8, 7, 4, 6, 5])
state = eight_digits.A()
if not state:
print('初始状态无法移动至目标状态')
else:
EightDigits.show(state)