概念介绍
本博客在学习北京大学陈斌老师《数据结构与算法》MOOC课程中总结反思形成。
骑士周游问题:
在 8 × 8 8 \times 8 8×8的国际象棋棋盘上按照"马"走日的规则,从一个格子出发,要走遍所有棋盘格所有恰好一次(周游)。
骑士周游问题的解决步骤:
-
构建骑士周游图;
-
采用“深度优先搜索 DFS”,来搜寻有效的骑士周游路径;
具体来讲,首先将合法走棋次序表示为一个图;然后采用图搜索算法搜寻一个长度为(行×列-1)的路径,路径上包含每个顶点恰一次
深度优先搜索算法:
深度优先搜索是沿着树的单支尽量深入向下搜索。
如果到无法继续的程度还未找到问题解就回溯上一层再搜索下一支
代码解析
建立骑士周游图
建立骑士周游图步骤:
-
将棋盘格作为顶点;
-
按照“马走日”规则的走棋步骤作为连接边;
-
建立每一个棋盘格的所有合法走棋步骤能够到达的棋盘格关系图。
from pythonds.graphs.adjGraph import Graph
def genLegalMoves(x, y, bdsize):
newMoves = []
# 马走日8个格子
movesOffsets = [(-1, -2), (-1, 2), (-2, -1), (-2, 1),
(1, -2), (1, 2), (2, -1), (2, 1)]
for i in movesOffsets:
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, bdszie):
if x >= 0 and x < bdszie:
return True
else:
return False
def posToNodeId(row, col, bdsize):
return row * bdsize + col
# 构建走棋关系图
def knightGraph(bdsize):
ktGraph = Graph()
# 遍历每个格子
for row in range(bdsize):
for col in range(bdsize):
nodeId = posToNodeId(row, col, bdsize)
newPositions = genLegalMoves(row, col, bdsize)
for e in newPositions:
nid = posToNodeId(e[0], e[1], bdsize)
ktGraph.addEdge(nodeId, nid)
return ktGraph
针对骑士周游问题的DFS
目前实现的算法,其复杂度为 O ( k n ) O(k^n) O(kn)
# DFS 算法
# n,层次;path,路径;u,当前顶点;limit,搜索总深度
def knightTour(n, path, u, limit):
u.setColor('gray')
# 当前顶点加入路径
path.append(u)
if n < limit:
# 对所有合法移动逐一深入
nbrList = list(u.getConnections())
# nbrList = orderByAvail(u)
i = 0
done = False
while i < len(nbrList) and not done:
# 选择白色未经过的顶点深入
if nbrList[i].getColor() == 'white':
# 层次+1 ,递归深入
done = knightTour(n + 1, path, nbrList[i], limit)
i = i + 1
# 都无法完成总深度,回溯,尝试本层下一个顶点
if not done:
path.pop()
u.setColor('white')
else:
done = True
return done
DFS改进:Warnsdorff算法
指数时间复杂度算法可以在实际性能上加以大幅度改进:
- 对nbrList的灵巧构造,以特定方式排列顶点访问次序
- 采用先验的知识来改进算法性能的做法,称作为“启发式规则heuristic”,可以有效地减小搜索范围、更快达到目标等等
# 骑士周游算法改进
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]
通用的深度优先搜索
-
通用的深度优先搜索目标是在图上进行尽量深的搜索,连接尽量多的顶点,必要时可以进行分支(创建树)
-
另外要设置“发现时间”和“结束时间”属性;体现为(self.dtime和self.ftime,可以在pythonds.graphs.adjGraph模块查看)
• 前者是在第几步访问到这个顶点(设置灰色)
• 后者是在第几步完成此顶点探索(设置黑色)
# 通用的深度优先搜索算法
# BFS采用队列存储待访问顶点
# DFS则是通过递归调用,隐式使用了栈
class DFSGraph(Graph):
def __init__(self):
# 细节:supre()
super().__init__()
self.time = 0
def dfs(self):
for aVertex in self:
aVertex.setColor('white')
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)
调用方式
if __name__ == '__main__':
# 棋盘格行列数
NN = 6
kgtgraph = knightGraph(NN)
resPath = []
start = kgtgraph.getVertex(4)
knightTour(n=0, path=resPath, u=start, limit=NN * NN - 1)
for i in range(len(resPath)): # 输出路径
if i != len(resPath) - 1:
print(resPath[i].id, end=' ->')
else:
print(resPath[i].id)
DFS算法分析
[DFS可视化代码参考][https://blog.csdn.net/weixin_42353399/article/details/103380708]
-
dfs函数中有两个循环,每个都是|V|次,所以是$O(|V|) $;
-
dfsvisit函数中的循环则是对当前顶点所连接的顶点进行,而且仅有在顶点为白色的情况下才进行递归调用,所以对每条边来说只会运行一步,所以是 O ( ∣ E ∣ ) ; O(|E|); O(∣E∣);
-
DFS算法复杂度加起来就是和BFS一样的 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。