基本图算法-图的表示和图的搜索
图的表示
图的搜索: 指系统化地跟随图中的边来访问图中的每个结点. 可以用来发现力瓣结构, 许多的图算法在一开始都会先通过搜索来获得图的结构, 其它的一些图算法则是对基本的搜索算法加以优化.
图的表示: 对于一个图\(G= (V, E)\), 其中\(\left|V\right|\)表示图的结点数, \(\left|E\right|\)表示图的边的条数(连接各个结点, 假设两个结点分为为\(u, v\), 则其形成的边可以表示为\((u, v)\), 满足\((u, v) \in E\)), \(G.V\)表示图\(G\)的结点集, \(G.E\)表示图\(G\)的边集. 可以有两种标准表示方法: 一种表示法将图作为邻接链表的组合, 另一种表示法则将图作为邻接矩阵来看. 两种表示方法都既可以表示无向图, 也可以表示有向图. 邻接链表因为在表示稀疏图(\(\left| E \right| \ll \left| V \right| ^2\))时非常紧凑而成为通常的选择, 并且可以快速判断任意两个结点之间是否有边相连.
邻接链表表示由一个包含\(\left| V \right|\)条链表的数组\(Adj\)所构成, 每个结点有一条链表, 对于每个结点\(u \in V\), 邻接链表\(Adj[u]\)包含所有与结点\(u\)之间有边相连的结点\(v\), 即\(Adj[u]\)包含图\(G\)中所有与\(u\)邻接的结点.
如果\(G\)是一个有向图, 则对于边\((u, v)\)来说, 结点\(v\)将出现在链表\(Adj[u]\)里, 如果是无向图, 则\(Adj[u]\)中包含\(v\), 同时\(Adj[v]\)中包含结点\(u\), 其所需空间为\(\Theta (V+E)\).
链表的结点可以包含任意卫星信息, 这意味着其鲁棒性很高.
但是其无法在\(O(1)\)的时间内判断边\((u, v)\)是否存在于图中.
def build_chain(V, E):
"""
构建一个邻接链表
假设结点集合V用数组[0, .., v - 1]表示, 下标为结点编号
边集合E用数组[..., (u, v), ...]表示, u, v为边的两个结点
用数组替代链表
"""
out = [[] for _ in range(len(V))]
[out[e[0]].append(e[1]) for e in E]
return out
邻接矩阵表示将图中的结点进行编号, 编号为\(i \in [A, \left| V \right|)\), 构建一个方阵\(A_{\left| V \right| \times \left| V \right|} = (a_{ij})\), 若边\((i, j)\)存在于图中, 则置\(a_{ij}\)为\(1\), 否则置为\(0\), 因此其所需空间为$\Theta (V^2) $.
对于无向图, 矩阵\(A\)为一个对称矩阵(\(A = A^T\)), 因此可以将所需空间减半.
对于有权图, 则置\(a_{ij}\)为相应的值.
def build_matrix(V, E):
"""
构建邻接矩阵
"""
size = len(V)
out = [[0] * size for _ in range(size)]
for e in E:
out[e[0]][e[1]] = 1
return out
广度优先搜索
给定图\(G = (V, E)\)和一个可识别的源结点\(s\), 广度优先搜索对图\(G\)中的边进行系统性的探索来发现可以从源结点\(s\)到达的所有结点. 可用于有向和无向图.
def breadth_first_search(G, s):
"""
广度优先搜索, G是一个邻接链表, s是起始结点
"""
size = len(G)
parents = [None] * size
# 此处可以构成一棵广度优先搜索树, 如果搜索结束后结点i的父结点为parents[i]
# 根结点, 也就是s, 的父结点为None
depth = [-1] * size
# 从上面的树中实际上可以得出与源点的距离, 也就是结点在树中的深度, 这里记录一下
# 并没有去构建这棵树
processing = [s]
depth[s] = 0
while len(processing) > 0:
item = processing.pop()
next_depth = depth[item] + 1
for sibling in G[s]:
if parents[sibling] == None and sibling != s:
depth[sibling] = next_depth
parents[sibling] = item
processing.append(sibling)
return depth, parents
最短路径: 结点在广度搜索树中的最简路径即是从源点(根结点)到结点的最短路径, 若弔不存在于树中, 则无法到达
广度优先树: 对于图\(G = (V, E)\)和源结点\(s\), 定义前驱子图\(G_{\pi} = (V_{\pi}, E_{\pi})\), 其中\(V_{\pi} = \{v \in V: v.\pi \neq \text{NIL}\}\cup\{s\}, E_{\pi} = \{(v.\pi , v): v \in V_{\pi} - \{s\}\}\), \(v.\pi\)代表结点\(v\)的父结点.
深度优先搜索
def depth_first_search(chain):
"""
深度优先搜索, G是一个邻接链表, 与BFS不同的是, 这里不再
指定源结点, 而是遍历所有结点, 进行搜索, 最终得到的是一片
不相交的森林. 原因在于二者用途不同, BFS主要用于最短路径
及相关前驱子图, 而DFS则常作为另一个算法里的一个子程序.
"""
size = len(chain)
records = [[None, -1, -1] for _ in range(size)]
# 需要记录的信息, 每个结点包括[父结点, 起始时间, 结束时间]
count = [0]
# hack method to write closure
def walk(s):
"""
递归访问, 如果不采用递归, 可以用栈来实现
"""
count[0] += 1
records[s][1] = count[0]
for c in chain[s]:
if records[c][1] == -1:
records[c][0] = s
walk(c)
count[0] += 1
records[s][2] = count[0]
for n in range(size):
if records[n][1] == -1:
walk(n)
return records
边的分类:
对图\(G\)进行深度优先搜索, 得到森林\(E_{\pi}\), 后者的边称为树边, 是图的边集合\(E\)的子集(如果是无向图, 是真子集), 但是图中的边不一定存在于森林的边中, 对图中的边进行分类:
- 树边: 也就是森林中的边, 边\((u, v)\)是由对结点\(u\)搜索时发现的(在之前\(v\)没有被搜索过)
- 后向边: 不存在于森林中, 在搜索结点\(v\)时, 结点\(u\)已经被搜索, 但是还没有结束(有向图和无向图均会产生)
- 前向边: 将结点\(u\)连接到其在深度优先树中一个后代结点\(v\)的边\((u, v)\). 中文版算法导论在这里描述的很不清楚, 无论对有向图还是无向图, 都会遇到在搜索结点\(u\)时, 邻接结点\(v\)已经被搜索结束的情况(对于无向图, 是必然情况), 但是对于无向图而言, 如果\(v\)已经结束搜索, 必然边\((v, u)\)已经被访问过, 这个时候边\((u, v)\)是一条后向边, 而\((u, v)\)和\((v, u)\)在无向图中是等价的.
- 横向边: 发生情况和前向边一样, 即对边\((u, v)\)进行搜索时, \(v\)已经搜索完毕, 不同之处在于\(v\)结束搜索时\(u\)还没有开始被搜索, 这个时候结点\(u, v\)在深度优先树中不是前后代关系.
一个有向图是否存在环可以通过深度优先搜索中是否产生后向边进行判定
拓扑排序
针对有向无环图\(G = (V, E)\)中所有结点的一种线性次序: 如果\(G\)包含边\((u, v)\), 则结点\(u\)在拓扑排序中处于结点\(v\)之前.
在深度优先搜索中, 只需要将结点按结束时间进行反向排序即可.
强连通分量
对于有向图\(G = (V, E)\), 其强连通分量是一个结点集\(C \subseteq V\), 对于任意结点\(u, v \in C\), 路径\(u \leadsto v\)和路径\(v \leadsto u\)同时存在. 也就是说, 存在一个大环, 可以连接集合\(C\)中的所有结点. 一个图可以有多个强连通分量, 但是不同的强连通分量的并集为空集.
图的转置: 对于有向图\(G = (V, E)\), 其转置\(G^T = (V, E^T), E^T = {(u, v): (v, u) \in E}\). 即对边进行反向. 如果是邻接链表法, 转置操作需要\(O (V + E)\)的时间.
分量图: 假定图\(G\)有强连通分量\(C = \{C_1, C_2, ..., C_k\}\),结点集\(V^{SCC} = \{v_1, v_2, ..., v_k\}\)代表所有强连通分量的集合, 如果强连通分量\(C_i, C_j\)之间存在边\((x, y), x \in C_i, y \in C_j\), 则边\((v_i, v_j) \in E^{SCC}\). 分量图是一个有向无环图.
源代码: https://github.com/acrazing/study/blob/master/study/algorithm/c6_graph.py
附: 有关LaTeX
- 表示绝对值:
\left| value \right|, 比如: $$\begin{align}\notag\left| \sqrt[3]{1 + \frac{1}{x^2}} \right|\end{align}$$ - 在线编辑: https://www.codecogs.com/latex/eqneditor.php
- 常用字符集: http://mohu.org/info/symbols/symbols.htm

浙公网安备 33010602011771号