图——广度优先搜索BFS(词梯问题)

今天要开启新的一章学习啦,近期学习目标:

  • 图及算法,慢慢来
  • py实现逻辑回归(easy)、cart回归树(median)、SVM(hard)
  • 经典机器学习算法还差rf和boosting,学完之后可以了解下强化学习的概念
  • 做完这些就需要对数据结构进行复习啦,暂定为看b站视频&leetcode简单题目

图的概念

  1. 顶点Vertex
    具有名称标识Key,也可以携带数据项payload
  2. 边Edge/弧Arc
    表示两个顶点之间的关系,可以有方向也可以没有
  3. 权重Weight
    给边赋权重,可以理解为从一个顶点到另一个顶点的“代价”
  4. 图Graph 一个图G可以定义为G = (V,E),即点和边的集合
    • E中的每一条e = (v,w) v,w是两个顶点
    • 如果有权重,e中还要增加权重分量
  5. 路径Path 依次链接的顶点序列:
    • 有权重:路径的长度=边的数量
    • 无权重:路径的长度=边权重的和
  6. 圈Cycle 首尾相连/顶点相同的路径,有些有向图不存在任何圈(称为DAG)

实现ADT Graph

  • 添加:顶点、边/有权重的边addVertex、addEgde
  • 查找:按key查找顶点

实现形式:

  • 邻接矩阵

    1. 行列代表顶点,如果i和j有连接,矩阵ij处保存权重
    2. 问题在于效率,大多数时候都是系数的
  • 邻接表

    1. 一个masterlist,包括所有顶点
    2. 每个顶点都关联一个list,保存所有和自己有边连接的顶点
## 将代码块运行结果全部输出,而不是只输出最后的,适用于全文
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"    

#### 1.首先定义顶点类(不带payload的、有方向的图)
class Vertex:
    def __init__(self, key):
        self.id = key                  # 用id保存key
        self.connectedTo = {}     # 字典保存,非常类似昨天10.14写的ID3里对于多叉数的节点定义!
        
    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索引

# 测试
v1 = Vertex(1);v2 = Vertex(2);v3 = Vertex(3)
v1.addNeighbor(1, 12);v1.addNeighbor(2, 13);v2.addNeighbor(3, 23)
v1.getConnections()
v2.getWeight(3)
1
2





23
##### 2.接着实现Graph类(邻接表的形式)

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
        
# 测试
g = Graph()
# 添加顶点
for i in range(6): g.addVertex(i)
# 查看所有顶点
g.getVertices()
# 添加新边
g.addEdge(1, 2, cost=12)
g.addEdge(1, 3, cost=13)
g.addEdge(1, 7,  cost=17)
# 顶点数目
g.numVertices
# 查看某个顶点
print(g.getVertex(1))
dict_keys([0, 1, 2, 3, 4, 5])






7



1 connectedTo: [2, 3, 7]

图的应用

单词游戏——词梯问题

  • 相邻两个单词之间的差异只能是一个字母

  • 我们的目标是找到最短的单词变换序列

  • 如何用图来解释——把可能的单词之间的演变关系表达为图

    • 把所有固定长度的单词作为顶点key
    • 对每个顶点,如果单词之间之只差一个字母,就设一条边(比较次数多)
    • 无向图、无权重

    避免逐个比较建边的改进方法

    • ,每个桶可以存放若干单词——只有一个字母不一样
    • 桶的key是用_占替代一个字母
    • 然后在同一个桶的相互之间建边
  • 广度优先搜索BFS:搜索从开始单词到结束单词之间的所有有效路径

  • 选出最短路径

建立单词关系图

### 前置处理——采用字典建立“桶”
import os             # 加载标准库
os.chdir('/Users/mac/Desktop/快乐研一/python/data')   # 相当于setwd()

