宽度优先搜索算法解决八数码问题
原理
1、宽度优先搜索是指在一个搜索树中,搜索以同层邻近节点依次扩展节点。这种搜索是逐层进行的,在对下一层的任一节点进行搜索之前,必须搜索完本层的所有节点。
宽度优先搜索算法主要步骤可描述如下:
①令N为一个由初始状态构成的表。
②若N为空退出,标志失败。
③令n为N中第一个节点,将n从N中删除。
④若n是目标,则退出,标志成功。
⑤若n不是目标,将n的后继节点加入到N表的末端,转第②步。
宽度优先搜索算法流程图下图所示:
2、八数码问题知识表示方法(状态空间法)分析:
1.定义状态空间:根据八数码问题定义出状态空间。然后通过对象给出问题的初始状态、目标状态,给出状态的一般表示。
2.定义操作规则:规定一组算子,作用于一个状态后过渡到另一个状态。
3.定义搜索策略:使用宽度优先搜索,使得能够从初始状态出发,沿某个路径达到目标状态。搜索过程为:沿着4个方向,如果可行都前进一步看是否达到位置。如果没有达到,则依次从新的位置为起点,沿4个方向继续前进一步,直到搜索到目标位置或者找不到未搜索的位置为止。
3、八数码问题判断有无解
对于棋子数列中任何一个棋子c[i],如果有j>i且c[j]<c[i],那么 c[j]是c[i]的一个逆序,或者c i]和c[j]构成一个逆序对。定义棋子c[i]的逆序数为c[i]的逆序个数;棋子数列的逆序数为该数列所有棋子的逆序数总和。
定理:如果棋子数列经过n次相邻棋子交换后,若n为偶数,则数列逆序数奇偶性不变;若n为奇数,则数列逆序数将发生奇偶性互变。
推广:
(1)当初始状态棋局的棋子数列的逆序数是奇数时,八数码问题无解;
(2)当初始状态棋局的棋子数列的逆序数是偶数时,八数码问题有解。
设计思路
1、设计状态空间表示方式
空格定义为0,将数字按行存入列表中,用列表sample存储八数码问题的初始状态,例如[2,8,3,1,6,4,7,0,5];用goal存储八数码问题的目标状态,例如[1,2,3,8,0,4,7,6,5],八数码问题中数字的移动就看做是空格(0)的移动,在列表中直接完成,同时把操作记录存储在列表的末端,如第一步空格向上移动,则移动后的列表变为[2,8,3,1,0,4,7,6,5,‘up’]。
初始节点为[2,8,3,1,6,4,7,0,5],它是搜索树的根节点,而目标状态为[1,2,3,8,0,4,7,6,5],我们做的,就是找出一条或者多条从根节点通往[1,2,3,8,0,4,7,6,5]节点的路径。
2、设计一组算子
用来求解该问题的算子可以用4条规则来描述
①空格(0)向上移动(注意:要满足前提条件,即空格移动方向有数字且移动后的状态为初次生成状态,以下规则也一样)
②空格(0)向下移动
③空格(0)向左移动
④空格(0)向右移动
这四条规则,事实上就是搜索树上从当前节点生成下一个节点的方法。
3、移动方法以及可移动的条件
向上移动和向下移动:假设初始状态 中,0(空格)所在的位置为中心,存储为[2,8,3,1,0,4,7,6,5]。0所在位置的下标为4,若0要向上移动或向下移动,则实质为与8或6交换位置,则交换操作为:
temp[flag], temp[flag - 3] = temp[flag - 3], temp[flag](向上移动)
temp[flag], temp[flag + 3] = temp[flag + 3], temp[flag](向下移动)
若0所在的位置已经在边界,即无法向上或向下移动,则判断条件为:
flag - 3 >= 0(向上)
flag + 3 <= 8(向下)
向左移动和向右移动:假设初始状态如上面相同,0所在的位置为中心,存储为[2,8,3,1,0,4,7,6,5],若0要向左向右移动,则实质为与1或4交换位置,交换操作为:
temp[flag], temp[flag - 1] = temp[flag - 1], temp[flag](向左移动)
temp[flag], temp[flag + 1] = temp[flag + 1], temp[flag](向右移动)
若0已经在边界,即无法向左或向右移动,则判断条件为:
flag % 3 != 0(向左)
(flag + 1) % 3 != 0(向右)
4、判断有无解实现方法
通过双重循环,将列表逆序循环,逐一比较,计算逆序对的个数,遇到0则跳过。最后判断奇偶,偶数有解,奇数无解。
5、宽度优先搜索实现方法
①把起始节点放到OPEN表中,如果该起始节点为一目标节点,则求得一个解答。
②若OPEN是空表,则没有解,失败退出;否则继续。
③把第一个节点(节点死)从OPEN表移出,并把它放入CLOSED的扩展节点表中。
④扩展n。如果没有后继节点,则转向步骤②。
⑤把n的所有后继节点放到OPEN表的末端,并提供从这些后继节点回到n的指针。
⑥如果n的任一个后继节点是个目标节点,则找到一个解答,成功退出;否则转向步骤②;
完整代码:
import time
class QNode(object):#定义队列的数据结构
def __init__(self, elem, next=None):
self.elem = elem
self.next = next
class Queue(object): # 队列
def __init__(self):
self.head = None
self.rear = None
def is_empty(self):#判断队列是否为空
return self.head is None
def enqueue(self, elem): # 往队列中添加一个elem元素
p = QNode(elem)
if self.is_empty():
self.head = p
self.rear = p
else:
self.rear.next = p
self.rear = p
def dequeue(self): # 从队列的头部删除一个元素
result = self.head.elem
self.head = self.head.next
return result
def Search(self,elem):#搜索队列中是否有与elem相同的元素
temp = self.head
while temp is not None:
if elem[:9] == temp.elem[:9]:
return False
temp = temp.next
return True
def jugde(open,closed,temp,i,creatpoint):#判断是否重复
if open.Search(temp) and closed.Search(temp):
temp.append(operato[i])
Open.enqueue(temp)
creatpoint += 1
return creatpoint
def Jugde(sample):#判断有无解
flag = 0
for i in sample[::-1]:
if i != '0':
this = sample.index(i)
for x in sample[:this]:
if x > i:
flag += 1
if flag % 2 ==0 :
return 0
return 1
if __name__ == '__main__':
sample = list(input('请输入初始状态:').split())#存储8数码问题的初始状态
goal = list(input('请输入目标状态:').split(' '))#存储8数码问题的目标状态
m = Jugde(sample)
n = Jugde(goal)
if m != n:#通过求逆序数判断有无解
print('无解!')
exit()
operato = ['up','down','left','right']
k = creatpoint = 0 # k 为搜索的节点数 creatpoint 为生成节点数
Open = Queue()#创建open表
Closed = Queue()#创建closed表
Open.enqueue(sample)
Max = input("请输入最大搜索深度:")
start = time.time()
while(1):
if(sample == goal ):
print("起始节点为目标结点!")
break
if Open.is_empty():
print("没有解!")
break
else:
p = Open.dequeue()#从Open表中取出
if len(p)-9 >= int(Max)+1:#判断是否超过设置的最大深度
print('已达最大深度,未找到解!')
exit()
Closed.enqueue(p)#放入Closed表中
if p[:9] == goal:#判断是否相等
k += 1
print('当前层次:{},已搜索节点数:{},已生成结点数{}\n查找成功!'.format(len(p)-9,k,creatpoint))
print("空格的移动路径依次为:",end = '')
for i in p[9:]:
print(i,end='->')
end = time.time()
print('完成\nRunning time:{} Seconds'.format(end - start))
exit()
k += 1
flag = p.index('0')# 返回列表中0的索引 flag = p.index('0')
if flag - 3 >= 0 :#空格向上移动
temp = p.copy()
temp[flag], temp[flag - 3] = temp[flag - 3], temp[flag]
creatpoint = jugde(Open,Closed,temp,0,creatpoint)
if flag + 3 <= 8:#空格向下移动
temp = p.copy()
temp[flag], temp[flag + 3] = temp[flag + 3], temp[flag]
creatpoint = jugde(Open, Closed, temp, 1, creatpoint)
if flag % 3 != 0 :#空格向左移动
temp = p.copy()
temp[flag], temp[flag - 1] = temp[flag - 1], temp[flag]
creatpoint = jugde(Open, Closed, temp, 2, creatpoint)
if (flag + 1) % 3 != 0:#空格向右移动
temp = p.copy()
temp[flag], temp[flag + 1] = temp[flag + 1], temp[flag]
creatpoint = jugde(Open, Closed, temp, 3, creatpoint)
运行结果
参数设置方案:
初始状态:2 8 3 1 6 4 7 0 5
目标状态:1 2 3 8 0 4 7 6 5
最大搜索深度:10
结果讨论:
对于一些简单的八数码问题,宽度优先算法可以比较快得找到目标,但是对于一些复杂的步数较多的问题,宽度优先搜索的的效率很低
比如当初始状态为:[2,4 8,6,0,3,1,7,5],目标状态为:[1,2,3,8,0,4,7,6,5]的八数码问题,当最大深度设置为10时,宽度优先搜索无法找到目标状态
当设置最大深度为15时,找到了目标状态。