图——深度优先搜索(骑士周游问题)

需要用到上一节中定义的Graph类,直接复制:

## 将代码块运行结果全部输出,而不是只输出最后的,适用于全文
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"  

class Vertex:
    def __init__(self, key):
        self.id = key                  # 用id保存key
        self.connectedTo = {}     # 字典保存,非常类似昨天10.14写的ID3里对于多叉数的节点定义
        self.distance = None
        self.pred = None
        self.color = "white"
        
    def addNeighbor(self, nbr, weight = 0):    # nbr——链接的对象的key
        self.connectedTo[nbr]=weight            # 加入connection里,并赋边的权重
    
    def __str__(self):
        return str(self.id) +' connectedTo: '  +str([x.id for x in self.connectedTo])

    def getId(self):                      # 获得id
        return self.id
    
    def getConnections(self):       # 该顶点链接的所有顶点的key
        return self.connectedTo.keys()
        
    def getWeight(self,nbr):              # 查看两个顶点的边权重
        return self.connectedTo[nbr]   # 字典按照key索引
    
    ##### 增加部分 ######
    def setDistance(self, x):   # 设置距离
        self.distance = x
        
    def setPred(self, x):     # 设置前驱顶点
        self.pred = x
        
    def setColor(self, x):    # 设置颜色标识
        self.color = x

        
class Graph:                                  # 维护一个masterlist
    def __init__(self):
        self.vertList = {}                       # 存储所有顶点,是一个字典key:vertex
        self.numVertices = 0               # 顶点数量
    
    def addVertex(self,key):              # 添加顶点
        newVertex = Vertex(key)         # 变成顶点形式
        self.vertList[key] = newVertex   # 加入masterlist字典
        self.numVertices +=1 
        #print(newVertex)
    
    def getVertex(self,item):            # 按照key从master里查找一个顶点
        if item in self.vertList:
            return self.vertList[item]
        else:
            return None
    
    def __contain__(self,item):          # 是否包含某个key
        return item in self.vertList
    
    def addEdge(self, f, t, cost=0):     # 添加边 f 到 t(key),cost是边的权重
        # 如果给出的f和t在masterlist里面找不到
        if f not in self.vertList:
            nv = self.addVertex(f)
        if t not in self.vertList:
            nv = self.addVertex(t)
       
        self.vertList[f].addNeighbor(self.vertList[t], cost)     # 调用vertex类里定义联系的方法
        #print(self.vertList[f])
    
    def getVertices(self):    # 输出所有顶点key
        return self.vertList.keys()
    
    def __iter__(self):
        return iter(self.vertList.values())    # 可迭代   所有vertex

图——骑士周游问题

马从一个格子开始,按照“马走日”的规则,把棋盘中所有格子都走一遍(不重复)

“马走日”规则:

  • 每步棋先横走或直走一格,然后再往外斜走一格;
  • 或者先斜走一格,最后再往外横走或竖走一格

问题处理——构建图

思路:

  • 把棋盘格作为图的顶点
  • 把“马走日”这个规则的走棋步骤作为连接边
  • 每一次最多有8个格子可以走

生成合法走棋位置:

genLegalMoves

  • 输入:当前位置
  • 输出:当前位置上骑士所有合法移动的列表
def genLegalMoves(x,y,bdSize):
    newMoves = []
    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 x >= 0 and x < bdSize:
        return True
    else:
        return False

genLegalMoves(0, 0, 8)
genLegalMoves(3, 3, 8)
[(1, 2), (2, 1)]

[(2, 1), (2, 5), (1, 2), (1, 4), (4, 1), (4, 5), (5, 2), (5, 4)]

走棋关系图

# 给顶点的id,没有重复
def NodeID(row, col, bdsize):
    return row*bdsize + col

def revNodeID(nodeid, bdsize):   # 根据node id 计算棋盘上的位置
    x = nodeid//bdsize
    y = nodeid%bdsize
    return (x,y)
    
