图–深度优先搜索
骑士周游问题:
-
在一个棋盘山,一个棋子"马",按照"马走日"的规则,从一个格子出发,要走遍所有棋盘格恰好一次,把一个这样的走棋序列称为一次"周游"
-
采用图搜索算法,是解决骑士问题最容易理解和编程的方案之一
-
解决方法:
-
首先将合法走棋次序表示为一个图
- 将棋盘格作为顶点
- 按照"马走日"规则的走棋步骤作为连接边
- 建立每一个棋盘格的所有合法走棋步骤能够到达的棋盘格关系图
-
采用图搜索算法搜寻一个长度为(行×列-1)的路径,路径上包含每个顶点恰一次
-
深度优先搜索解决骑士周游的关键思路
如果沿着单支深入搜索到无法继续(所有合法移动都已经被走过了)时,路径长度还没达到预定值(8×8棋盘为63), 那么就清除颜色标记,返回到上一层,换一个分支继续深入探索
-
引入一个栈来记录路径,方便实施返回上一层的回溯操作
-
-
用于解决骑士周游问题的图搜索算法是深度优先搜索
深度优先搜素就是沿着树的单支尽量深入向下搜索,如果无法继续的程度还未找到问题的解就回溯到上一层再搜索下一支
一个DFS算法用于解决骑士周游问题,其特点是每个顶点仅访问一次
另一个DFS算法更为通用,允许顶点被重复访问,可作为其他图算法的基础
def knightTour(n, path, u, limit):
"""
:param n: 层次
:param path: 路径
:param u: 当前顶点
:param limit: 搜索总深度
:return:
目前实现的算法,其复杂度为O(k^n),其中n是棋盘格数目
"""
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':
# 层次加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
上面代码重点:
一是while循环
二是递归调用
三是用灰色白色来保证只访问一次
完整代码如下:
# coding: utf-8
# from . import graph_ccc
from GraphCode.graph_ccc import *
def genLegalMoves(x, y, bdsize):
newMoves = []
# 马走日8个格子
moveOffsets = [(-1, -2), (-1, 2), (-2, -1), (-2, 1),
(1, -2), (1, 2), (2, -1), (2, 1)]
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 0 <= x < bdsize:
return True
else:
return False
# 构建走棋关系图
def knightGraph(bdsize):
ktGrapth = 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)
# 添加边和顶点
ktGrapth.addEdge(nodeId, nid)
return ktGrapth
def posToNodeId(row, col, bdsize):
"""
将坐标转化为id, row
row和col都是从0开始的
pos: (0,0)(0,1)(0,2)(0,3),(0,4)
id: 0 1 2 3 4
:param row:
:param col:
:param bdsize:
:return:
"""
return row * bdsize + col
def orderbyAvail(n):
resultList = []
for v in n.getConnections():
if v.getColor() == 'white':
c = 0
for w in v.getConnections():
if w.getColor() == 'white':
c += 1
resultList.append((c,v))
resultList.sort(key=lambda x:x[0])
return [y[1] for y in resultList]
def knightTour(n, path, u, limit):
"""
knightTour(0, [], 4, 63)
:param n: 层次, 是搜索树的当前深度
:param path: 路径, 是到目前为止访问到的顶点列表
:param u: 当前顶点, 是希望在图中访问的顶点
:param limit: 搜索总深度, 路径上的顶点总数
:return:
目前实现的算法,其复杂度为O(k^n),其中n是棋盘格数目
"""
u.setColor('gray')
# 当前顶点加入路径
path.append(u)
if n < limit:
# 对所有合法移动逐一深入
# nbrList = list(u.getConnections())
nbrList = list(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
if __name__ == '__main__':
g = knightGraph(8)
# for i in g:
# print(i)
path = []
startVertex = g.getVertex(4)
knightTour(0, path, startVertex, 63)
# print(path)
for node in path:
print(node.getId(), end=" ")
目前实现的算法,其复杂度是 O ( k n ) \mathbf{O}\left(\mathbf{k}^{n}\right) O(kn),其中n是棋盘格数目, 比如8*8的棋盘,n就是64, k是平均分支数,对于8*8的各自,平均分支数是5(就是平均每格有5种走法)
这是一个指数时间复杂度的算法,其搜索过程表现为一个层次为n的树
-
即便是指数时间复杂度算法也可以在实际性能上加以大幅度改进
- 对nbrList的灵巧构造,以特定方式排列顶点访问次序,可以使得8×8棋盘的周游路径搜索时间降低到秒级
-
这个改进算法叫做Warnsdorff算法
通用的深度优先搜索
-
一般的深度优先搜索目标是在图上进行尽量深的搜索,连接尽量多的顶点,必要时可以进行分支(创建了树), 有时候创建多棵树,称为"深度优先森林"
-
深度优先搜索同样要用到顶点的"前驱"属性,来构建树或森林
- 另外要设置"发现时间"和"结束时间"属性,前者是指在第几步访问到这个顶点(设置为灰色),后者是指在第几步完成了此顶点的探索(设置为黑色)
- 这两个新属性对后面的图算法很重要
-
带有DFS算法的图实现为Graph的子类
- 顶点vertex增加了成员Discovery以及Finish
- 图Graph增加了成员time用于记录算法执行的步骤数目
-
DFS构建的树,其顶点的"发现时间"和"结束时间"属性,具有类似括号的性质
- 即一个顶点的"发现时间"总小于所有子顶点的"发现时间"
- 而"结束时间"则大于所有子顶点"结束时间",比子顶点更早被发现,更晚被结束探索
-
DFS运行时间同样也包含两方面
- dfs函数中有两个循环,每个都是|V|次,所以是o(|V|)
- 而dfsvisit函数中的循环则是对当前顶点所连接的顶点进行,而且仅有在顶点为白色的情况下才进行递归调用,所以对每条边来说只会运行一步,所以是o(|E|)
- 加起来就是和BFS一样的o(|V|+|E|)
BFS采用队列存储待访问顶点
DFS则是通过递归调用,隐式使用了栈
# coding: utf-8
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('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)