def buildGraph(wordFile):
    d = {}
    g = Graph()
    wfile = open(wordFile,'r')
    
    # 构造桶
    for line in wfile:              # 对于每一个单词
        word = line[:-1]
        for i in range(len(word)):   # 有a个字母的单词会属于a个桶!!
            bucket = word[:i] + "_" + word[i+1:]    # 对于单词的每一个字母
            if bucket in d:         # 如果已经有了这个桶,直接加入,没有的话新建桶用bucket当做key
                d[bucket].append(word)
            else:  
                d[bucket] = [word]
            
    # 桶内建立边
    for bucket in d.keys():          # 对每一个桶
        for word1 in d[bucket]:     
            for word2 in d[bucket]:
                if word1!=word2:    # 对于桶内的每对单词,建边
                    g.addEdge(word1, word2)  # 默认权重0,相当于无权重
    
    return g   # 就是各个单词的图

wordslink = buildGraph('fourletterwords.txt')  
print(wordslink.getVertex('SAIL'))
#wordslink.getVertices()
length = 0
for key in wordslink.getVertices():
    length += len(wordslink.getVertex(key).connectedTo)
print("\n 单词数量={},邻接表的边数目={},如果用邻接矩阵单元数目将会是={} ".format(wordslink.numVertices ,length, wordslink.numVertices *wordslink.numVertices))
SAIL connectedTo: ['BAIL', 'FAIL', 'HAIL', 'JAIL', 'KAIL', 'MAIL', 'NAIL', 'PAIL', 'RAIL', 'TAIL', 'VAIL', 'WAIL', 'SAID', 'SAIN', 'SOIL', 'SALL', 'SAUL']

 单词数量=3838,邻接表的边数目=42004,如果用邻接矩阵单元数目将会是=14730244 

广度优先搜索BFS

初步理解

  • 给定一个图G和搜索的起始顶点S,搜索所有从s可到顶点的边
  • 可以想象为s作为根节点,构建一颗多叉树的过程
  • **“广度优先”**意味着在增加搜索距离到k+1/加深层次之前,把所有兄弟节点/距离为k的顶点都找到
  • 产生的问题:有一些节点到S有不同路径,导致距离不同——如何解决?

跟踪顶点加入过程——给顶点增加属性

  • 距离distance:起始S到当前顶点的路径
  • 前驱顶点 predecessor:反向追溯
  • 标识color:白色:尚未发现;灰色:已经发现;完成探索:黑色
  • 队列:对已经发现的顶点进行排序
## 首先扩展一下vertex类,加入三个属性
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

算法过程

  1. 第一步、从起始顶点S开始(颜色灰色,距离0,前驱none,加入队列)

  2. 第二步、迭代:

    • 从队首pop出一个顶点,作为current顶点
    • 遍历current顶点的connected顶点,如果是白色,则:改为灰色、距离+1、前驱设为current顶点、加入队列)
    • 遍历结束后,把current顶点设为黑色
  3. 最终的结果:所有的顶点都有颜色黑色、距离X、前驱X,可以根据前驱一步步找回起始顶点S——一条路径

def bfs(g,start):   # 按照上述写成函数
    # 第一步
    queue = []
    start.setDistance(0)
    start.setColor("grey")
    queue.append(start)
    
    # 第二步
    while len(queue)>0:
        current = queue.pop(0)
        for node in current.connectedTo:
            if node.color == "white":
                node.setColor("grey")
                node.setDistance(current.distance+1)
                node.setPred(current)
                queue.append(node)
        current.setColor("black")

    # 第三步
def traverse(y):   # 根据pred来回溯
    x = y
    path = []
    while x.pred:  # 顶点有predecessor时
        path.append(x.id)
        x = x.pred   # 继续向上
    path.append(x.id)
    path.reverse()
    return path


# 测试
wordslink = buildGraph('fourletterwords.txt')  
bfs(wordslink,  wordslink.getVertex('FOOL'))  # 给出图和起始顶点
res = traverse(wordslink.getVertex('SAGE'))
print("词梯问题的一个解:\n"+ ' -> '.join(res))
词梯问题的一个解:
FOOL -> MOOL -> MOLL -> MALL -> SALL -> SALE -> SAGE