1.图的表示
对于图G=(V, E),有两种标准表示方法表示。
一种表示法是将图作为邻接链表的组合,另一种表示法则将图作为邻接矩阵来看待。两种表示方法既可以表示无向图,也可以表示有向图。
邻接链表:一般表示稀疏图(边的条数远远小于点的个数的平方),不管是有向图还是无向图,邻接链表的存储空间需求均为O(V+E),对邻接链表稍加修改就可以用来表示权重图。
邻接矩阵:一般表示稠密图(边的条数接近点的个数的平方),如果需要快速判断任意两个结点之间是否有边相连,推荐使用邻接矩阵。对于邻接矩阵来说,一个图不管有多少条边,空间需求均为O(V的平方)。无向图的邻接矩阵是一个对称矩阵,也就是说无向图的邻接矩阵A就是自己的转置。
2.广度优先搜索(BFS)
宽度优先搜索算法(BFS,又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。
已知图G=(V,E)和一个源顶点s,宽度优先搜索以一种系统的方式探寻G的边,从而“发现”s所能到达的所有顶点,并计算s到所有这些顶点的距离(最少边数),该算法同时能生成一棵根为s且包括所有可达顶点的宽度优先树。对从s可达的任意顶点v,宽度优先树中从s到v的路径对应于图G中从s到v的最短路径,即包含最小边数的路径。该算法对有向图和无向图同样适用。
之所以称之为宽度优先算法,是因为算法自始至终一直通过已找到和末找到顶点之间的边界向外扩展,就是说,算法首先搜索和s距离为k的所有顶点,然后再去搜索和S距离为k+1的其他顶点。
BFS经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。
# 构建一个图的类
class Graph:
# 初始化函数
def __init__(self):
# 存储所有的结点
self.V = []
# 构建结点类
class Vertex:
# 初始化函数
def __init__(self, x):
# 将x定为结点的键
self.key = x
# 设定初始化的颜色为白色 也就是还未遍历到
self.color = 'white'
# 开始时将路径长度赋值为0
self.d = 10000
# 父节点设置为空
self.pi = None
# 子节点用一个列表存起来
self.adj = []
# 将函数全都放在Solution类里面
class Solution:
def BFS(self, G, s):
# 遍历图中的所有结点用来初始化
for u in G.V:
# s为待遍历的结点
if u != s:
# 初始化所有属性
u.color = 'white'
u.d = 10000
u.pi = None
# 从s开始遍历 s的颜色变为灰色
s.color = 'gray'
s.d = 0
s.pi = None
# 存储待遍历的结点
Q = []
Q.append(s)
# 进入循环 只要Q不为空
while Q != []:
# 弹出队列中第一个结点
u = Q.pop(0)
# 遍历当前结点的子节点
for v in u.adj:
# 如果还未遍历 颜色是白色
if v.color == 'white':
# 将颜色变为灰色
v.color = 'gray'
# 路径数加一
v.d = u.d + 1
# 确定v的父节点为u
v.pi = u
# 将v这一个未遍历的结点加入到队列中
Q.append(v)
# 遍历完了u的所有子节点之后 将其颜色变为黑色
u.color = 'black'
# 开始进行操作 写出主函数
if __name__ == '__main__':
# 实例化一个图
G = Graph()
# 构建图并且确定子节点
r = Vertex('r')
s = Vertex('s')
t = Vertex('t')
u = Vertex('u')
v = Vertex('v')
w = Vertex('w')
x = Vertex('x')
y = Vertex('y')
r.adj = [s, v]
s.adj = [r, w]
t.adj = [u, w, x]
u.adj = [t, x, y]
v.adj = [r]
w.adj = [s, t, x]
x.adj = [t, u, w, y]
y.adj = [u, x]
# 将所有结点放入G.V中
G.V = [r, s, t, u, v, w, x, y]
# 实例化Solution类 可以调用封装好的方法
m = Solution()
# 调用广度优先搜索方法
m.BFS(G, s)
# 搜索完成之后 看看每一个点记录下的结果
for v in G.V:
# 起始结点不打印
if v != s:
print(v.key, v.color, v.d, v.pi.key)
# v.d一定是表示的最短路径
过程图示
最短路径
对于一个图G=(V,E),宽度优先搜索算法能够得到从已知源结点s∈V到每一个可达结点的距离。我们定义最短路径长度δ(s,v)为从顶点s到顶点v的路径中具有最少边数的路径所包括的边数。若从s到v没有通路则为∞。
具有这一距离δ(s,v)的路径即为从s到v的最短路径(后文我们将把最短路径推广到赋权图,当中每边都有一个实型的权值,一条路径的权是组成该路径全部边的权值之和,眼下讨论的图都不是赋权图)。
在证明宽度优先搜索计算出的就是最短路径长度之前。我们先看一下最短路径长度的一个重要性质。
引理1
设G=(V,E)是一个有向图或无向图。s∈V为G的随意一个结点,则对随意边(u,v)∈E,δ(s,v)≤δ(s,u)+1。
引理2
设G=(V,E)是一个有向或无向图,并如果算法BFS从G中一已知源结点s∈V開始运行。在运行终止时,对每一个顶点v∈V。变量d[v]的值满足:d[v]≥δ(s,v)。
引理3
如果过程BFS在图G=(V,E)之上的运行过程中。队列Q包括例如以下结点<v1,v2,…,vr>,当中v1是队列Q的头,vr是队列的尾,则d[vi]≤d[v1]+1且d[vi]≤d[vi+1], i=1,2,…,r-1。
定理1 宽度优先搜索的正确性
设G=(V,E)是一个有向图或无向图,并如果过程BFS从G上某顶点s∈V開始运行,则在运行过程中。BFS能够发现源结点s可达的每个结点v∈V。在运行终止时,对随意v∈V,d[v]=δ(s,v)。此外,对随意从s可达的节点v≠s,从s到v的最短路径之中的一个是从s到π[v]的最短路径再加上边(π[v],v)。
宽度优先树
过程BFS在搜索图的同一时候建立了一棵宽度优先树。如图1所看到的,这棵树是由每一个结点的π域所表示。我们正式定义先辈子图例如以下,对于图G=(V,E),源顶点为s。其先辈子图Gπ=(Vπ,Eπ)满足:
Vπ={v∈V:π[v]≠NIL}∪{s}
且
Eπ={(π[v],v)∈E:v∈Vπ-{s}}
假设Vπ由从s可达的顶点构成,那么先辈子图Gπ是一棵宽度优先树,而且对于全部v∈Vπ,Gπ中唯一的由s到v的简单路径也相同是G中从s到v的一条最短路径。
因为它互相连通,且|Eπ|=|Vπ|-1(由树的性质),所以宽度优先树其实就是一棵树,Eπ中的边称为树枝。
当BFS从图G的源结点s開始运行后,以下的引理说明先辈子图是一棵宽度优先树。
引理4
当过程BFS应用于某一有向或无向图G=(V,E)时,该过程同一时候建立的π域满足条件:其先辈子图Gπ=(Vπ,Eπ)是一棵宽度优先树。
3.深度优先搜索
深度优先搜索就好比走迷宫, 不断顺着一条路走, 直到走不通为止, 然后回退到上一个路口再向另外的方向行走(走过的方向就不会再走了,又不是傻子, 知道走不通,还向走不通的方向走), 不断重复(试过所有路口, 状态转移), 重复直到找到唯一的一条合适的路径;
DFS可以看做是二叉树的先序遍历, 多说无益; 直接上代码。
# 构建一个图的类
class Graph:
# 初始化函数
def __init__(self):
# 存储所有的结点
self.V = []
# 构建结点类
class Vertex:
# 初始化函数
def __init__(self, x):
# 将x定为结点的键
self.key = x
# 设定初始化的颜色为白色 也就是还未遍历到
self.color = 'white'
# 搜索到时的时间戳
self.d = 10000
# 搜索完成时的时间戳
self.f = 10000
# 父节点设置为空
self.pi = None
# 子节点用一个列表存起来
self.adj = []
# 将函数全都放在Solution类里面
class Solution:
def DFS(self, G):
# 遍历图中的所有结点用来初始化
for u in G.V:
# 初始化所有属性
u.color = 'white'
u.pi = None
# 如果想要在函数内定义全局作用域,需要加上global修饰符,这样在函数里面不需要先声明再使用
# 定义全局变量time 贯穿整个函数内外
global time
# 将time初始化为0
time = 0
# 遍历每一个结点进行深搜
for u in G.V:
if u.color == 'white':
self.DfsVisit(G, u)
# 深搜的方法
def DfsVisit(self, G, u):
# 声明全局变量 time
global time
# 这时候时间戳过了一个 所以时间加一
time = time + 1
# 当前点的开始时间戳可以赋上time
u.d = time
# 搜索到的点颜色变为灰色
u.color = 'gray'
# 开始遍历u的子节点
for v in u.adj:
# 如果v还未被便利到
if v.color == 'white':
# 确定v的父节点为u
v.pi = u
# 然后递归从v开始进行深搜
self.DfsVisit(G, v)
# 当u点所有子节点搜索完了之后 u的颜色变为黑色
u.color = 'black'
# 时间戳加一
time = time + 1
# 确定u完成搜索的时间
u.f = time
# 开始进行操作 写出主函数
if __name__ == '__main__':
# 初始化六个结点
u, v, w, x, y, z = [Vertex(i) for i in ['u', 'v', 'w', 'x', 'y', 'z']]
# 确定子节点
u.adj = [v, x]
v.adj = [y]
w.adj = [y, z]
x.adj = [v]
y.adj = [x]
z.adj = [z]
# 实例化一个图
G = Graph()
# 将所有结点装入图中
G.V = [u, v, w, x, y, z]
# 实例化Solution类 可以调用封装好的方法
m = Solution()
# 调用深度优先搜索方法
m.DFS(G)
# 搜索完成之后 看看每一个点记录下的结果
for v in G.V:
print(v.key, v.color, v.d, v.f)
# v.d一定是表示的最短路径
过程图示:
深度优先搜索的性质
依据深度优先搜索可以获得有关图的结构的大量信息。也许深度优先搜索的最基本的特征是它的先辈子图G,形成一个由树组成的森林,这是因为深度优先树的结构准确反映了DFS_Visit中递归调用的结构的缘故,即u=π[v]当且仅当在搜索u的邻接表过程中调用了过程DFS_Visit(v)。
定理1 括号定理
在对有向图或无向图G=(V,E)的任何深度优先搜索中,对于图中任意两结点u和v,下述三个条件中有且仅有一条成立:
区间[d[u],f[u]]和区间[d[v],f[v]]是完全分离的;
区间[d[u],f[u]]完全包含于区间[d[v],f[v]]中且在深度优先树中u是v的后裔;
区间[d[v],f[v]]完全包含于区间[d[u],f[u]]中且在深度优先树中v是u的后裔。
推论1 后裔区间的嵌入
在有向或无向图G的深度优先森林中,结节v是结点u的后裔当且仅当d[u]<d[v]<f[v]<f[u]。
定理2 白色路径定理
在一个有向或无向图G=(V,E)的深度优先森林中,结点v是结点u的后裔当且仅当在搜索发现u的时刻d[u],从结点u出发经一条仅由白色结点组成的路径可达v。
边的分类
在深度优先搜索中,另一个令人感兴趣的特点就是可以通过搜索对输入图G=(V,E)的边进行归类,这种归类可以发现图的很多重要信息。例如一个有向图是无回路的,当且仅当深度优先搜索中没有发现“反向边”。
根据在图G上进行深度优先搜索所产生的深度优先森林G,我们可以把图的边分为四种类型。
1.树枝,是深度优先森林Gπ中的边,如果结点v是在探寻边(u,v)时第一次被发现,那么边(u,v)就是一个树枝。
2.反向边,是深度优先树中连结结点u到它的祖先v的那些边,环也被认为是反向边。
3.正向边,是指深度优先树中连接顶点u到它的后裔的非树枝的边。
4.交叉边,是指所有其他类型的边,它们可以连结同一棵深度优先树中的两个结点,只要一结点不是另一结点的祖先,也可以连结分属两棵深度优先树的结点。
结论:对于某条边(u,v),
当且仅当d[u]<d[v]<f[v]<f[u]时,是树枝边或正向边;
当且仅当d[v]<d[u]<f[u]<f[v]时,是反向边;
当且仅当d[v]<f[v]<d[u]<f[u]时,是交叉边;
定理3
在对无向图G进行深度优先搜索的过程中,G的每条边要么是树是反向边。
4.拓扑排序
一,拓扑排序的思想
拓扑排序是利用深度优先搜索对有向无环图进行线性排序,即对于一个图G进行拓扑排序后,所有结点按线性排放,凡是在图G中u在v前面,拓扑排序后u也一定在v前面。
常见的拓扑排序的应用比如早上穿衣的顺序,袜子必须在穿鞋前穿好,领带必须在衬衫穿好后才能戴。
二,拓扑排序算法介绍
我们对有向无环图进行DFS操作,并将结点按结束时间逆序线性排列
三,拓扑排序伪代码
TOPLOGICAL_SORT(G)
1. call DFS(G) to compute finishing times v.f for each vertex v
2. as each vertex is finished,insert it onto the front of a linked list
3. return the linked list of vertices
总共就三步,先对图G进行DFS,然后每当一个结点被访问完毕(标为黑色),将他放到一个队列的最前端,即先访问完的结点一个在队列最后,最后返回这个队列
四,拓扑排序的复杂度
时间复杂度:
拓扑排序的时间复杂度就是DFS的时间复杂度,即O(V+E)
五,拓扑排序的代码
# 算法导论: 基于DFS的拓扑排序
# 构建一个图的类
class Graph:
# 初始化函数
def __init__(self):
# 存储所有的结点
self.V = []
# 构建结点类
class Vertex:
# 初始化函数
def __init__(self, x):
# 将x定为结点的键
self.key = x
# 设定初始化的颜色为白色 也就是还未遍历到
self.color = 'white'
# 搜索到时的时间戳
self.d = 10000
# 搜索完成时的时间戳
self.f = 10000
# 父节点设置为空
self.pi = None
# 子节点用一个列表存起来
self.adj = []
# 链表的下一个结点
self.next = None
# 将函数全都放在Solution类里面
class Solution:
def DFS(self, G):
# 遍历图中的所有结点用来初始化
for u in G.V:
# 初始化所有属性
u.color = 'white'
u.pi = None
# 如果想要在函数内定义全局作用域,需要加上global修饰符,这样在函数里面不需要先声明再使用
# 定义全局变量time 贯穿整个函数内外
global time
# 将time初始化为0
time = 0
# 遍历每一个结点进行深搜
for u in G.V:
if u.color == 'white':
self.DfsVisit(G, u)
# 深搜的方法
def DfsVisit(self, G, u):
# 声明全局变量 time
global time
# 这时候时间戳过了一个 所以时间加一
time = time + 1
# 当前点的开始时间戳可以赋上time
u.d = time
# 搜索到的点颜色变为灰色
u.color = 'gray'
# 开始遍历u的子节点
for v in u.adj:
# 如果v还未被便利到
if v.color == 'white':
# 确定v的父节点为u
v.pi = u
# 然后递归从v开始进行深搜
self.DfsVisit(G, v)
# 当u点所有子节点搜索完了之后 u的颜色变为黑色
u.color = 'black'
# 时间戳加一
time = time + 1
# 确定u完成搜索的时间
u.f = time
# 拓扑排序的方法定义
def TopologicalSort(self, G):
# 定义一个链表记录
LinkedList = Vertex('#')
# 进行一次深搜
self.DFS(G)
# 按照结束搜索额时间从小到大排序
G.V.sort(key=lambda v: v.f)
# 遍历排序后的结点
for v in G.V:
# 采用头插法按搜索完成时间从大到小排序
v.next = LinkedList.next
LinkedList.next = v
# 返回这个链表
return LinkedList
# 开始进行操作 写出主函数
if __name__ == '__main__':
# 生成结点
undershorts = Vertex('undershorts')
socks = Vertex('socks')
pants = Vertex('pants')
shoes = Vertex('shoes')
belt = Vertex('belt')
shirt = Vertex('shirt')
tie = Vertex('tie')
jacket = Vertex('jacket')
watch = Vertex('watch')
# 按照穿衣顺序确定结点的子节点
undershorts.adj = [pants, shoes]
socks.adj = [shoes]
pants.adj = [belt, shoes]
shoes.adj = []
belt.adj = [jacket]
shirt.adj = [belt, tie]
tie.adj = [jacket]
jacket.adj = []
watch.adj = []
# 初始化一个图
G = Graph()
# 将结点赋给图
G.V = [undershorts, socks, pants, shoes, belt, shirt, tie, jacket, watch]
# 初始化Solution方法
m = Solution()
# 调用拓扑排序的方法
Sort_List = m.TopologicalSort(G)
p = Sort_List
# 遍历链表 一个一个打印出即可
while p.next != None:
print(p.next.key, p.next.f)
p = p.next
5.强连通分量
有向图G=(V,E)的强连通分量是一个最大结点集合C⊆V,对于该集合的任意一对结点可以互相到达。
下图a的灰色覆盖区域就是图的强联通分量。
算法过程:
- 对G做DFS,并记下每个结点的完成时间u.f
- 转置G,得到GT,如上图b
- 按照u.f大小,从大到小对GT做DFS
- 第三步得到的深度优先深林中,每棵深度优先树的结点都组成为G的一个强联通分量
伪代码:
贴上代码:
算法导论: 基于DFS的强连通分量
# 构建一个图的类
class Graph:
# 初始化函数
def __init__(self):
# 存储所有的结点
self.V = []
# 构建结点类
class Vertex:
# 初始化函数
def __init__(self, x):
# 将x定为结点的键
self.key = x
# 设定初始化的颜色为白色 也就是还未遍历到
self.color = 'white'
# 搜索到时的时间戳
self.d = 10000
# 搜索完成时的时间戳
self.f = 10000
# 父节点设置为空
self.pi = None
# 子节点用一个列表存起来
self.adj = []
# 将函数全都放在Solution类里面
class Solution:
def DFS(self, G):
# 遍历图中的所有结点用来初始化
for u in G.V:
# 初始化所有属性
u.color = 'white'
u.pi = None
# 如果想要在函数内定义全局作用域,需要加上global修饰符,这样在函数里面不需要先声明再使用
# 定义全局变量time 贯穿整个函数内外
global time
# 将time初始化为0
time = 0
# 遍历每一个结点进行深搜
for u in G.V:
# 如果还未搜索到该点
if u.color == 'white':
# 将该点存入一个列表中
list = [u]
# 从当前点开始深搜
self.DfsVisit(G, u, list)
# 以json格式输出列表中结点的名字
print(''.join([i.key for i in list]))
# 深搜的方法
def DfsVisit(self, G, u, list):
# 声明全局变量 time
global time
# 这时候时间戳过了一个 所以时间加一
time = time + 1
# 当前点的开始时间戳可以赋上time
u.d = time
# 搜索到的点颜色变为灰色
u.color = 'gray'
# 开始遍历u的子节点
for v in u.adj:
# 如果v还未被便利到
if v.color == 'white':
# 将该节点加入列表中
list.append(v)
# 确定v的父节点为u
v.pi = u
# 然后递归从v开始进行深搜
self.DfsVisit(G, v, list)
# 当u点所有子节点搜索完了之后 u的颜色变为黑色
u.color = 'black'
# 时间戳加一
time = time + 1
# 确定u完成搜索的时间
u.f = time
# 图的转置
def GraphTranspositon(self, G):
# 遍历所有结点
for u in G.V:
# 将每一个结点的子节点列表后面添加一个空列表
# 现在每一个子节点的列表中有两个列表元素 一个和原来一样 一个是空的
u.adj = (u.adj, [])
# 继续遍历结点
for u in G.V:
# 遍历每个节点的子节点列表中的每个元素
for v in u.adj[0]:
# 然后这个子节点将其父节点加入到它的子节点列表中
v.adj[1].append(u)
# 遍历结点 将刚才得到的更新后的每个点对应的子节点的列表赋给对应点的结点
for u in G.V:
u.adj = u.adj[1]
# 返回转置之后的图
return G
# 强连通分量函数
def StronglyConnectedComponents(self, G):
# 先深搜一次
self.DFS(G)
# 进行一次转置
G_Transposition = self.GraphTranspositon(G)
# 将转置之后的图按照搜索完成的时间的从大到小排序
G_Transposition.V.sort(key=lambda v: v.f, reverse=True)
# 排序之后然后再对转置之后的图进行深搜
self.DFS(G_Transposition)
# 主函数
if __name__ == '__main__':
# 新建结点
a, b, c, d, e, f, g, h = [Vertex(i) for i in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']]
# 定义每个节点的子节点
a.adj = [b]
b.adj = [c, e, f]
c.adj = [d, g]
d.adj = [c, h]
e.adj = [a, f]
f.adj = [g]
g.adj = [f, h]
h.adj = [h]
# 新建一个图
G = Graph()
# 将所有结点装到图中
G.V = [a, b, c, d, e, f, g, h]
# 实例化函数类
m = Solution()
# 对图调用强连通分量函数 得到该图的强连通分量
m.StronglyConnectedComponents(G)
图纸上画出过程:
强连通分量的最终变化图: