6. 图
408 学习笔记:6. 图。保留原有知识内容、公式与图示;有对应原件时提供查看和下载入口。
本页目录
ABSTRACT · 本节范围 本笔记逐页覆盖原 PDF 的 13 页和 12 道实际题目:题 0~8、10、11、13。原文件本身没有题 9、12。内容包括两种存储结构、欧拉路、BFS/DFS、无权最短路、可达性、判树、最远点、非递归 DFS、连通分量和存储转换。代码统一为 C++17。
0. 图的存储结构与基本操作
题 0:邻接表、邻接矩阵与无向边插入
以下结构统一支持顶点编号 0...n-1。edgeCount 统计逻辑边数:无向边虽然在邻接表中存两份邻接关系,但只计 1 条边。
#include <algorithm>
#include <cstddef>
#include <queue>
#include <stack>
#include <stdexcept>
#include <utility>
#include <vector>
struct AdjListGraph {
bool directed = false;
std::size_t edgeCount = 0;
std::vector<std::vector<int>> adjacent;
explicit AdjListGraph(std::size_t vertexCount = 0,
bool isDirected = false)
: directed(isDirected), adjacent(vertexCount) {}
};
struct AdjMatrixGraph {
bool directed = false;
std::size_t edgeCount = 0;
std::vector<std::vector<int>> edge;
explicit AdjMatrixGraph(std::size_t vertexCount = 0,
bool isDirected = false)
: directed(isDirected),
edge(vertexCount,
std::vector<int>(vertexCount, 0)) {}
};
void checkVertex(std::size_t vertexCount, int vertex) {
if (vertex < 0
|| static_cast<std::size_t>(vertex) >= vertexCount) {
throw std::out_of_range("vertex index");
}
}
void addEdge(AdjListGraph& graph, int from, int to) {
checkVertex(graph.adjacent.size(), from);
checkVertex(graph.adjacent.size(), to);
graph.adjacent[from].push_back(to);
if (!graph.directed) {
graph.adjacent[to].push_back(from);
}
++graph.edgeCount;
}
void addEdge(AdjMatrixGraph& graph, int from, int to) {
checkVertex(graph.edge.size(), from);
checkVertex(graph.edge.size(), to);
++graph.edge[from][to];
if (!graph.directed && from != to) {
++graph.edge[to][from];
}
++graph.edgeCount;
}
邻接表空间为 $O(|V|+|E|)$,适合稀疏图;邻接矩阵空间为 $O(|V|^2)$,判断两点是否邻接为 $O(1)$。原 PDF 的转换题按简单图、无自环理解时,矩阵元素只需 0/1;本稿的矩阵元素记录边的条数,后文转换实现还可处理重边和自环。欧拉路函数按此约定正确计算对角线自环的度数贡献。
DANGER · 原稿错误 原稿先使用
VNode、ArcNode再声明它们,结点指针和对象成员访问混用./->,最后还返回未声明的G。无向边必须对两个端点各加入一条邻接记录。
1. 欧拉路判定
题 1:无向图是否存在经过全部边一次的路
对含边的无向图,存在欧拉通路当且仅当:
- 所有度数非 0 的顶点位于同一连通分量;
- 奇度顶点数为 0 或 2。
奇度数为 0 时存在欧拉回路;为 2 时存在端点不同的欧拉通路。原题已给“无向连通图且 $|E|>0$”,因此只数奇度顶点也足够;下列实现保留完整检查。
int matrixDegree(const AdjMatrixGraph& graph, int vertex) {
int degree = 0;
for (std::size_t other = 0;
other < graph.edge.size(); ++other) {
degree += graph.edge[vertex][other];
}
degree += graph.edge[vertex][vertex];
return degree; // 自环对无向图度数贡献 2。
}
bool hasEulerTrail(const AdjMatrixGraph& graph) {
if (graph.directed || graph.edgeCount == 0) {
return false;
}
const int n = static_cast<int>(graph.edge.size());
int start = -1;
int oddDegreeVertices = 0;
for (int vertex = 0; vertex < n; ++vertex) {
const int degree = matrixDegree(graph, vertex);
if (degree > 0 && start == -1) {
start = vertex;
}
oddDegreeVertices += degree % 2;
}
std::vector<bool> visited(n, false);
std::queue<int> pending;
visited[start] = true;
pending.push(start);
while (!pending.empty()) {
const int vertex = pending.front();
pending.pop();
for (int next = 0; next < n; ++next) {
if (graph.edge[vertex][next] > 0 && !visited[next]) {
visited[next] = true;
pending.push(next);
}
}
}
for (int vertex = 0; vertex < n; ++vertex) {
if (matrixDegree(graph, vertex) > 0 && !visited[vertex]) {
return false;
}
}
return oddDegreeVertices == 0 || oddDegreeVertices == 2;
}
矩阵求度和连通检查均需扫描行,时间 $O(|V|^2)$,辅助空间 $O(|V|)$。
2. BFS 与无权最短路
题 2:广度优先遍历
顶点在“首次发现、准备入队”时立即标记,保证每个顶点最多入队一次。
std::vector<int> breadthFirstOrder(const AdjListGraph& graph,
int start) {
checkVertex(graph.adjacent.size(), start);
std::vector<int> order;
std::vector<bool> visited(graph.adjacent.size(), false);
std::queue<int> pending;
visited[start] = true;
pending.push(start);
while (!pending.empty()) {
const int vertex = pending.front();
pending.pop();
order.push_back(vertex);
for (int next : graph.adjacent[vertex]) {
if (!visited[next]) {
visited[next] = true;
pending.push(next);
}
}
}
return order;
}
题 3:BFS 求无权图最短距离
distance[v] 表示源点到 $v$ 的最少边数,-1 表示不可达。首次发现邻点时:
$$ distance[next]=distance[vertex]+1. $$
std::vector<int> unweightedDistances(const AdjListGraph& graph,
int source) {
checkVertex(graph.adjacent.size(), source);
std::vector<int> distance(graph.adjacent.size(), -1);
std::queue<int> pending;
distance[source] = 0;
pending.push(source);
while (!pending.empty()) {
const int vertex = pending.front();
pending.pop();
for (int next : graph.adjacent[vertex]) {
if (distance[next] == -1) {
distance[next] = distance[vertex] + 1;
pending.push(next);
}
}
}
return distance;
}
邻接表 BFS 和无权最短路均为时间 $O(|V|+|E|)$、空间 $O(|V|)$。原稿把 visited 初始化语句放到了循环外,还使用 INT16_MAX 作为通用距离哨兵,顶点较多时会溢出;-1 足以表达不可达。
3. 递归 DFS、可达性与判树
题 4:递归深度优先遍历
void depthFirstImpl(const AdjListGraph& graph,
int vertex,
std::vector<bool>& visited,
std::vector<int>& order) {
visited[vertex] = true;
order.push_back(vertex);
for (int next : graph.adjacent[vertex]) {
if (!visited[next]) {
depthFirstImpl(graph, next, visited, order);
}
}
}
std::vector<int> depthFirstOrder(const AdjListGraph& graph,
int start) {
checkVertex(graph.adjacent.size(), start);
std::vector<bool> visited(graph.adjacent.size(), false);
std::vector<int> order;
depthFirstImpl(graph, start, visited, order);
return order;
}
题 5:判断从顶点 $i$ 到 $j$ 是否有路径
有向图按弧方向判断,故“$i$ 可达 $j$”不一定推出“$j$ 可达 $i$”;无向图才具有对称性。
bool reachableImpl(const AdjListGraph& graph,
int vertex,
int target,
std::vector<bool>& visited) {
if (vertex == target) {
return true;
}
visited[vertex] = true;
for (int next : graph.adjacent[vertex]) {
if (!visited[next]
&& reachableImpl(graph, next, target, visited)) {
return true;
}
}
return false;
}
bool hasPath(const AdjListGraph& graph, int from, int to) {
checkVertex(graph.adjacent.size(), from);
checkVertex(graph.adjacent.size(), to);
std::vector<bool> visited(graph.adjacent.size(), false);
return reachableImpl(graph, from, to, visited);
}
题 6:判断无向图是否为树
非空无向图是树,当且仅当它连通且
$$ |E|=|V|-1. $$
bool isUndirectedTree(const AdjListGraph& graph) {
const std::size_t n = graph.adjacent.size();
if (graph.directed || n == 0 || graph.edgeCount != n - 1) {
return false;
}
std::vector<bool> visited(n, false);
std::queue<int> pending;
visited[0] = true;
pending.push(0);
std::size_t reached = 0;
while (!pending.empty()) {
const int vertex = pending.front();
pending.pop();
++reached;
for (int next : graph.adjacent[vertex]) {
if (!visited[next]) {
visited[next] = true;
pending.push(next);
}
}
}
return reached == n;
}
单顶点、无边图满足树定义;空图不视为树。三题在邻接表上的时间均为 $O(|V|+|E|)$。
DANGER · 原稿错误 多个 DFS 函数混用了
vertex/v、p.adjVertex/p->adjVertex、nextArc/netxArc,数组实参还错误写成visit[]。判树代码把指针与对象混用,并存在同一分支中连续return true; return false;的不可达语句。
4. 最远顶点与非递归 DFS
题 7:求无向连通图中距顶点 $v$ 最远的一个顶点
这里的“距离”指最短路径所含的边数。BFS 按距离非递减顺序处理顶点,所以最后一层中的顶点都最远;但同一层可能有多个顶点,单说“最后出队者”会依赖邻接表次序。下列实现规定:若最远顶点不唯一,返回编号最小者,使结果确定。
int farthestVertex(const AdjListGraph& graph, int source) {
if (graph.directed || graph.adjacent.empty()) {
throw std::invalid_argument(
"a nonempty undirected graph is required");
}
const std::vector<int> distance =
unweightedDistances(graph, source);
int farthest = source;
for (int vertex = 0;
vertex < static_cast<int>(distance.size()); ++vertex) {
if (distance[vertex] == -1) {
throw std::invalid_argument(
"the graph must be connected");
}
if (distance[vertex] > distance[farthest]
|| (distance[vertex] == distance[farthest]
&& vertex < farthest)) {
farthest = vertex;
}
}
return farthest;
}
单顶点图返回该顶点自身。时间 $O(|V|+|E|)$,辅助空间 $O(|V|)$。
题 8:深度优先遍历的非递归算法
若只在栈中保存顶点,每次回到栈顶后都从第一条邻边重新扫描,虽可借助 visited 得到遍历结果,却会反复扫描,且不容易严格复现递归 DFS 的邻接次序。准确模拟递归调用时,栈帧还应保存“下一条待检查邻边”的位置。
struct DfsFrame {
int vertex;
std::size_t nextNeighbor;
};
std::vector<int> depthFirstOrderIterative(
const AdjListGraph& graph, int start) {
checkVertex(graph.adjacent.size(), start);
std::vector<bool> visited(graph.adjacent.size(), false);
std::vector<int> order;
std::vector<DfsFrame> path;
visited[start] = true;
order.push_back(start);
path.push_back({start, 0});
while (!path.empty()) {
DfsFrame& frame = path.back();
const auto& neighbors = graph.adjacent[frame.vertex];
if (frame.nextNeighbor == neighbors.size()) {
path.pop_back();
continue;
}
const int next = neighbors[frame.nextNeighbor++];
if (!visited[next]) {
visited[next] = true;
order.push_back(next);
path.push_back({next, 0});
}
}
return order;
}
每条邻接记录最多检查一次,时间 $O(|V|+|E|)$,显式栈、访问数组和结果均占 $O(|V|)$。其输出与本节递归版本在相同邻接表次序下完全一致。
flowchart LR
S[访问起点] --> C{待处理结构}
C -->|队列| B[BFS 按层扩展]
B --> D[无权最短路与最远层]
C -->|递归或栈帧| F[DFS 沿路径深入]
F --> A[可达性、判树与连通分量]
DANGER · 原稿错误 题 7 的代码声明为
void却返回顶点,并把队列下标写成容易越界的前置自增形式;“最后出队”也没有说明同层平局。题 8 出栈后又在p == null分支重复top--,会跳过栈帧;同时每次都把邻接指针重置到首边,无法正确保存递归回溯位置。
5. 连通分量
题 10:求无向图的连通分量个数
依次扫描所有顶点;每遇到一个未访问顶点,就从它开始一次 DFS,并把计数加 1。一次 DFS 恰好覆盖一个连通分量。空图的连通分量数为 0。
std::size_t countConnectedComponents(
const AdjListGraph& graph) {
if (graph.directed) {
throw std::invalid_argument(
"connected components require an undirected graph");
}
std::vector<bool> visited(graph.adjacent.size(), false);
std::size_t components = 0;
for (int start = 0;
start < static_cast<int>(graph.adjacent.size()); ++start) {
if (visited[start]) {
continue;
}
++components;
std::stack<int> pending;
visited[start] = true;
pending.push(start);
while (!pending.empty()) {
const int vertex = pending.top();
pending.pop();
for (int next : graph.adjacent[vertex]) {
if (!visited[next]) {
visited[next] = true;
pending.push(next);
}
}
}
}
return components;
}
时间 $O(|V|+|E|)$,辅助空间 $O(|V|)$。对有向图必须先明确求弱连通分量还是强连通分量,不能直接套用本函数。
DANGER · 原稿错误 原稿调用
DFS(G, v, visit[]),其中v未定义,数组实参语法也错误;它还没有声明算法只适用于无向图。
6. 邻接表与邻接矩阵互转
本文矩阵元素记录边的条数,因此简单图取值为 0/1,也能表示重边;无向图对角元素记录自环条数,而邻接表按常规定义为每个自环保存两条邻接记录。
题 11:邻接表转换为邻接矩阵
先逐条统计邻接记录。对无向图,还要验证矩阵关于主对角线对称,并把对角线上成对的自环邻接记录折算为自环条数。
AdjMatrixGraph toAdjMatrix(const AdjListGraph& graph) {
const std::size_t n = graph.adjacent.size();
AdjMatrixGraph result(n, graph.directed);
for (int from = 0; from < static_cast<int>(n); ++from) {
for (int to : graph.adjacent[from]) {
checkVertex(n, to);
++result.edge[from][to];
if (graph.directed) {
++result.edgeCount;
}
}
}
if (!graph.directed) {
for (int vertex = 0;
vertex < static_cast<int>(n); ++vertex) {
if (result.edge[vertex][vertex] % 2 != 0) {
throw std::logic_error(
"an undirected self-loop needs two list entries");
}
result.edge[vertex][vertex] /= 2;
result.edgeCount += result.edge[vertex][vertex];
for (int other = vertex + 1;
other < static_cast<int>(n); ++other) {
if (result.edge[vertex][other]
!= result.edge[other][vertex]) {
throw std::logic_error(
"the undirected adjacency list is asymmetric");
}
result.edgeCount += result.edge[vertex][other];
}
}
}
if (result.edgeCount != graph.edgeCount) {
throw std::logic_error("inconsistent edge count");
}
return result;
}
题 13:邻接矩阵转换为邻接表
有向图扫描全部矩阵元素;无向图只处理主对角线及上三角,避免把一条无向边加入两次。矩阵必须为方阵、元素非负;无向矩阵还必须对称。
AdjListGraph toAdjList(const AdjMatrixGraph& graph) {
const std::size_t n = graph.edge.size();
for (const auto& row : graph.edge) {
if (row.size() != n) {
throw std::invalid_argument("the matrix must be square");
}
}
AdjListGraph result(n, graph.directed);
if (graph.directed) {
for (int from = 0; from < static_cast<int>(n); ++from) {
for (int to = 0; to < static_cast<int>(n); ++to) {
if (graph.edge[from][to] < 0) {
throw std::invalid_argument(
"edge multiplicity cannot be negative");
}
for (int copy = 0;
copy < graph.edge[from][to]; ++copy) {
addEdge(result, from, to);
}
}
}
} else {
for (int vertex = 0;
vertex < static_cast<int>(n); ++vertex) {
if (graph.edge[vertex][vertex] < 0) {
throw std::invalid_argument(
"edge multiplicity cannot be negative");
}
for (int copy = 0;
copy < graph.edge[vertex][vertex]; ++copy) {
addEdge(result, vertex, vertex);
}
for (int other = vertex + 1;
other < static_cast<int>(n); ++other) {
if (graph.edge[vertex][other] < 0
|| graph.edge[other][vertex] < 0) {
throw std::invalid_argument(
"edge multiplicity cannot be negative");
}
if (graph.edge[vertex][other]
!= graph.edge[other][vertex]) {
throw std::invalid_argument(
"an undirected matrix must be symmetric");
}
for (int copy = 0;
copy < graph.edge[vertex][other]; ++copy) {
addEdge(result, vertex, other);
}
}
}
}
if (result.edgeCount != graph.edgeCount) {
throw std::logic_error("inconsistent edge count");
}
return result;
}
邻接表转矩阵需要初始化 $|V|^2$ 个单元并遍历 $O(|E|)$ 条邻接记录,时间为 $O(|V|^2+|E|)$、结果空间为 $O(|V|^2)$;邻接矩阵转邻接表扫描矩阵的时间为 $O(|V|^2+|E|)$,结果空间为 $O(|V|+|E|)$。在简单图中 $|E|=O(|V|^2)$,前者时间才可简写为 $O(|V|^2)$。
DANGER · 原稿错误 两个转换函数都把输出图按值传递,调用者得不到修改结果;
edgeNum被错误赋成vertexNum。邻接表转矩阵未清零无边位置;矩阵转邻接表把顶点结点误赋为null,也没有区分有向图与无向图的重复计边。
7. 原稿纠错与题目覆盖
| 范围 | 原稿主要问题 | 本稿处理 |
|---|---|---|
| 存储与插边 | 类型声明顺序、指针访问、返回值均有编译错误 | 统一两种结构与插边接口 |
| 欧拉路 | 只数奇度,不检查非零度顶点连通性 | 同时检查连通性与 0/2 个奇度顶点 |
| BFS/最短路 | 初始化位置、队列下标、距离哨兵错误 | 首次发现即标记,-1 表示不可达 |
| DFS/可达性 | 变量名、成员名和数组实参混乱 | 递归状态显式传引用 |
| 判树 | 指针对象混用,返回分支矛盾 | 严格检查非空、无向、连通和 $ |
| 最远顶点 | 未说明最远层平局,函数返回类型错误 | 规定平局取最小编号 |
| 非递归 DFS | 重复出栈,未保存下一邻边 | 栈帧保存顶点与邻边位置 |
| 连通分量 | 未定义变量,适用图类型不明 | 逐顶点启动 DFS,并限定无向图 |
| 存储转换 | 按值输出、边数错误、重复计边 | 返回新图并验证矩阵/邻接表不变量 |
| 原题号 | 内容 | 状态 |
|---|---|---|
| 0 | 图的存储结构与无向边插入 | 已覆盖 |
| 1 | 欧拉通路判定 | 已覆盖并补全连通条件 |
| 2 | BFS | 已覆盖 |
| 3 | BFS 求无权最短路 | 已覆盖 |
| 4 | 递归 DFS | 已覆盖 |
| 5 | 可达性 | 已覆盖 |
| 6 | 无向图判树 | 已覆盖 |
| 7 | 最远顶点 | 已覆盖并规定平局 |
| 8 | 非递归 DFS | 已覆盖并修正回溯状态 |
| 10 | 无向图连通分量 | 已覆盖 |
| 11 | 邻接表转邻接矩阵 | 已覆盖 |
| 13 | 邻接矩阵转邻接表 | 已覆盖 |
NOTE · 题号说明 原 PDF 的实际编号就是题 0~8、10、11、13,共 12 题;文件中没有题 9 和题 12,并非本稿遗漏。
相关知识
8. 本节复盘
- BFS 用队列按层扩展,可直接求无权最短距离;最远点来自最大距离层,同层答案可能不唯一。
- DFS 用递归或显式栈沿路径深入;若要复现递归次序,栈帧必须保存下一邻接位置。
- 无向图是树,当且仅当非空、连通且 $|E|=|V|-1$;单顶点无边图是树。
- 无向图存在欧拉通路,需要所有非零度顶点连通,并且奇度顶点恰有 0 个或 2 个。
- 从每个尚未访问的顶点启动一次 DFS,启动次数就是无向图的连通分量数。
- 存储转换必须同步顶点数、图方向和逻辑边数;无向图要处理矩阵对称性、自环与重复邻接记录。
- 邻接表适合稀疏图,邻接矩阵适合稠密图或频繁进行邻接判定的场景。