# 生成图
def knightGraph(bdsize=8):
    ktGraph = Graph()
    for row in range(bdsize):  
        for col in range(bdsize):   # 遍历每个格子
            nodeid = NodeID(row, col, bdsize)   # 生成id
            new_xy = genLegalMoves(row, col, bdsize)      # 计算合法移动位置
            for position in new_xy:   # 对每个合法移动的位置
                nid = NodeID(position[0], position[1], bdsize)  # 生成id
                ktGraph.addEdge(nodeid, nid)    # add函数建边,没有顶点就先建立顶点再建边
    return ktGraph
knightG = knightGraph(8)
length = 0
for key in knightG.getVertices():
    length += len(knightG.getVertex(key).connectedTo)
print("一共%d个顶点,%d条边\n"% (knightG.numVertices, length))

# 与(0,0) (3,3)两个顶点有边相连的顶点
print(knightG.getVertex(NodeID(0, 0 ,8)))
print(knightG.getVertex(NodeID(3, 3, 8)))
[revNodeID(x.id, 8) for x in knightG.getVertex(NodeID(0, 0 ,8)).connectedTo] # 2个(2个可走位置)
[revNodeID(x.id, 8) for x in knightG.getVertex(NodeID(3, 3 ,8)).connectedTo]# 8个(8个可走位置)
一共64个顶点,336条边

0 connectedTo: [10, 17]
27 connectedTo: [17, 21, 10, 12, 33, 37, 42, 44]


[(1, 2), (2, 1)]


[(2, 1), (2, 5), (1, 2), (1, 4), (4, 1), (4, 5), (5, 2), (5, 4)]

深度优先搜索 DFS

Depth First Search

  • 沿着树的单支尽量深入,
  • 直到无法继续还没有找到解时
  • 回溯到上一层,搜索下一支

实现的算法:

  1. 第一种,每个顶点仅访问一次
  2. 第二种,允许顶点被重复访问

用于解决骑士周游问题

  1. 思路:

    • 假定每个顶点只能被访问一次

    • 沿着单支深入,直到:还没有走到63步(还没完成周游时),已经出现“当前位置的所有合法移动位置都被走过了”

    • 那就清除当前位置标记,返回上一层换一个分支搜索

  2. 根据这个思路可以发现,访问路径可以用栈来处理:

    • 引入一个栈记录路径
  3. 整个算法用递归完成:
    访问一个节点——标记、对所有合法位置递归、如果都失败则清楚当前节点标记,并返回上一步

def knightTour(position, path, depth, limit):
    position.setColor('grey')   # 走过了就记作灰色,避免重复
    path.append(position)      # 纳入路径中

    if depth==limit:   # 结束条件
        available = True
    else:
        available = False
        new_xy = list(position.getConnections())  # 下一步走棋位置
        i = 0
        # 对于所有合法且没有走过的解法,递归调用
        while i < len(new_xy) and not available:
            if new_xy[i].color =="white":  # 如果没走过
                available,path = knightTour(new_xy[i], path, depth+1, limit)
            i += 1
        if not available:   # 循环结束,如果没有找到
            path.pop()      # 返回上一步
            position.setColor("white")       # 当做没来过
    return available, path


knightG = knightGraph(5)
available, path = knightTour(knightG.getVertex(NodeID(2, 2 ,5)), [], 0, 24)
path = [str(revNodeID(x.id, 5)) for x in path]
print("5x5的棋盘上骑士周游的一条路径:\n"+ ' -> '.join(path))
5x5的棋盘上骑士周游的一条路径:
(2, 2) -> (1, 0) -> (0, 2) -> (1, 4) -> (3, 3) -> (4, 1) -> (2, 0) -> (0, 1) -> (1, 3) -> (3, 4) -> (4, 2) -> (3, 0) -> (1, 1) -> (0, 3) -> (2, 4) -> (4, 3) -> (3, 1) -> (2, 3) -> (0, 4) -> (1, 2) -> (0, 0) -> (2, 1) -> (4, 0) -> (3, 2) -> (4, 4)

改进算法

骑士周游问题的改进算法——在进行对于所有合法移动位置new_xy,按照新位置的合法移动位置数目来决定搜索顺序。

  • 下一步选择有最少可能移动位置的顶点!!
