文章目录
- 前言
- 参考目录
- 学习笔记
- 1:图表介绍
- 1.1:定义
- 1.2:常见应用
- 1.3:术语
- 1.4:一些图表处理问题
- 2:图表 API
- 2.1:图的表示
- 2.2:无向图 API
- 2.3:典型图处理代码
- 2.4:图的几种表示方法
- 2.4.1:边集(边的数组) Set-of-edges
- 2.4.2:邻接矩阵 Adjacency-matrix
- 2.4.3:邻接表数组 Adjacency-list
- 2.4.4:Java 实现:邻接表数组
- 2.5:实际应用
- 2.6: 小结
- 3:深度优先搜索 depth-first search
- 3.1:走迷宫
- 3.2:深度优先搜索概述
- 3.3:图形处理的设计模式
- 3.4:demo 演示
- 3.5:数据结构
- 3.6:Java 实现
- 3.7:特性
- 4:广度优先搜索 breadth-first search
- 4.1:demo 演示
- 4.2:特性
- 4.3:Java 实现
- 5:连通分量 connected components
- 5.1:定义
- 5.2:demo 演示
- 5.3:Java 实现
- 6:挑战(略)
前言
来到书本后半部分的学习,视频对应到 Part Ⅱ 部分。书本第 4 章主要内容是图(Graph),本篇主要内容包括:图表介绍、图表 API、深度优先搜索、广度优先搜索 以及 连通分量。
参考目录
- B站 普林斯顿大学《Algorithms》视频课
(请自行搜索。主要以该视频课顺序来进行笔记整理,课程讲述的教授本人是该书原版作者之一 Robert Sedgewick。) - 微信读书《算法(第4版)》
(本文主要内容来自《4.1 无向图》) - 官方网站
(有书本配套的内容以及代码)
学习笔记
注1:下面引用内容如无注明出处,均是书中摘录。
注2:所有 demo 演示均为视频 PPT demo 截图。
注3:如果 PPT 截图中没有翻译,会在下面进行汉化翻译,因为内容比较多,本文不再一一说明。
Prof. Sedgewick 对本章节的开篇介绍:
Our topic today is algorithms for processing undirected graphs which are a model that are widely used for many, many applications.
今天我们讨论的主题是在计算机编程中处理无向图的算法,这类算法模型在很多很多应用中都有着广泛的应用。
We’ll look at three fundamental algorithms for processing undirected graphs and consider some of the challenges of developing algorithms for this kind of structure.
我们将关注用于处理无向图结构的三个核心算法,并考察在开发这类结构适用算法时所遇到的部分难题。
1:图表介绍
1.1:定义
图:由边(edges)两两连接起来的顶点(vertexes)集合。
书中的定义:
定义。图是由一组顶点和一组能够将两个顶点相连的边组成的。
为什么学习图表算法?
- 实际应用广泛多样。
(数千种实际应用场合中都可以见到图算法的身影。) - 已知数百种不同的图算法。
(在已有的知识体系中,有许多经过验证且有效的针对图问题的算法解决方案。) - 图是一种有趣且广泛应用的抽象概念。
(它能将大量实际问题抽象成易于理解和处理的图形结构。) - 是计算机科学和离散数学中富有挑战性的分支。
(研究图算法不仅有助于解决复杂问题,也有助于深入理解计算理论和数学原理。)
1.2:常见应用
图 (graph) | 节点 (vertex) | 边 (edge) |
---|---|---|
通信网络 (communication network) | 电话、计算机 (telephone, computer) | 光纤电缆 (fiber optic cable) |
电路 (circuit) | 门、寄存器、处理器 (gate, register, processor) | 导线 (wire) |
机械系统 (mechanical system) | 关节 (joint) | 杆、梁、弹簧 (rod, beam, spring) |
金融网络 (financial network) | 股票、货币 (stock, currency) | 交易 (transactions) |
交通网络 (transportation network) | 交叉路口 (intersection) | 街道 (street) |
互联网 (internet) | C类网络 (class C network) | 连接 (connection) |
游戏状态图 (game state graph) | 盘面位置 (board position) | 合法走步 (legal move) |
社交关系网 (social relationship network) | 个人 (person) | 友谊关系 (friendship) |
神经网络 (neural network) | 神经元 (neuron) | 突触 (synapse) |
蛋白质交互网络 (protein interaction network) | 蛋白质 (protein) | 蛋白质-蛋白质相互作用 (protein-protein interaction) |
分子结构 (molecular structure) | 原子 (atom) | 化学键 (bond) |
1.3:术语
路径:一系列通过边相互连接的顶点构成的序列。
循环:起点与终点相同的路径。
如果两个顶点之间存在一条路径,则认为这两个顶点是相互连接的。
1.4:一些图表处理问题
问题 | 描述 |
---|---|
s-t 路径 | 图中是否存在从 s 到 t 的路径? |
最短 s-t 路径 | s 和 t 之间的最短路径是什么? |
循环 | 图中是否存在循环? |
Euler 循环 | 图中是否存在恰好使用每条边一次的循环(回路)? |
Hamilton 循环 | 图中是否存在恰好经过每个顶点一次的循环(回路)? |
连通性 | 是否有一种方式能够连接所有顶点? |
双连通性 | 是否存在一个顶点,移除该顶点后会导致图不连通? |
可平面性 | 图能否在一个平面上画出而不产生任何交叉边? |
图同构 | 两个邻接列表是否代表同一个图形结构? |
挑战:哪些图处理问题是容易解决的?哪些是困难的?哪些是不可解的?
2:图表 API
2.1:图的表示
图的绘制:提供了对图结构直观的理解。
注意事项:直观理解可能具有误导性。
顶点表示方法
- 本讲内容:采用范围从 0 至 V-1 的整数来表示顶点。
- 实际应用:利用符号表实现顶点名称与整数标识间的相互转换。
2.2:无向图 API
2.3:典型图处理代码
2.4:图的几种表示方法
2.4.1:边集(边的数组) Set-of-edges
2.4.2:邻接矩阵 Adjacency-matrix
维护一个大小为 VxV
的布尔型二维数组;
对于图中的每一条边v与w:adj[v][w] = adj[w][v] = true
;
问:遍历顶点 v 的所有相邻顶点所需的时间是多少?
2.4.3:邻接表数组 Adjacency-list
2.4.4:Java 实现:邻接表数组
edu.princeton.cs.algs4.Graph
edu.princeton.cs.algs4.Graph#Graph
edu.princeton.cs.algs4.Graph#addEdge
edu.princeton.cs.algs4.Graph#adj
2.5:实际应用
在实际应用中,我们采用邻接表的方式来表示图结构。
- 许多算法都是基于逐个遍历顶点 v 的相邻顶点来设计和实现的。
- 实际应用中的图结构往往呈现出稀疏特性。(虽然顶点数量巨大,但平均每个顶点的邻接顶点数量却相对较少。)
2.6: 小结
3:深度优先搜索 depth-first search
3.1:走迷宫
Trémaux 迷宫探索 (Trémaux maze exploration):
书中关于 Trémaux 迷宫算法的描述:
说个题外话,当看到教授 PPT 原文中 Unroll a ball of string
,我觉得可能翻译成展开一个线团更合适,这让我想到了希腊神话里面忒休斯 (Theseus) 解开米诺斯的迷宫(得益于阿里阿德涅 (Ariadne) 帮助,他把线团绑在腰上解开了迷宫),并战胜了牛头人弥诺陶洛斯 (Minotaurus)。(感兴趣的朋友可以看看我的童年动画《奥林匹斯星传》第29集 《忒修斯和米诺特罗斯》)不知道法国数学家 Charles Pierre Trémaux 发明这个算法的时候是不是从这里得到了灵感哈哈哈。
最神奇的是,前面刚讲完,然后教授的 PPT 里面就出现了:
(视频演示了不同难度的迷宫探索,略过)
3.2:深度优先搜索概述
目标:系统性地遍历一个图。
想法:模仿迷宫探索方式。(函数调用栈充当线团的角色)
典型应用:
- 寻找与给定起始顶点相连的所有顶点。
- 寻找两个顶点之间的路径。
3.3:图形处理的设计模式
设计模式:将图的数据类型与图处理过程解耦。
步骤如下:
- 创建一个 Graph 对象,用于存储和表示图结构。
- 将创建的 Graph 对象传递给一个专门处理图的函数或方法。
- 通过查询该图处理函数或方法获取所需信息。
3.4:demo 演示
访问一个顶点 v 时:
- 将顶点 v 标记为已访问状态。
- 递归地访问所有与 v 相邻且尚未被标记过的顶点。
初始状态:
访问第一个顶点 0,标记为 T:
需要依次检查顶点 0 的相邻顶点(顺序不是特别重要,随机顺序):分别是 6、2、1、5。
检查顶点 6,标记为 T,边 edgeTo[v]
标记为 0,表示从 0 开始到此顶点:
顶点 6 同样需要递归检查相邻顶点:分别是 0 和 4。
首先检查顶点 0,已经被标记。
检查顶点 4,标记为 T,边 edgeTo[v]
标记为 6:
顶点 4 需要递归检查相邻顶点:分别是 5、6、3。
检查顶点 5,标记为 T,边 edgeTo[v]
标记为 4:
顶点 5 需要递归检查相邻顶点:分别是 3、4、0。
检查顶点 3,标记为 T,边 edgeTo[v]
标记为 5:
顶点 3 需要递归检查相邻顶点:分别是 5 和 4。
检查顶点 5,已经被标记。
检查顶点 4,已经被标记。
此时完成了顶点 3 的搜索:
返回顶点 5 继续搜索:
检查顶点 4,已经被标记。
检查顶点 0,已经被标记。
完成了顶点 5 的搜索:
返回顶点 4 继续搜索:
检查顶点 6,已经被标记。
检查顶点 3,已经被标记。
完成了顶点 4 的搜索:
返回顶点 6,同理完成顶点 6 搜索:
返回顶点 0 继续搜索:
检查顶点 2,标记为 T,边 edgeTo[v]
标记为 0:
检查相邻顶点 0,完成搜索:
检查顶点 1,标记为 T,边 edgeTo[v]
标记为 0:
检查相邻顶点 0,完成搜索:
返回顶点 0 继续搜索:
检查顶点 5,已经被标记。
完成顶点 0 的搜索:
完成了所有顶点 0 可达的顶点搜索:
3.5:数据结构
数据结构:
- 布尔数组
marked[]
用于记录已访问过的顶点。 - 整数数组
edgeTo[]
用于追踪路径。
若(edgeTo[w] == v)
,则表示首次访问顶点 w 时是通过从顶点 v 到顶点 w 的边到达的。 - 函数调用栈用于实现递归过程。
3.6:Java 实现
edu.princeton.cs.algs4.DepthFirstPaths
3.7:特性
命题:深度优先搜索(DFS)可以在时间复杂度与起始顶点 s 连接的所有顶点的度数之和成比例(加上初始化 marked[] 数组所需的时间)的情况下,标记所有与 s 相连的顶点。
证明【正确性】:
- 若顶点 w 已被标记,则说明 w 与 s 相连(原因在于 DFS 会递归地访问与已访问顶点相邻的未访问顶点,直至所有相连顶点都被访问到)。
- 若顶点 w 与 s 相连,则 w 会被标记(如果 w 未被标记,则考虑从 s 到 w 路径上最后一个由已标记顶点指向未标记顶点的边,DFS 会在递归过程中找到并标记 w)。
证明【运行时间】:
与起始顶点s相连的每个顶点仅会被访问一次。
对应书本命题 A:
命题:在执行深度优先搜索(DFS)后,可以在常数时间内检查顶点 v 是否与顶点 s 连通,并且如果存在一条路径,可以在与其长度成比例的时间内找到从 v 到 s 的路径。
证明:edgeTo[] 是根为顶点 s 的树的父节点链接表示。
相关代码:
edu.princeton.cs.algs4.DepthFirstPaths#pathTo
对应书本命题 A (续):
4:广度优先搜索 breadth-first search
4.1:demo 演示
重复以下操作,直到队列为空:
- 从队列中移除顶点 v。
- 将所有与顶点 v 相邻且尚未标记的顶点添加到队列中,并将它们标记为已访问。
初始状态:
将顶点 0 添加到队列:
queue
代表队列,v
代表顶点,edgeTo[]
保存边信息,distTo[]
保存路径长度信息。
顶点 0 出队:
需要依次检查顶点 0 的相邻顶点:分别是 2、1、5。
检查顶点 2:
顶点 2 没有被标记,添加到队列中。
检查顶点 1:
顶点 1 没有被标记,添加到队列中。
检查顶点 5:
顶点 5 没有被标记,添加到队列中。
完成顶点 0 搜索:
顶点 2 出队:
依次检查顶点 2 的相邻顶点:分别是 0、1、3、4。
检查顶点 0,已经被标记。
检查顶点 1,已经被标记。
检查顶点 3:
顶点 3 没有被标记,添加到队列中。
检查顶点 4:
顶点 4 没有被标记,添加到队列中。
完成顶点 2 搜索:
顶点 1 出队:
依次检查顶点 1 的相邻顶点:分别是 0、2。
检查顶点 0,已经被标记。
检查顶点 2,已经被标记。
完成顶点 1 搜索:
顶点 5 出队:
依次检查顶点 5 的相邻顶点:分别是 3、0。
检查顶点 3,已经被标记。
检查顶点 0,已经被标记。
完成顶点 5 搜索:
顶点 3 出队:
依次检查顶点 3 的相邻顶点:分别是 5、4、2。
检查顶点 5,已经被标记。
检查顶点 4,已经被标记。
检查顶点 2,已经被标记。
完成顶点 3 搜索:
顶点 4 出队:
依次检查顶点 4 的相邻顶点:分别是 3、2。
检查顶点 3,已经被标记。
检查顶点 2,已经被标记。
完成顶点 4 搜索:
完成所有搜索:
4.2:特性
Q. BFS(宽度优先搜索)按照怎样的顺序检查顶点?
A. 从顶点 s 出发,按与 s 的距离(即边的数量)递增的顺序进行检查(队列中始终保持包含至少 0 个距离为 k 的顶点,并紧接着包含至少 0 个距离为 k+1 的顶点)。
命题:在任何连通图 G 中,BFS 能够在与 E + V 成比例的时间内计算出从顶点 s 到所有其他顶点的最短路径。
对应书本命题 B:
4.3:Java 实现
edu.princeton.cs.algs4.BreadthFirstPaths
edu.princeton.cs.algs4.BreadthFirstPaths#bfs
5:连通分量 connected components
5.1:定义
书中 API:
等价关系:
“是连通的”这一关系被视为等价关系:
・自反性:顶点 v 与自身是连通的。
・对称性:若顶点 v 与顶点 w 是连通的,则顶点 w 也与顶点 v 是连通的。
・传递性:若顶点 v 与顶点 w 是连通的,并且顶点 w 与顶点 x 是连通的,则顶点 v 与顶点 x 也是连通的。
定义:连通分量是一个最大的连通顶点集合。
注:在给定连通分量的情况下,可以在常数时间内得到查询结果。
5.2:demo 演示
访问一个顶点 v 时:
- 将顶点 v 标记为已访问状态。
- 递归地访问所有与 v 相邻且尚未被标记过的顶点。
(和深度优先搜索一样)
初始状态:
marked[]
保存标记状态,cc[]
保存连通分量信息。
访问第一个顶点 0:
依次检查顶点 0 的相邻顶点:分别是 6、2、1、5。
检查顶点 6:
依次检查顶点 6 的相邻顶点:分别是 0、4。
检查顶点 0,已经被标记。
检查顶点 4:
依次检查顶点 4 的相邻顶点:分别是 5、6、3。
检查顶点 5:
顶点 5 需要递归检查相邻顶点:分别是 3、4、0。
检查顶点 3:
顶点 3 需要递归检查相邻顶点:分别是 5、4。
检查顶点 5,已经被标记。
检查顶点 4,已经被标记。
完成顶点 3 的搜索:
返回顶点 5 继续搜索:
检查顶点 4,已经被标记。
检查顶点 0,已经被标记。
完成顶点 5 的搜索:
返回顶点 4 继续搜索:
检查顶点 6,已经被标记。
检查顶点 3,已经被标记。
完成顶点 4 的搜索:
返回顶点 6,同理完成顶点 6 搜索:
返回顶点 0 继续搜索:
检查顶点 2:
完成顶点 2 的搜索。
检查顶点 1:
完成顶点 1 的搜索。
返回顶点 0 继续搜索:
检查顶点 5,已经被标记。
完成顶点 0 的搜索:
连通分量:
顶点 0、2、3、4、5、6 的连通分量编号都是 0。
检查顶点 7:
检查顶点 8:
顶点 7 和顶点 8 的连通分量编号都是 1。
检查顶点 9:
顶点 9 需要递归检查相邻顶点:分别是 11、10、12。
检查步骤同上,最终结果:
顶点 9、10、11、12 的连通分量编号都是 2。
完成所有的连通分量查询:
5.3:Java 实现
edu.princeton.cs.algs4.CC
edu.princeton.cs.algs4.CC#dfs
6:挑战(略)
视频最后讲到几个挑战案例,本文不再详细展开,感兴趣的朋友请移步视频查看学习。
(完)