第 6 章 图
本章地位:图是 408 数据结构当之无愧的「大题高地」——算法大题几乎年年从这里出(遍历序列、最短路径、拓扑排序、关键路径轮流坐庄),选择题还稳定贡献 2~4 分。本章主线只有三条:①把图存起来(邻接矩阵 / 邻接表 / 十字链表 / 邻接多重表);②把图走一遍(BFS / DFS);③在图上做优化(最小生成树、最短路径、拓扑排序、关键路径)。后面四类算法在真题里全部以「手工模拟 + 填表」的形式考查,本章每个表格都配了具体数值核对 ✓,请务必拿草稿纸跟着算一遍——图的大题不是「想」出来的,是「算」出来的。
6.1 图的基本概念 高频考点
- 有向图 / 无向图:有向图的边是有序对,称弧,记 \(\langle v, w\rangle\)(\(v\) 为弧尾,\(w\) 为弧头,箭头指向 \(w\));无向图的边是无序对 \((v,\ w)\)。
- 简单图 / 多重图:若图中①不存在自环(顶点到自身的边)、②不存在重边(同一对顶点间的重复边),称为简单图;否则为多重图。408 除非特别声明,讨论的都是简单图。
6.1.1 顶点的度与握手定理
数值验证:三角形(\(n=3,\ e=3\))每个顶点度都是 2,度和 \(2+2+2=6=2\times3\) ✓;有向环 \(\langle v_1,v_2\rangle,\langle v_2,v_3\rangle,\langle v_3,v_1\rangle\)(\(e=3\))每点入度 = 出度 = 1,\(\sum ID=3=\sum OD=e\) ✓。
设无向图 \(G\) 有 12 条边,其中度数为 3 的顶点有 6 个,其余顶点的度数均小于 3,则 \(G\) 至少有多少个顶点?
查看解答
由握手定理,\(\sum TD=2e=24\)。6 个 3 度顶点占去 \(6\times3=18\),剩余度数和 \(6\);其余顶点每个度数 \(\le 2\)(且属于图至少 1 度),要顶点最少就让每个顶点尽量吃满 2 度:\(6\div2=3\) 个。故至少 \(6+3=\)9 个顶点。核对:\(6\times3+3\times2=18+6=24=2\times12\) ✓。
易错:别写成 \(24\div2=12\) 个顶点——度数和除以「平均度」没有意义;正确思路是「剩余度和 ÷ 每点度上限」向上取整。
6.1.2 完全图、连通与树图
数值验证:无向 \(n=5\):\(\frac{5\times4}{2}=10\) 条,每点度 4,度和 \(5\times4=20=2\times10\) ✓;有向 \(n=4\):\(4\times3=12\) 条弧,每点 \(TD=6\),度和 \(4\times6=24=2\times12\) ✓。
- 路径:顶点序列 \(v_p,\dots,v_q\) 相邻顶点间都有边;路径长度 = 路上边(弧)的条数;顶点不重复的路径叫简单路径;首尾相同的路径叫回路(环)。
- 连通(无向):任意两顶点之间有路径;图中的极大连通子图叫连通分量。
- 强连通(有向):任意两顶点之间互相可达(\(v\to w\) 且 \(w\to v\));极大强连通子图叫强连通分量。
- 生成树:连通图的包含全部 \(n\) 个顶点的极小连通子图(\(n-1\) 条边);非连通图各分量的生成树构成生成森林。
具有 \(n\) 个顶点的无向图,若要在任何情况下(无论边怎样分布)都保持连通,至少需要多少条边?( )
A. \(n-1\) B. \(\dfrac{n(n-1)}{2}\) C. \(\dfrac{(n-1)(n-2)}{2}+1\) D. \(\dfrac{n(n-1)}{2}+1\)
查看解答
C。反过来想:边最多的不连通图长什么样?把 \(n-1\) 个顶点做成完全图、剩 1 个孤立点,此时恰不连通且边数最大:
\[ e_{\max}(\text{不连通})=\frac{(n-1)(n-2)}{2} \]再添 1 条边必然碰到孤立点,图必连通。故答案 \(\frac{(n-1)(n-2)}{2}+1\)。数值验证(\(n=4\)):\(\frac{3\times2}{2}+1=4\)。只有 3 条边时可以摆成三角形 + 孤立点(不连通);有 4 条边时若仍不连通,最多 3 条(划分 3+1),矛盾 ✓。A 错:\(n-1\) 条边只有在「恰好构成树」这一种摆法下才连通,「任何情况下」不成立。
判断正误:(1) 有向图中所有顶点的入度之和等于出度之和;(2) \(n\) 个顶点(\(n\ge2\))的强连通图中,每个顶点的入度和出度都不小于 1;(3) \(n\) 个顶点的无向图若有 \(\frac{(n-1)(n-2)}{2}+1\) 条边,则一定连通。
查看答案
(1) 对:每条弧同时贡献 1 出度 1 入度,\(\sum ID=\sum OD=e\)。
(2) 对:强连通要求每个点既有指向它的路径(入度 \(\ge1\))又有它出发的路径(出度 \(\ge1\))。
(3) 对:这正是例 2 的结论——超过「最大不连通边数」必连通。
分别计算 \(n=6\) 时无向完全图与有向完全图的边数,并用握手定理核对。
查看答案
无向 \(\frac{6\times5}{2}=15\) 条:每点度 5,度和 \(6\times5=30=2\times15\) ✓;有向 \(6\times5=30\) 条弧:每点入度 = 出度 = 5,\(\sum ID=\sum OD=30=e\) ✓。
6.2 图的存储结构
图的存储要同时安放「顶点」和「多对多的关系」,关系不像线性表那样能靠位置隐含,必须显式记录。主角是两种:邻接矩阵(顺序存储思路)与邻接表(链式存储思路),另有两个改进型号(十字链表、邻接多重表)专门修补它们各自的痛点——「每种结构解决了什么问题」正是选择题的出题点。
6.2.1 邻接矩阵与邻接表
- 无向图的邻接矩阵是对称矩阵(\(A=A^{\mathrm T}\),边 \((i,j)\) 同时落在 \([i][j]\) 与 \([j][i]\));有向图一般不对称。
- 无向图顶点 \(v_i\) 的度 = 第 \(i\) 行(或列)非零(非 \(\infty\))元个数;有向图出度看第 \(i\) 行,入度看第 \(i\) 列。
- 判断任意两顶点是否邻接、读权值都是 \(O(1)\)。
- 空间 \(O(n^{2})\),与边数无关——稠密图合算,稀疏图浪费。
// 邻接矩阵存储(严蔚敏教材风格)
#define MAX_VERTEX_NUM 100
typedef char VertexType; // 顶点数据类型
typedef int EdgeType; // 带权图:权值类型
typedef struct {
VertexType vexs[MAX_VERTEX_NUM]; // 顶点表
EdgeType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵
int vexnum, arcnum; // 顶点数、边(弧)数
} MGraph; // 无向图加边要对称写两格:arcs[i][j] = arcs[j][i] = w
- 空间 \(O(n+e)\);无向图每条边被存两次(两端各一个边结点),边结点共 \(2e\) 个;有向图只存一次,天然是「出边表」。
- 有向图求出度容易(数一下边表长度),求入度难——必须遍历全部 \(n\) 条边表(\(O(n+e)\)),或另建逆邻接表。
- 判断 \(i\)、\(j\) 是否邻接要顺着边表找,\(O(\text{该点度})\),不如矩阵快;适合稀疏图,后面 BFS / DFS 用它能拿到更好的复杂度。
// 邻接表存储(边结点 + 顶点结点 + 图)
typedef struct ArcNode { // 边(弧)结点
int adjvex; // 该边指向的邻接点下标
struct ArcNode *nextarc; // 指向依附于同一顶点的下一条边
int weight; // 权值(带权图才需要)
} ArcNode;
typedef struct VNode { // 顶点结点
VertexType data; // 顶点信息
ArcNode *firstarc; // 该顶点边表的头指针
} VNode, AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices; // 顶点数组
int vexnum, arcnum; // 顶点数、边数
} ALGraph;
6.2.2 十字链表、邻接多重表与对比
- 十字链表(有向图):把邻接表(易找出边 / 出度)与逆邻接表(易找入边 / 入度)合并成一张。弧结点含两个链域:
hlink挂到「弧头相同」的下一条弧,tlink挂到「弧尾相同」的下一条弧,同一条弧被两条链共享。解决的问题:有向图求入度难。 - 邻接多重表(无向图):每条边只设一个边结点,带
ilink、jlink两个链域,同时挂在两个端点的链上。解决的问题:无向邻接表每条边存两份、标记 / 删除一条边要改两处。
| 存储结构 | 适用 | 空间 | 求度 / 邻接 | 解决了什么 / 软肋 |
|---|---|---|---|---|
| 邻接矩阵 | 有向、无向皆可,宜稠密 | \(O(n^{2})\),与 \(e\) 无关 | 度 = 行 / 列非零元;判邻接 \(O(1)\) | 判邻接快;软肋:稀疏图浪费空间 |
| 邻接表 | 有向、无向皆可,宜稀疏 | \(O(n+e)\) | 出度易(边表长);入度要扫全表 | 省空间、利于遍历;软肋:入度难、判邻接慢 |
| 十字链表 | 有向图 | \(O(n+e)\) | 入度、出度都容易 | 补上「有向图求入度难」 |
| 邻接多重表 | 无向图 | \(O(n+e)\) | 度易求;每边仅一个结点 | 补上「无向图每边存两份、删边两处」 |
② 无向图邻接矩阵第 \(i\) 行与第 \(i\) 列的非零元个数相等(对称性),只数一处即可;
③ 十字链表对应有向图、邻接多重表对应无向图,方向别记反(「十」字有箭头方向 → 有向;「多重」两边扯平 → 无向)。
具有 \(n\) 个顶点、\(e\) 条边的无向图采用邻接表存储,下列说法错误的是( )
A. 存储空间为 \(O(n+e)\) B. 求某个顶点的度只需数其边表长度 C. 判断任意两个顶点之间是否有边只需 \(O(1)\) D. 表中边结点共 \(2e\) 个
查看解答
C。判断 \(v_i\) 与 \(v_j\) 是否邻接,需在 \(v_i\) 的边表中顺链查找,最坏 \(O(\text{度}(v_i))\),不是 \(O(1)\)——\(O(1)\) 判邻接是邻接矩阵的本领。
A、B、D 均正确。用图 6-1 核对:\(n=6\) 顶点结点,边结点 \(2\times7=14\) 个 ✓;顶点 1 的边表 3 个结点,其度 = 3(关联边 0-1、1-3、1-4)✓。
有向图采用邻接表存储时,如何求某个顶点的入度?时间复杂度多少?有哪些改进方案?
查看答案
邻接表是「出边表」,入度没有现成的链——只能遍历全部 \(n\) 条边表,数 adjvex 等于该顶点下标的结点个数,时间 \(O(n+e)\)。
改进:①另建逆邻接表(按入边建链,空间翻倍);②直接改用十字链表,出入两类链共存,求入度、出度都是顺链数结点。
6.3 图的遍历 高频考点
图的遍历要解决「每个顶点可能有多条进入路径」的重复访问问题:设辅助数组 visited[],初始全 false,访问过的顶点置 true。另一件必须刻进本能的事:从某顶点出发的一次遍历,只能访问到它所在的连通分量(无向)或它能到达的部分(有向)——非连通图的完整遍历要对每个未访问顶点再启动一次。
6.3.1 BFS 与 DFS
// BFS:队列 + visited 标记(邻接表版;入口先清空 visited,再对每个未访问顶点调用 BFS——非连通图多次启动)
bool visited[MAX_VERTEX_NUM]; // 全局辅助数组
void BFS(ALGraph G, int v) {
visit(v); // 访问出发顶点
visited[v] = true; // 入队前就标记!
EnQueue(Q, v);
while (!IsEmpty(Q)) {
DeQueue(Q, v); // 队头顶点出队
for (ArcNode *p = G.vertices[v].firstarc; p != NULL; p = p->nextarc)
if (!visited[p->adjvex]) { // 只处理未访问的邻接点
visit(p->adjvex);
visited[p->adjvex] = true; // 同一顶点绝不二次入队
EnQueue(Q, p->adjvex);
}
}
}
// DFS:递归 + 回溯(邻接表版)
void DFS(ALGraph G, int v) {
visit(v); // 访问
visited[v] = true;
for (ArcNode *p = G.vertices[v].firstarc; p != NULL; p = p->nextarc)
if (!visited[p->adjvex]) // 未访问才递归
DFS(G, p->adjvex); // 深入下一层,走完自动回溯
}
void DFSTraverse(ALGraph G) {
for (int v = 0; v < G.vexnum; v++) visited[v] = false;
for (int v = 0; v < G.vexnum; v++)
if (!visited[v]) DFS(G, v); // 非连通:调用几次就有几个分量
}
6.3.2 遍历的应用与复杂度
DFSTraverse 中 DFS 被启动的次数 = 无向图连通分量个数;②判连通:一次 DFS 后 visited 全为 true ⟺ 图连通;③判两点连通 / 可达(有向图判可达是经典大题雏形)。序列不唯一时按「邻接点编号小者优先」约定书写。
对图 6-1 的无向图 G(邻接表边表按编号升序),(1) 从顶点 0 出发写出 DFS 序列与 BFS 序列;(2) 若删除边 (0,1) 与 (0,2),再从顶点 0 出发遍历,结果如何?
查看解答
(1) DFS:visit(0) → 邻接点 1 → visit(1) → 1 的邻接点 0 已访问,走 3 → visit(3) → 3 的邻接点 1 已访问,走 5 → visit(5) → 5 的邻接点 4 → visit(4) → 4 的邻接点 1 已访问,走 2 → visit(2),全部回溯结束,序列为 \(0,\ 1,\ 3,\ 5,\ 4,\ 2\)。
BFS:访问 0,入队 1、2 → 出队 1,入队 3、4 → 出队 2(邻接点 0、4 均已标记)→ 出队 3,入队 5 → 出队 4(1、2、5 均已标记)→ 出队 5,序列为 \(0,\ 1,\ 2,\ 3,\ 4,\ 5\)。逐点核对:访问顶点恰 6 个、每条边最多被碰 2 次(无向图两边表各一次),与 \(O(n+e)\) 一致 ✓。
(2) 删边后 0 成孤立点:从 0 出发的 DFS、BFS 都只输出「0」便结束——剩余 5 个顶点属于另一个连通分量,必须由外层循环重新启动才能访问。核对:此时连通分量 2 个,遍历函数启动次数也是 2 次 ✓。
// 补全下面 DFS 程序的三个空
void DFS(ALGraph G, int v) {
visit(v);
visited[v] = ________; // ①
for (ArcNode *p = G.vertices[v].firstarc; p != NULL; p = p->nextarc)
if (________) // ②
DFS(G, ________); // ③
}
查看解答
① true——访问后立刻标记,防止重复访问;② !visited[p->adjvex]——只对未访问邻接点递归;③ p->adjvex——沿当前边深入邻接点。
追问两个变体:若漏掉①,同一顶点被反复递归,无限递归直到栈溢出;若把②的条件写反(写成对已访问点递归),同样死循环。三个空合起来一句话:「先标记、再挑新的、往深处走」。
再问复杂度:邻接表 \(O(n+e)\),邻接矩阵 \(O(n^{2})\);递归栈深不超过 \(n\)。
(1) 具有 \(n\) 个顶点、\(e\) 条边的图分别用邻接表、邻接矩阵存储,BFS 的时间复杂度各是多少?(2) 对有向图,从某顶点出发一次 DFS 能访问所有顶点,能断定该图强连通吗?
查看答案
(1) 邻接表 \(O(n+e)\),邻接矩阵 \(O(n^{2})\)。
(2) 不能。一次 DFS 全访问只说明该顶点到其余各点可达(单向),强连通要求互相可达。反例:弧 \(\langle 0,1\rangle,\langle 0,2\rangle,\langle 1,2\rangle\),从 0 出发 DFS 访问全部 3 个顶点,但没有任何回到 0 的弧,非强连通。正确判法:从每个顶点各做一次 DFS(或一次 DFS + 一次逆图 DFS)。
6.4 最小生成树 高频考点
6.4.1 Prim:逐步加顶点
对图 6-2 从 \(v_0\) 出发手工模拟(候选边按「权值最小,权同取两端编号小者」):
| 轮次 | 已选集合 \(U\) | 当前候选跨割边(权) | 选中边 | 权 |
|---|---|---|---|---|
| 初始 | \(\{0\}\) | (0,1):4 (0,2):1 | — | — |
| 1 | \(\{0,2\}\) | (0,1):4 (2,1):2 (2,4):5 | (0,2) | 1 |
| 2 | \(\{0,1,2\}\) | (1,3):3 (2,4):5 | (2,1) | 2 |
| 3 | \(\{0,1,2,3\}\) | (3,4):2 (3,5):6 (2,4):5 | (1,3) | 3 |
| 4 | \(\{0,1,2,3,4\}\) | (4,5):4 (3,5):6 | (3,4) | 2 |
| 5 | 全部 6 点 | — | (4,5) | 4 |
得 MST 边集 \(\{(0,2),(2,1),(1,3),(3,4),(4,5)\}\),权和 \(1+2+3+2+4=\)12(恰 \(n-1=5\) 条 ✓)。
6.4.2 Kruskal:逐步加边
对图 6-2 手工模拟(排序后依次:(0,2)1、(2,1)2、(3,4)2、(1,3)3、(0,1)4、(4,5)4、(2,4)5、(3,5)6):
| 次序 | 当前边(权) | 判定 | 选后连通分量状态 |
|---|---|---|---|
| 1 | (0,2):1 | 选 | \(\{0,2\}\)、\(\{1\}\)、\(\{3\}\)、\(\{4\}\)、\(\{5\}\) |
| 2 | (2,1):2 | 选 | \(\{0,1,2\}\)、\(\{3\}\)、\(\{4\}\)、\(\{5\}\) |
| 3 | (3,4):2 | 选 | \(\{0,1,2\}\)、\(\{3,4\}\)、\(\{5\}\) |
| 4 | (1,3):3 | 选 | \(\{0,1,2,3,4\}\)、\(\{5\}\) |
| 5 | (0,1):4 | 弃(0、1 已同分量,成环) | 不变 |
| 6 | (4,5):4 | 选 | 全部合并,已选 \(5=n-1\) 条,结束 |
边集与 Prim 完全一致,权和同为 12 ✓——同一图两法殊途同归。
(1) 判断:① MST 必包含全图权值最小的那条边;② 若各边权互不相同,则 MST 唯一;③ 图中存在等权边时,MST 一定不唯一;④ Prim 与 Kruskal 求出的 MST 边集总相同。
(2) 对图 6-2 改从顶点 5 出发执行 Prim,写出依次加入的顶点与边,并验证所得 MST 边集是否与正文从 0 出发时相同。
查看答案
(1) ① 对:最小权边是某个割的最小跨割边,由 MST 性质必在某棵 MST 中;② 对:唯一性的标准充分条件;③ 错:「可能不唯一」≠「一定不唯一」,图 6-2 就是等权但唯一的反例;④ 错:MST 不唯一时两种贪心可能落入不同的(权和相同的)MST,MST 唯一时边集才必然相同。
(2) \(\{5\}\):选 (5,4)(4);\(\{4,5\}\):选 (4,3)(2);\(\{3,4,5\}\):选 (3,1)(3);\(\{1,3,4,5\}\):选 (1,2)(2);最后选 (2,0)(1)。加顶点顺序 \(5\to4\to3\to1\to2\to0\),权和 \(4+2+3+2+1=12\) ✓,边集与从 0 出发完全相同——起点只改变构造过程,MST 唯一时结果必相同。
6.5 最短路径 高频考点
6.5.1 Dijkstra:逐步填表
- 在未确定的顶点中挑 \(dist\) 最小者 \(u\)(多个同小取编号小者),将 \(u\) 最终确定;
- 用 \(u\) 松弛其邻接点:若 \(dist[u]+w(u,j)\lt dist[j]\),更新 \(dist[j]\) 与 \(prev[j]\);
- 已确定的顶点永不回头再更新;不可达记 \(\infty\)。
对图 6-2(无向边双向可走)从 \(v_0\) 出发手工填表(格子记「路径长(前驱)」,加粗=本轮确定):
| 轮次 | 确定 | \(dist[1]\) | \(dist[2]\) | \(dist[3]\) | \(dist[4]\) | \(dist[5]\) |
|---|---|---|---|---|---|---|
| 初始 | — | 4 (0) | 1 (0) | \(\infty\) | \(\infty\) | \(\infty\) |
| 1 | \(v_2\) | 3 (2) | 1 | \(\infty\) | 6 (2) | \(\infty\) |
| 2 | \(v_1\) | 3 | — | 6 (1) | 6 (2) | \(\infty\) |
| 3 | \(v_3\) | — | — | 6 | 6 (2) | 12 (3) |
| 4 | \(v_4\) | — | — | — | 6 | 10 (4) |
| 5 | \(v_5\) | — | — | — | — | 10 |
最终 \(dist=[0,3,1,6,6,10]\),\(prev=[-,2,0,1,2,4]\)。逐条核对:\(0\to2=1\) ✓;\(0\to2\to1=1+2=3\)(比直连 4 更短)✓;\(0\to2\to1\to3=3+3=6\) ✓;\(0\to2\to4=1+5=6\) ✓;\(0\to2\to4\to5=6+4=10\)(第 4 轮把 12 改成 10)✓。
有向带权图:顶点 \(v_0\sim v_4\),弧 \(\langle0,1\rangle{:}10\)、\(\langle0,3\rangle{:}30\)、\(\langle0,4\rangle{:}100\)、\(\langle1,2\rangle{:}50\)、\(\langle2,4\rangle{:}10\)、\(\langle3,2\rangle{:}20\)、\(\langle3,4\rangle{:}60\)。从 \(v_0\) 出发执行 Dijkstra,给出每轮 \(dist\) 数组与最终最短路径。
查看解答
初始:\(dist=[0,10,\infty,30,100]\)(直接弧 3 条)。
| 轮 | 确定 | \(dist[1]\) | \(dist[2]\) | \(dist[3]\) | \(dist[4]\) | 说明 |
|---|---|---|---|---|---|---|
| 1 | \(v_1\) (10) | 10 | 60 | 30 | 100 | 松弛 \(\langle1,2\rangle\):\(10+50=60\lt\infty\) |
| 2 | \(v_3\) (30) | — | 50 | 30 | 90 | 松弛 \(\langle3,2\rangle\):\(30+20=50\lt60\);\(\langle3,4\rangle\):\(30+60=90\lt100\) |
| 3 | \(v_2\) (50) | — | 50 | — | 60 | 松弛 \(\langle2,4\rangle\):\(50+10=60\lt90\) |
| 4 | \(v_4\) (60) | — | — | — | 60 | 全部确定 |
最终 \(dist=[0,10,50,30,60]\)。最短路径:\(v_0\to v_1=10\) ✓;\(v_0\to v_3=30\) ✓;\(v_0\to v_3\to v_2=30+20=50\) ✓;\(v_0\to v_3\to v_2\to v_4=50+10=60\) ✓。
易错:\(dist[4]\) 一路被改三次(100→90→60),直连弧最长反而是「假象」;漏掉某轮松弛是填表大题的头号失分点。
6.5.2 Floyd 与负权问题
// Floyd 核心:中转点 k 在最外层
for (int k = 0; k < n; k++) // 依次放开每个中转点
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (A[i][k] + A[k][j] < A[i][j])
A[i][j] = A[i][k] + A[k][j]; // i→k→j 更短则改道
Floyd 允许负权边,但不允许负权回路(沿回路转一圈权和为负,每转一圈更短,最短路径不存在)。另外 Floyd 还能顺便判回路:跑完后若某 \(A[i][i]\lt0\),说明存在负回路。
下列说法正确的是( )
A. Dijkstra 算法在存在负权边的图上得到的结果可能出错 B. Floyd 算法只适用于无向图 C. Floyd 算法允许图中存在负权回路 D. Dijkstra 算法每一轮确定的顶点,之后仍可能被更短的路径取代
查看解答
A。正是上文反例的结论。B 错:Floyd 对有向、无向图都适用;C 错:负权回路上距离可无限变小,最短路径无定义,Floyd 前提是不含负权回路;D 错:「已确定不再更新」是 Dijkstra 的铁律,写代码时不许回头碰已确定集合。
6.6 拓扑排序与关键路径 高频考点
6.6.1 AOV 网与拓扑排序
- 在图中选一个入度为 0 的顶点输出(多个任选 → 拓扑序列不唯一);
- 删除它及它发出的全部弧(其全部后继入度减 1)。
AOV 网:顶点 \(1\sim6\),弧集 \(\{\langle1,2\rangle,\langle1,3\rangle,\langle2,4\rangle,\langle3,4\rangle,\langle3,5\rangle,\langle4,6\rangle,\langle5,6\rangle\}\)。下列序列中不是合法拓扑序列的是( )
A. 1,2,3,4,5,6 B. 1,3,5,2,4,6 C. 1,2,4,3,5,6 D. 1,3,2,5,4,6
查看解答
C。逐项检查每条弧「起点是否在终点前」:A 全部弧满足,合法(Kahn 过程:1 出 → 2,3 入 0 → 2 出 → 3 出 → 4,5 → 6)✓;B:1,3,5,2,4,6 —— 5 只依赖 3,可在 2 前输出,合法 ✓;C:1,2,4,3,…——\(\langle3,4\rangle\) 要求 3 在 4 之前,此处 4 抢先,不合法;D:1,3,2,5,4,6 逐弧核对全部满足,合法 ✓。本图合法拓扑序列共 5 个(另两个是 1,2,3,5,4,6 与 1,3,2,4,5,6),印证「拓扑序列不唯一」。
6.6.2 AOE 网与关键路径
- 事件最早发生时间(正推取大):\(VE(v_j)=\max\{VE(v_i)+w_{ij}\}\),源点 \(VE=0\);
- 事件最迟发生时间(反推取小):\(VL(v_i)=\min\{VL(v_j)-w_{ij}\}\),汇点 \(VL=\) 工期;
- 活动最早开始:\(e(a)=VE(v_i)\)(弧尾事件一发生即可开工);活动最迟开始:\(l(a)=VL(v_j)-w\)(再晚就耽误弧头事件);
- 时间余量 \(l(a)-e(a)=0\) 的活动即关键活动;关键活动连成关键路径。
对图 6-3 的 AOE 网:(1) 填出各事件 \(VE\)、\(VL\);(2) 填出各活动的 \(e\)、\(l\)、\(l-e\),指出全部关键活动与关键路径;(3) 将 a₃ 缩短 1 天,总工期变为多少?将 a₇ 缩短 1 天呢?
查看解答
(1) 正推(取大):\(VE(v_1)=0\);\(VE(v_2)=0+3=3\);\(VE(v_3)=0+1=1\);\(VE(v_4)=\max\{3+4,\ 1+5\}=\max\{7,6\}=7\);\(VE(v_5)=\max\{3+6,\ 7+2\}=\max\{9,9\}=9\);\(VE(v_6)=\max\{9+4,\ 7+3\}=\max\{13,10\}=13\)。工期 13。
反推(取小,\(VL(v_6)=13\)):\(VL(v_5)=13-4=9\);\(VL(v_4)=\min\{9-2,\ 13-3\}=\min\{7,10\}=7\);\(VL(v_3)=7-5=2\);\(VL(v_2)=\min\{7-4,\ 9-6\}=\min\{3,3\}=3\);\(VL(v_1)=\min\{3-3,\ 2-1\}=\min\{0,1\}=0\)。
| 事件 | \(v_1\) | \(v_2\) | \(v_3\) | \(v_4\) | \(v_5\) | \(v_6\) |
|---|---|---|---|---|---|---|
| \(VE\) | 0 | 3 | 1 | 7 | 9 | 13 |
| \(VL\) | 0 | 3 | 2 | 7 | 9 | 13 |
(2) \(e(a)=VE(\text{弧尾})\),\(l(a)=VL(\text{弧头})-w\),逐活动计算:
a₁(\(v_1\to v_2\),3):\(e=0\),\(l=3-3=0\),\(l-e=0\),关键;a₂(\(v_1\to v_3\),1):\(e=0\),\(l=2-1=1\),余量 1;a₃(\(v_2\to v_4\),4):\(e=3\),\(l=7-4=3\),关键;a₄(\(v_3\to v_4\),5):\(e=1\),\(l=7-5=2\),余量 1;a₅(\(v_2\to v_5\),6):\(e=3\),\(l=9-6=3\),关键;a₆(\(v_4\to v_5\),2):\(e=7\),\(l=9-2=7\),关键;a₇(\(v_5\to v_6\),4):\(e=9\),\(l=13-4=9\),关键;a₈(\(v_4\to v_6\),3):\(e=7\),\(l=13-3=10\),余量 3。
关键活动 a₁、a₃、a₅、a₆、a₇,连成两条关键路径:\(v_1\to v_2\to v_4\to v_5\to v_6\)(\(3+4+2+4=13\) ✓)与 \(v_1\to v_2\to v_5\to v_6\)(\(3+6+4=13\) ✓)。
(3) a₃ 缩短 1 天:工期仍为 13——a₃ 只在第一条关键路径上,缩短后该路径变 12,但第二条仍为 13,瓶颈在另一条;a₇ 缩短 1 天:工期变 12——a₇ 是两条路径的公共活动,两条同时变 12 ✓。
套路总结:缩短工期只能动关键活动;关键路径不止一条时,必须同时缩短所有关键路径共有的活动(或各条各动一个)才见效;且活动缩短后关键路径可能转移,要重算一遍。
判断:(1) 关键路径是从源点到汇点最长的路径;(2) 任何一个关键活动缩短 1 天,总工期一定缩短 1 天;(3) AOE 网的关键路径是唯一的;(4) 事件的 \(VE\) 与 \(VL\) 相等是该事件位于所有关键路径上的必要条件。
查看答案
(1) 对:工期由最慢的路线决定,定义即「最长路径」。
(2) 错:例 9(3) 中缩短 a₃ 工期不变;只有该活动落在所有关键路径上(或关键路径唯一)时才必然见效。
(3) 错:图 6-3 就有两条。
(4) 对:某事件 \(VE\ne VL\) 说明它有正的时间余量,不可能在任何关键路径上(关键路径上每个事件、每个活动余量均为 0)。
6.7 章末自测 真题风格
限时 60 分钟,先做后看答案。难度:★★ 基础 / ★★★ 强化 / ★★★★ 冲刺。涉及手工模拟的题,请在草稿纸上把表格完整画出来再对答案。
无向图 \(G\) 有 12 条边,度数为 3 的顶点有 5 个,其余顶点的度数均小于 3,则 \(G\) 至少有( )个顶点。
A. 8 B. 9 C. 10 D. 11
查看答案
C。\(\sum TD=24\),5 个 3 度点占 15,余 9;其余每点度 \(\le2\),需至少 \(\lceil9/2\rceil=5\) 个,共 \(5+5=10\)。核对:\(5\times3+4\times2+1\times1=15+8+1=24=2\times12\) ✓。
具有 \(n\)(\(n\ge2\))个顶点的有向强连通图,最少含多少条弧?( )
A. \(n-1\) B. \(n\) C. \(n(n-1)\) D. \(\frac{(n-1)(n-2)}{2}+1\)
查看答案
B。\(n\) 条弧首尾相接构成一个经过全部顶点的有向环即可(每点入 = 出 = 1)。\(n=3\) 验证:\(\langle1,2\rangle,\langle2,3\rangle,\langle3,1\rangle\) ✓;少于 \(n\) 条弧时必有点出度或入度为 0,无法互相可达。C 是有向完全图边数;D 是无向图保证连通的边数,都是干扰项。
有向图用邻接矩阵存储,顶点 \(i\) 的出度与入度分别等于( )
A. 第 \(i\) 行非零元个数、第 \(i\) 列非零元个数 B. 第 \(i\) 列非零元个数、第 \(i\) 行非零元个数 C. 第 \(i\) 行与第 \(i\) 列非零元个数之和的一半 D. 第 \(i\) 行非零元个数与第 \(i\) 列非零元个数之差
查看答案
A。矩阵元素 \(A[i][j]=1\) 表示弧 \(\langle i,j\rangle\),从 \(i\) 发出 → 计入 \(i\) 的行(出度);射向 \(j\) → 计入 \(j\) 的列(入度)。记忆:「出行入列」。无向图因对称,行 = 列,都等于度。
具有 \(n\) 个顶点、\(e\) 条边的图采用邻接表存储,从任一顶点出发做一次 BFS 的时间复杂度为( );采用邻接矩阵存储时为( )
A. \(O(n+e)\)、\(O(n^{2})\) B. \(O(n^{2})\)、\(O(n+e)\) C. \(O(n)\)、\(O(e)\) D. \(O(n\log_2 n)\)、\(O(n^{2})\)
查看答案
A。邻接表:每个顶点进出队一次 \(O(n)\),每条边(无向图两个边结点)被扫常数次 \(O(e)\),合计 \(O(n+e)\)。邻接矩阵:每个出队顶点都要扫一整行 \(n\) 格,\(n\times n=n^{2}\)。DFS 结论完全相同(对照 6.3 节)。
对 8 个顶点的非连通无向图,从顶点 0 出发做一次 DFS,共访问了 6 个顶点,则该图连通分量数至少为( )
A. 1 B. 2 C. 6 D. 8
查看答案
B。一次 DFS 恰好覆盖一个连通分量,0 所在分量含 6 个顶点;剩下 2 个顶点无论连不连通,至少还要 1 个分量,故至少 2 个(若那 2 点互不邻接则是 3 个)。核对:遍历函数对 8 点图启动 DFS 的次数 = 分量数 ≥ 2 ✓。
关于最小生成树,下列说法正确的是( )
A. Prim 算法更适合稀疏图 B. 若连通带权图各边权互不相同,则其最小生成树唯一 C. 最小生成树唯一,当且仅当各边权互不相同 D. Kruskal 算法的时间复杂度为 \(O(n^{2})\)
查看答案
B。A 反了:Prim \(O(n^{2})\) 与边数无关,适合稠密,Kruskal \(O(e\log_2 e)\) 适合稀疏。B 是标准结论 ✓。C「当且仅当」过强:等权图的 MST 也可能唯一(图 6-2 即反例)。D 错在 \(n^{2}\),应为 \(O(e\log_2 e)\)。
关于最短路径算法,正确的是( )
A. Floyd 算法用于求单源最短路径 B. Dijkstra 算法能正确处理含负权边的图 C. Floyd 算法的时间复杂度为 \(O(n^{2})\) D. Floyd 算法允许负权边,但不允许负权回路
查看答案
D。A:Floyd 求每一对顶点间最短路径;B:Dijkstra 的「已确定不回头」假设会被负权边破坏(6.5.2 反例:正确 0、算出 1);C:三重循环 \(O(n^{3})\);D 正确,且跑完后 \(A[i][i]\lt0\) 可判存在负回路。
有向图边集为 \(\{\langle1,2\rangle,\langle1,3\rangle,\langle2,4\rangle,\langle3,4\rangle,\langle3,6\rangle,\langle4,5\rangle,\langle6,5\rangle\}\),下列序列中不是合法拓扑序列的是( )
A. 1,2,3,4,6,5 B. 1,3,2,6,4,5 C. 1,3,6,2,4,5 D. 1,2,4,3,6,5
查看答案
D。D 中 4 排在 3 之前,违反弧 \(\langle3,4\rangle\)。A、B、C 逐弧检查「起点在前」均满足。技巧:不用重排全图,只盯每个选项里「编号乱序」的相邻对查弧。
AOE 网:事件 \(v_1\sim v_4\),活动 a₁=\(v_1\to v_2\)(2 天)、a₂=\(v_1\to v_3\)(3 天)、a₃=\(v_2\to v_4\)(4 天)、a₄=\(v_3\to v_4\)(1 天)。求各事件的 \(VE\)、\(VL\) 与各活动的 \(e\)、\(l\),指出关键路径与工期。
查看解答
正推(取大):\(VE(v_1)=0\),\(VE(v_2)=0+2=2\),\(VE(v_3)=0+3=3\),\(VE(v_4)=\max\{2+4,\ 3+1\}=6\)。反推(取小):\(VL(v_4)=6\),\(VL(v_3)=6-1=5\),\(VL(v_2)=6-4=2\),\(VL(v_1)=\min\{2-2,\ 5-3\}=\min\{0,2\}=0\)。
活动表:a₁(\(v_1\to v_2\),2):\(e=0\),\(l=2-2=0\),\(l-e=0\),关键;a₂(\(v_1\to v_3\),3):\(e=0\),\(l=5-3=2\),余量 2;a₃(\(v_2\to v_4\),4):\(e=2\),\(l=6-4=2\),关键;a₄(\(v_3\to v_4\),1):\(e=3\),\(l=6-1=5\),余量 2。
关键路径 \(v_1\to v_2\to v_4\)(\(2+4=6\) ✓),工期 6 天;a₂、a₄ 各有 2 天机动时间。
有向带权图:顶点 \(0\sim3\),弧 \(\langle0,1\rangle{:}10\)、\(\langle0,2\rangle{:}5\)、\(\langle1,3\rangle{:}1\)、\(\langle2,1\rangle{:}3\)、\(\langle2,3\rangle{:}2\)。从顶点 0 执行 Dijkstra,给出每轮 \(dist\) 数组、最终最短路径及路径长度。
查看解答
初始:\(dist=[0,10,5,\infty]\)(直接弧 \(\langle0,1\rangle\)、\(\langle0,2\rangle\))。
| 轮 | 确定 | \(dist[1]\) | \(dist[2]\) | \(dist[3]\) | 说明 |
|---|---|---|---|---|---|
| 1 | \(v_2\) (5) | 8 | 5 | 7 | \(5+3=8\lt10\);\(5+2=7\lt\infty\) |
| 2 | \(v_3\) (7) | 8 | — | 7 | \(v_3\) 无出弧 |
| 3 | \(v_1\) (8) | 8 | — | — | 松弛 \(\langle1,3\rangle\):\(8+1=9\gt7\),不改 |
最终 \(dist=[0,8,5,7]\):\(0\to2=5\) ✓;\(0\to2\to1=5+3=8\)(比直连 10 短)✓;\(0\to2\to3=5+2=7\)(比 \(0\to2\to1\to3=9\) 短)✓。
套路总结:Dijkstra 大题拿分三件事——每轮选对最小未确定点、松弛该点全部出弧、已确定的行不再看;表格里顺手写前驱,最后逆着 prev 还原路径。
6.8 本章考点总结
| 考点 | 常考题型 | 热度 | 核心方法 |
|---|---|---|---|
| 度与边数计算 | 选择题 | ★★★★ 高频 | 握手定理 \(\sum TD=2e\)、\(\sum ID=\sum OD=e\);完全图 \(\frac{n(n-1)}{2}\) / \(n(n-1)\);强连通最少 \(n\) 弧;树图 \(\Leftrightarrow\) 连通 + \(n-1\) 边 |
| 四种存储结构 | 选择题 | ★★★★ | 矩阵对称性、出行入列、\(O(n^{2})\);表 \(O(n+e)\)、入度难;十字链表(有向)补入度、多重表(无向)边存一份 |
| BFS / DFS | 选择 + 大题 | ★★★★★ 每年必考 | 队列 / 递归栈;邻接表 \(O(n+e)\)、矩阵 \(O(n^{2})\);序列按小编号约定;DFS 启动次数 = 连通分量数;入队前标记 |
| 最小生成树 | 大题 | ★★★★ | Prim 逐步加点 \(O(n^{2})\) 宜稠密;Kruskal 排序加边 + 并查集判环 \(O(e\log_2 e)\) 宜稀疏;权互不相同 ⟹ MST 唯一 |
| 最短路径 | 大题(高频高地) | ★★★★★ | Dijkstra 每轮「定一点、松一圈、不回头」,\(O(n^{2})\),不能负权;Floyd 三层循环 \(k\) 最外 \(O(n^{3})\),可负边不可负回路 |
| 拓扑排序 | 选择 + 大题 | ★★★★ | Kahn 入度法 \(O(n+e)\);序列不唯一;输出 < \(n\) 个 ⟹ 有环;逆拓扑 = 倒置 |
| 关键路径 | 大题 | ★★★★★ | VE 正推取大、VL 反推取小;\(e=VE\)、\(l=VL-w\)、\(l-e=0\) 为关键活动;多条关键路径时需同时缩短才见效 |