### 1.首先定义函数——把下一步的合法移动位置按照其包含的合法移动位置排序:
def orderByAvail(n):
    nextlist = []
    for i in n.getConnections():
        if i.color =="white":
            count = 0
            for j in i.getConnections():   # 计算新的顶点有多少可能的移动位置
                if j.color=="white":
                    count+=1
            nextlist.append((count,i))
    # 按照可能移动位置排序,由小到大
    nextlist.sort(key = lambda x: x[0])
    # 输出排序后的节点列表
    return [x[1] for x in nextlist]

# 测试
knightG = knightGraph(8)
orderByAvail( knightG.getVertex(NodeID(4, 4 ,8)))


### 2.修改原本的算法
def knightTour(position, path, depth, limit):
    position.setColor('grey')   # 走过了就记作灰色,避免重复
    path.append(position)      # 纳入路径中

    if depth==limit:   # 结束条件
        available = True
    else:
        available = False
        new_xy = orderByAvail(position)   # 【修改位置】
        i = 0
        # 对于所有合法且没有走过的解法,递归调用
        while i < len(new_xy) and not available:
            if new_xy[i].color =="white":  # 如果没走过
                available,path = knightTour(new_xy[i], path, depth+1, limit)
            i += 1
        if not available:   # 循环结束,如果没有找到
            path.pop()      # 返回上一步
            position.setColor("white")       # 当做没来过
    return available, path
[<__main__.Vertex at 0x10a0f9198>,
 <__main__.Vertex at 0x10a0f9518>,
 <__main__.Vertex at 0x10a0f9630>,
 <__main__.Vertex at 0x10a0f96a0>,
 <__main__.Vertex at 0x10a0f90b8>,
 <__main__.Vertex at 0x10a0f7cf8>,
 <__main__.Vertex at 0x10a0f7e80>,
 <__main__.Vertex at 0x10a0f9438>]
knightG = knightGraph(8)
available, path = knightTour(knightG.getVertex(NodeID(2, 6 ,8)), [], 0, 63)
path = [str(revNodeID(x.id, 8)) for x in path]
print("8x8的棋盘上骑士周游的一条路径:\n"+ ' -> '.join(path))
8x8的棋盘上骑士周游的一条路径:
(2, 6) -> (0, 7) -> (1, 5) -> (0, 3) -> (1, 1) -> (3, 0) -> (5, 1) -> (7, 0) -> (6, 2) -> (5, 0) -> (7, 1) -> (6, 3) -> (7, 5) -> (6, 7) -> (4, 6) -> (2, 7) -> (0, 6) -> (1, 4) -> (0, 2) -> (1, 0) -> (2, 2) -> (0, 1) -> (2, 0) -> (4, 1) -> (6, 0) -> (7, 2) -> (6, 4) -> (7, 6) -> (5, 7) -> (3, 6) -> (1, 7) -> (0, 5) -> (1, 3) -> (3, 4) -> (5, 5) -> (4, 7) -> (6, 6) -> (7, 4) -> (5, 3) -> (4, 5) -> (3, 7) -> (2, 5) -> (0, 4) -> (1, 6) -> (2, 4) -> (3, 2) -> (4, 0) -> (6, 1) -> (4, 2) -> (2, 1) -> (0, 0) -> (1, 2) -> (3, 3) -> (5, 4) -> (7, 3) -> (6, 5) -> (7, 7) -> (5, 6) -> (4, 4) -> (5, 2) -> (3, 1) -> (2, 3) -> (3, 5) -> (4, 3)
  1. 小感受
    我的天这个效率提升也太快了,原因就是因为充分利用了先验知识,通常要棋盘边上走,选择有最多可能移动位置的节点作为下一个顶点,会导致问题骑士将倾向于在周游的早期访问棋盘中间的方格,因为不能重复走,最后没法完成周游

  2. list排序的新知识:

    • key = lambda 元素: 元素[字段索引]
nextlist.append((count,i))  
nextlist.sort(key = lambda x: x[0])  # 按照某一维排序
return [x[1] for x in nextlist]   # 只输出某一维
a = [(1,4,3),(6,2,4),(5,9,1)]   # 当然这里是圆括号还是方括号都可以
a.sort(key = lambda x :x[0]); [x[0] for x in a]
a.sort(key = lambda x :x[1]); [x[1] for x in a]
a.sort(key = lambda x :x[2]); [x[2] for x in a]
[1, 5, 6]






[2, 4, 9]






[1, 3, 4]