赞
踩
在一个国际象棋棋盘上,一个棋子“马”(骑士)按照“马走日”的规则,从一个格子出发,要走遍所有棋盘格恰好一次。把一个这样的走棋序列称为一次“周游”
采用图搜索算法是解决骑士周游问题最容易理解和编程的方案之一。解决方案分为两步:
(1)将合法走棋次序表示为一个图
(2)采用图搜索算法搜寻一个长度为(行*列-1)的路径,路径上包含每个顶点恰一次
将棋盘和走棋步骤构建为图的思路
按照“马走日”规则的走棋步骤作为连接边,建立每一个棋盘格的所有合法走棋步骤能够到达的棋盘格关系图
如上图所示,骑士位于12格,他可以走的有21 ,23 ,19 ,9 ,3 ,1 ,5 ,15
合格的走棋位置函数
#合法走棋位置函数
def getLegalMoves(x,y,bdSize):#x,y为骑士位置,bdSize为棋盘大小
newMoves=[]
moveOffsets=[(-1,-2),(-1,2),(-2,-1),(-2,1),(1,-2),(1,2),(2,-1),(2,1)]#马走日8个格子
for i in moveOffsets:
newX=x+i[0]
newY=y+i[1]
if legalCoord(newX,bdSize) and legalCoord(newY,bdSize):#确认不会走出棋盘
newMoves.append((newX,newY))
return newMoves
def legalCoord(x,bdSize):
if x>=0 and x<bdSize:
return True
else:
return False
构建走棋关系图
#构建走棋关系图
def knightGraph(bdSize):#bdSize为棋盘大小
ktGraph=Graph()
for row in range(bdSize):
for col in range(bdSize):#遍历每个格子
#单步合法走棋
nodeId=posToNodeId(row,col,bdSize)
newPositions=getLegalMoves(row,col,bdSize)
for e in newPositions:
nid=posToNodeId(e[0],e[1],bdSize)
ktGraph.addEdge(nodeId,nid)#添加边及顶点
def posToNodeId(row,col,bdSize):
return row*bdSize+col
用于解决骑士周有问题的图搜索算法是深度优先搜索(Depth First Search,DFS)。深度优先搜索是沿着树的单支尽量深入向下搜索,如果到无法继续的程度还未找到问题解,就回溯上一层再搜索下一支。
深度优先搜索解决骑士周游的关键思路
如果沿着单支深入搜索到无法继续(所有合法移动都已经被走过了)且路径长度还没有达到预定值(8*8棋盘为63),那么久清楚颜色标记,返回上一层分支继续深入搜索。还需要引入一个栈来记录路径,并实施返回上一层的回溯操作。
def knightTour(n,path,u,limit):#n:层次,path:路径(栈)u:当前顶点 limit:搜索总深度 u.setColor('gray')#当前顶点设置为灰色,表示正在探索 path.append(u)#当前顶点加入路径 if n<limit: nbrList=list(u.getConnections())#对所有合法移动逐一深入 i=0 done=False while i<len(nbrList) and not done: if nbrList[i].getColor()=='white':#选择白色未经过的顶点深入 done=knightTour(n+1,path,nbrList[i],limit)#层次加1,递归深入 i=i+1 if not done:#都无法完成总深度,回溯,试本层下一个顶点 path.pop() u.setColor('white') else: done=True return done
下图为深度优先搜索的一个简单举例。如Figure3所示,从A点出发,首先移动到B点(Figure4),然后移动到C点(Figure5),所有合法点都走过,但仍未达到预定值,所以将C点恢复为白色,退回B点(Figure6),然后移动到D点(Figure7),E点(Figure8),F点(Figure9),C点(Figure10),搜索结束。
上述算法的性能高度依赖于棋盘的大小。对于55的棋盘,约1.5秒就可以得到一个周游路径,但对于88的棋盘,则要半个小时以上才能得到一个解。目前实现算法的复杂度为O(kn),其中n是指棋盘格数目。
算法改进——Warnsdorff算法
初始算法中nbrList直接以原始顺序来确定深度优先搜索的分支次序。新的算法仅修改了遍历下一格的次序:将u的合法移动目标棋盘格排序为具有最少合法移动目标的格子优先搜索。
def orderByAvail(n):
resList=[]
for v in n.getConnections():
if v.getColor()=='white':
c=0
for w in v.getConnections():
if w.getColor()=='white':
c=c+1
resList.append((c,v))
resList.sort(key=lambda x:x[0])
return [y[1] for y in resList]
采用先验知识来改进算法性能的做法称为“启发式规则(heuristic)”
启发式规则经常用于人工智能领域,可以有效地减小搜索范围,更快达到目标等等。如棋类程序算法会余弦存入棋谱、布阵口诀、高手习惯等“启发式规则”,能够在最短时间内从海量的棋局落子点搜索树中定位最佳落子。
骑士周游问题是一种特殊的对图进行深度优先搜索。其目的是建立一个没有分支的最深的深度优先树,表现为一条线性的包含所有节点的退化树
一般的深度优先搜索目标是在图上进行尽量深的搜索,连接尽量多的顶点,必要时可进行分支(创建树)。有时候深度优先搜索会创建多棵树,称为“深度优先森林”。
深度优先搜索同样要用到顶点的“前驱”属性来构建树或森林。另外要设置“发现时间”和“结束时间”属性,前者是在第几步访问到这个顶点(设置灰色),后者是在第几步完成了此顶点探索(设置黑色)
带有DFS算法的图实现为Graph的子类:
顶点Vertex增加了成员Discovery(发现时间)及Finish(结束时间)
图Graph增加了成员time用于记录算法执行的步骤数目。
from pythonds.graphs import Graph class DFSGraph(Graph): def __init__(self): super().__init__() self.time=0 def dfs(self): #颜色初始化 for aVertex in self: aVertex.setColor('color') aVertex.setPred(-1) for aVertex in self: if aVertex.getColor()=='white':#如果还有未包括的顶点则建森林 self.dfsvisit(aVertex) def dfsvisit(self,startVertex): startVertex.setColor('gray') self.time+=1#算法的步数 startVertex.setDiscovery(self.time) for nextVertex in startVertex.getConnections(): if nextVertex.getColor()=='white': nextVertex.setPred(startVertex) self.dfsvisit(nextVertex)#深度优先递归访问 startVertex.setColor('black') self.time+=1 startVertex.setFinish(self.time)
下图为通用深度优先搜索的一个简单示例。从顶点A开始(Figure14),A的发现时间为1,然后到达B顶点,B的发现时间为2,之后到达C顶点,C的发现时间为3,由于C顶点之后无法继续探索,C顶点设为黑色,结束时间为4,然后退回B顶点,移动到D顶点,D的发现时间为5,之后到E点,发现时间为6,到F点,发现时间为7,由于F点之后无法继续探索,所以F点设为黑色,结束时间为8,退回E点,E点的结束时间为9,退回D点,D点的结束时间为10,然后退回B点,结束时间为11,最后退回A点,结束时间为12.
DFS构建的树,其顶点的“发现时间”和“结束时间”属性具有类似括号的性质。即一个顶点的“发现时间”总小于所有子顶点的“发现时间”,而“结束时间”则大于所有子顶点“结束时间”,比子顶点更早被发现,更晚结束探索
DFS运行时间同样包括了两方面:
dfs函数中油两个循环,每个都是|V|次,所以是O(|V|),而dfsvisit函数中的循环则是对当前顶点所连接的顶点进行,而且仅有在顶点为白色的情况下才进行递归调用,所以对每条边来说只会运行一步,所以是O(|E|)
DFS总的时间复杂度和BFS一样,都是O(|V|+|E|)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。