2013 年第 7 题,我对着一个 4×4 的邻接矩阵数了半天,选了 A。
题目给的矩阵长这样:
0 0 0 1
A = 1 0 1 0
0 1 0 0
1 1 0 0
问各顶点的度依次是多少。我想的是"度 = 这一行里非零元素的个数",数出来 1, 2, 1, 2,选 A,很干脆。
答案是 C:3, 4, 2, 3。
问题出在我没先看矩阵对不对称 。这个矩阵第 1 行第 4 列是 1,第 4 行第 1 列也是 1,但第 1 行第 2 列是 0、第 2 行第 1 列却是 1------不对称 ,说明它是有向图 。有向图里"度 = 入度 + 出度",得把行和列加起来数,只数行就漏了一半。
从那以后我做邻接矩阵的题,第一件事就是看它是不是对称的。这一步花两秒,能救一整道题。
〇、先花两分钟:图、度、连通是什么
图 (Graph):一堆顶点 (vertex,也叫结点)和一堆连接它们的边 (edge)。边没有方向叫无向图 ,边有方向(写成 <v₁, v₂>,只能从 v₁ 走到 v₂)叫有向图 ,也叫弧。
度:连在这个顶点上的边有几条。
-
无向图:度 = 挨着它的边数
-
有向图:入度 (箭头指向它的)+ 出度(从它指出去的)
连通 (无向图):从任意一个顶点都能走到任意另一个顶点。强连通 (有向图):从 u 到 v、从 v 到 u 双向都能走。
生成树 :连通图的一棵"骨架"------包含全部顶点 ,但只保留 n−1 条边且不成环。带权图里权值总和最小的那棵叫最小生成树。
度的两种数法、四种存储结构、BFS/DFS 对比、拓扑与关键路径手算
一、度的计算与图的基本概念
1.1 无向图 vs 有向图,度怎么数
| 图类型 | 度的算法 |
|---|---|
| 无向图 | 邻接矩阵中该顶点对应一行的非零元素个数 |
| 有向图 | 该顶点行的非零数(出度)+ 列的非零数(入度) |
2013(7) 完整验算一遍:
| 顶点 | 行(出度) | 列(入度) | 度 |
|---|---|---|---|
| 1 | 0 0 0 1 → 1 |
0,1,0,1 → 2 |
3 |
| 2 | 1 0 1 0 → 2 |
0,0,1,1 → 2 |
4 |
| 3 | 0 1 0 0 → 1 |
0,1,0,0 → 1 |
2 |
| 4 | 1 1 0 0 → 2 |
1,0,0,0 → 1 |
3 |
2013(7) 答案:C(3, 4, 2, 3)。
1.2 握手定理:度之和 = 2 × 边数
每条边在它的两个端点上各被数一次,所以:
无向图:Σ 各顶点度 = 2|E|
有向图:Σ 入度 = Σ 出度 = |E|
2009(7) 考的是无向连通图的性质:
-
I. 所有顶点的度之和为偶数 → 对(等于 2|E|)
-
II. 边数大于顶点数减 1 → 错,树就是 |E| = |V| − 1
-
III. 至少有一个顶点的度为 1 → 错,n ≥ 3 的完全图每个顶点度都是 n−1 ≥ 2
2009(7) 答案:A(只有 I)。
2017(7) 是握手定理的代数应用:16 条边,度为 4 的顶点 3 个、度为 3 的 4 个、其余顶点度都小于 3(即最大为 2),求顶点数至少多少。
设其余顶点数为 x,且都取最大度 2(这样用的顶点最少):
4×3 + 3×4 + 2x = 16×2
24 + 2x = 32
x = 4
顶点数 = 3 + 4 + 4 = 11。
2017(7) 答案:B(11)。
(思路:要让顶点数最少,就让剩余的度尽量"摊"到少数顶点上,所以其余顶点的度都取允许的最大值 2。)
1.3 连通性:保证连通需要多少条边
2010(7) :7 个顶点,在任何情况下都连通,最少需要多少条边?
关键词是"在任何情况下"------不仅现在连通,随便你怎么挪动边都不能把它拆开。做法是:让任意 6 个顶点构成完全图,再把第 7 个顶点接上去。
C(6,2) = 6×5/2 = 15 条(6 个点两两相连)
再 + 1 条(把第 7 个点连进来)
= 16 条
为什么这样最省?反过想:如果边数 ≤ 15,就可能存在一个"孤立"的划分方式把图拆成两半。
2010(7) 答案:C(16)。
对照 2022(6) :问 |V| 和 |E| 满足什么条件时图一定不连通。答案是 D(|V| > |E| + 1)。理由:n 个顶点要连通至少需要 n−1 条边,所以 |E| < |V| − 1 时必定不连通,移项就是 |V| > |E| + 1。
2025(6) 是概念判断题,答案 C :各顶点度都 ≥ 2 的无向图必有回路。其余三项:A 错(有向环里每个顶点入度都是 1,没有入度为 0 的);B 错(拓扑序列可以多个);D 错(带权图最短路用 Dijkstra,BFS 只能处理边权都为 1 的情况)。
2011(8) :答案 C(仅 III)。I 错(回路首尾相接,顶点重复了,不是简单路径);II 错(稀疏图用邻接表省空间,邻接矩阵是 O(n²));III 对(有回路就没有拓扑序列)。
二、存储结构:邻接矩阵、邻接表、邻接多重表
| 存储 | 空间 | 适合场景 | 求度 |
|---|---|---|---|
| 邻接矩阵 | O(n²) | 稠密图 | 无向看行,有向行+列 |
| 邻接表 | O(n + e) | 稀疏图 | 有向图出度好求、入度要扫全表 |
| 邻接多重表 | --- | 无向图(每条边只存一次) | 数该顶点在边链表中出现几次 |
| 十字链表 | --- | 有向图 | 入度出度都好求 |
2012(5) :n 个顶点 e 条边、邻接表存储的有向图 ,BFS 的时间复杂度 → O(n + e)。答案 C。 记忆法:每个顶点入队一次是 O(n),每条出边被扫一次是 O(e)。
2016(7) :拓扑排序用邻接表存储,时间复杂度 → O(n + e)。答案 B。同一个结论。
2024(4) 考的是邻接多重表求度:题目给了无向图的邻接多重表,问顶点 b、d 的度。答案是 B(2, 4)。
方法:邻接多重表里一条边只对应一个边结点 ,这个结点会同时挂在这条边两个端点 的链表上。所以求某个顶点的度,就是数它在所有边链表中出现了几次。
⚠️ 说明一下:这道题的原图在题库里是缺失的,上面给的是官方答案和通用做法,没有还原具体边表。遇到同类题按"数出现次数"的方法做即可。
三、图的遍历:BFS 与 DFS
BFS(广度优先) :用队列。从一个顶点出发,先访问它所有未访问过的邻接点,再依次从这些邻接点往外扩一层。像水波一圈圈扩散。
DFS(深度优先) :用栈(或递归)。一条路走到黑,走不动了就回退一步换条路。
3.1 判定一个序列是不是 BFS/DFS 序列
这类题年年考,做法只有一个:拿选项去模拟。
2013(8) 问哪个不是 BFS 序列。判据是 BFS 的一条硬性质:
若 v 在序列中排在 u 前面,且 u、v 之间有边,那么 u 的所有未访问邻接点 必须在 v 的所有未访问邻接点之前或同层 被访问------换句话说,BFS 序列里不会"跳层回退"。
题库里给的答案是 D 。题目原图同样是缺失的,但这类题的通用判据可以用:DFS 序列经常出现"沿一条链一路走到底"的形态,BFS 序列则是一层一层铺开 。选项 D 的 a, b, c, d, h, e, f, g 是典型的一路深入,所以判定它不是 BFS。
2016(6) 同理,问哪个不是 DFS 序列,答案 D 。判据反过来:DFS 走到某个顶点后,下一个必须是它尚未访问的邻接点之一;如果选项里出现了"当前顶点的邻接点还没访问完就跳到别处",那就不可能是 DFS。
⚠️ 再次提醒:这两道题的原图在题库里都没有,上面给的是官方答案与判定原则,不是逐图推演。
3.2 DFS 序列能有多少个
2015(5) :有向图 V = {v₀, v₁, v₂, v₃},边集 <v₀,v₁>, <v₀,v₂>, <v₀,v₃>, <v₁,v₃>,从 v₀ 开始 DFS,可能的不同遍历序列个数是?
边列出清楚:v₀ → v₁、v₀ → v₂、v₀ → v₃、v₁ → v₃。
从 v₀ 出发,第一步可以走 v₁、v₂、v₃ 中的任意一个,逐个展开:
-
先走 v₁ → v₁ 只能去 v₃ → 访问 v₃ → 回退,还剩 v₂ →
v₀,v₁,v₃,v₂ -
先走 v₂ → v₂ 没有出边 → 回退,剩下 v₁、v₃ 可选
-
v₀,v₂,v₁,v₃ -
v₀,v₂,v₃,v₁(走 v₃ 时,v₃ 无出边,再走 v₁)
-
-
先走 v₃ → 无出边 → 回退,剩下 v₁、v₂
-
v₀,v₃,v₁,v₂ -
v₀,v₃,v₂,v₁
-
共 5 种。2015(5) 答案:D。
3.3 DFS 与拓扑排序的关系
2020(6) 是道很有意思的题:把递归 DFS 里"访问顶点"的语句移到退出递归前(也就是后序打印),输出的结果是什么?
答案 B:逆拓扑有序序列。
道理:DFS 递归时,先访问的顶点先被压进栈底。若存在边 <vᵢ, vⱼ>,那么 vᵢ 一定比 vⱼ 先入栈,也就比 vⱼ 后出栈 、后打印。拓扑序列要求 vᵢ 在 vⱼ 之前,反过来打印自然就是逆拓扑。
(顺带记:DFS 逆序打印 = 逆拓扑排序,这是一个常考的等价关系。)
3.4 BFS 也能求最短路径
2023(6) :无向连通图各边权都是 1,哪个算法一定能求出单源最短路径?答案是 B(仅 III,BFS)。
因为边权都为 1 时,"路径长度"就等于"经过的边数",而 BFS 天然按层扩展,第一次到达某顶点的层数就是最短距离。Prim 和 Kruskal 求的是最小生成树,不是最短路径------这两个概念每年都有人混。
四、最小生成树:Prim 与 Kruskal
Prim :从一个顶点开始,每次挑一条连接"已选集合"和"未选顶点"的权值最小的边,把新顶点拉进来。像滚雪球。
Kruskal :把所有边按权值从小到大排序,依次尝试加入,只要不成环就留下,直到选够 n−1 条。像挑便宜货。
4.1 概念判断题(2012(8))
-
Ⅰ. 最小生成树的代价唯一 → 对。树形可以不同(权值相同的边导致),但总代价一定相同。
-
Ⅱ. 权值最小的边一定出现在所有最小生成树中 → 错。若权值最小的边有多条且构成环,那么某棵生成树里必然有一条被舍弃。
-
Ⅲ. Prim 从不同顶点出发得到的生成树一定相同 → 错。N 个点构成权值全相等的环时,从不同点出发会得到不同的树。
-
Ⅳ. Prim 和 Kruskal 得到的树总不相同 → 错。当最小生成树唯一(各边权互不相同)时,两者结果相同,"总不相同"太绝对。
2012(8) 答案:A(仅 Ⅰ)。
4.2 边的选择(2020(7))
Kruskal 的做法按权值升序扫边,用并查集判断是否成环。官方答案 A :(b,f)(b,d)(a,e)(c,e)(b,e)。
⚠️ 这道题的原图在题库里是缺失的,这里只给答案与算法步骤,不逐一还原边的权值。
4.3 Prim 与 Kruskal 选边的差别(2015(6))
题目问:哪条边可能是 Kruskal 第 2 次选中、但不是 Prim(从 V₄ 开始)第 2 次选中 的边。答案 C:(V₂, V₃)。
思路:
-
Kruskal 第 1 次一定选全局权值最小 的边,第 2 次选次小的------它是全局视角
-
Prim 从 V₄ 出发,第 1 次只能选与 V₄ 相连的最便宜边 ,第 2 次只能从"已选集合的邻边"里挑------它是局部视角
所以差别就出在"全局最小"和"从 V₄ 出发能碰到的最小"未必是同一条。这类题的通用做法:分别模拟两步,把两条边都列出来再比对。
⚠️ 原图同样缺失,这里给的是官方答案与通用做法。
五、拓扑排序
规则 :每次选一个入度为 0 的顶点输出,然后删掉它和它所有的出边;重复直到没有顶点。
判据 :如果某一步找不到入度为 0 的顶点,说明图里有环,拓扑排序不存在。
5.1 序列计数(2010(8))
2010(8) 问不同拓扑序列的个数,答案是 B(3) :abced、abecd、aebcd。
数个数的通用做法:画搜索树------每一步把当前所有入度为 0 的顶点都列出来,分别分支往下走,最后数叶子数。
⚠️ 原图缺失,这里给的是官方答案的三个序列。
5.2 邻接矩阵判拓扑序列(2012(6))
2012(6) :邻接矩阵主对角线以下元素全为 0,问拓扑序列如何?
对角线以下全 0 意味着:只有 i < j 时可能有边 i → j,绝不可能有 j → i------所以图中不可能有环 ,拓扑序列一定存在。
但是否唯一?举反例:3 个顶点的邻接矩阵
0 1 1
0 0 0
0 0 0
顶点 1 指向 2 和 3,而 2、3 之间没有约束,拓扑序列可以是 1,2,3 也可以是 1,3,2,不唯一。
2012(6) 答案:C(存在,可能不唯一)。
5.3 判定某个序列是不是拓扑序列
做法:按序列顺序逐个检查,每个顶点被输出时,它的前驱必须已经全部输出过(或等价地:把它删掉后,下一个顶点的入度必须为 0)。
2014(7) 答案 D(3,1,4,2,6,5) 。解析的关键句是:"删掉 4 之后,顶点 2 和 6 的入度都为 0,此时选不同的顶点会得到不同序列"------所以答案落在以 3,1,4 开头的两项里。
2018(7) 问哪个不是 拓扑序列,答案 D(5,2,1,6,3,4) 。判定技巧很实用:看序列前两位 ------最初只有顶点 1 和 5 的入度为 0,所以前两位必然是 1,5 或 5,1。D 的第 2 位是 2,直接排除。
2021(7) 问拓扑序列个数,答案 A(1)------只有一种可能,说明每步都只有一个入度为 0 的顶点。
⚠️ 以上三题原图均缺失,给的是官方答案与判定方法。
六、最短路径:Dijkstra
算法步骤(背下来):
-
初始化:源点 dist = 0,其余 dist = ∞,集合 S = {}(已确定最短路径的点)
-
每趟从未确定 的顶点里选 dist 最小的那个,加入 S
-
用这个新加入的顶点去松弛 它的所有出边(若
dist[u] + w(u,v) < dist[v]就更新) -
重复 n−1 趟
关键性质 :Dijkstra 不能处理负权边。看到"负权"先想到 Bellman-Ford。
6.1 推演(2012(7))
题目说前两条最短路径的目标顶点依次是 b、c,问后续依次是什么。官方答案是 C(f, d, e)。
⚠️ 这道题题库里的推演表是 OCR 错位的(e 行和 f 行混在一起),我不照抄它。能确定下来的信息是:五个顶点被确定的顺序是
b(2) → c(3) → f(4) → d(5) → e(6)
距离单调递增,符合 Dijkstra"每趟取当前最小"的性质。
提速技巧(解析里给了,很实用) :这种"问后续顺序"的题不用完整推演,用排除法更快。
-
A 说下一个是 d(经由 b,长度 5),但存在经由 c 到 f 的长度 4 的路径 ------ 4 < 5,d 不可能排在 f 前面,A 排除
-
B 同理(把 e 排在第 3 位),排除
-
剩下 C、D 的差异在第 4、5 位(d、e 谁先):把 f 加入 S 后再松弛一次就能分辨
所以考场上这类题的正确打法是:先算前两趟确认自己没理解错,然后用"距离必须单调递增"去砍选项,比完整画表快得多。
6.2 dist 数组(2021(8))
2021(8) 问"求出第二条最短路径后,dist 中的内容"。答案是 C(21, 3, 14, 6)。
题库给的 dist 变化过程:
dist[26, 3, ∞, 6] → [25, 3, ∞, 6] → [21, 3, 14, 6]
第 1 条后 第 2 条后
⚠️ 注意这里有个细节容易错:dist 数组里存的是"当前已知的最短距离",每加入一个新顶点就整体松弛一次,所以第 1 条路径确定后 26 变成 25,第 2 条后 25 变成 21。不是"确定一条才更新一个位置"。
2016(8) 答案 B(5, 2, 3, 6, 4)------从顶点 1 出发依次确定的目标顶点。⚠️ 原图缺失,只给答案。
七、关键路径:AOE 网
AOE 网 (Activity On Edge):用边表示活动 、顶点表示"事件"(某个活动完成、下个活动可以开始的时刻)的有向无环图。
两个核心量:
-
ve(i) :事件 i 的最早发生时间(从源点正向推,取 max)
-
vl(i) :事件 i 的最迟发生时间(从汇点反向推,取 min)
活动的两个量:
活动 a = <i, j>,持续时间为 d
最早开始时间 e(a) = ve(i)
最迟开始时间 l(a) = vl(j) − d
时间余量 = l(a) − e(a)
关键活动 = 时间余量为 0 的活动 ;关键路径 = 全部由关键活动组成的路径 ,也就是从源点到汇点路径长度最长的那条。
7.1 概念(2020(8))
-
A. 关键路径是边数最多的路径 → 错 ,是权值之和最大
-
B. 关键路径是从原点到汇点路径长度最长的路径 → 对 ,答案 B
-
C. 增加任一关键活动时间不会延长工期 → 错,关键路径会变长
-
D. 缩短任一关键活动时间将会缩短工期 → 错 ,存在多条关键路径时,只缩短其中一条没用,另一条仍然决定工期
7.2 多关键路径(2013(9))
2013(9) 的关键结论很实用:题目算出的关键路径有三条------(b,d,c,g)、(b,d,e,h)、(b,f,h)。
要缩短工期,必须让每一条关键路径都变短 。所以正确选项里的活动必须覆盖所有关键路径:
-
A(c 和 e)→ 覆盖不到
(b,f,h)✗ -
B(d 和 e)→ 覆盖不到
(b,f,h)✗ -
C(f 和 d)→ d 覆盖前两条,f 覆盖第三条 ✓
-
D(f 和 h)→ h 只覆盖
(b,d,e,h)、(b,f,h),漏了(b,d,c,g)✗
2013(9) 答案:C。
7.3 时间余量计算(2022(7))
2022(7) 题库给了完整的 ve / vl 表,这是最值得照着练一遍的题:
| 结点 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| ve(i) 最早 | 0 | 2 | 5 | 8 | 9 | 12 |
| vl(i) 最迟 | 0 | 4 | 5 | 8 | 11 | 12 |
时间余量公式:余量 = vl(终点) − ve(起点) − 活动持续时间
c: vl(3) − ve(2) − 1 = 5 − 2 − 1 = 2
g: vl(6) − ve(3) − 1 = 12 − 5 − 1 = 6
h: vl(5) − ve(4) − 1 = 11 − 8 − 1 = 2
j: vl(6) − ve(5) − 1 = 12 − 9 − 1 = 2
2022(7) 答案:B(g,余量 6 最大)。
7.4 最早/最迟开始时间(2019(5))
2019(5) 问活动 d 的最早和最迟开始时间,答案 C(12 和 14)。
题库给的推演:d 必须等 a 和 b 都结束才能开始,所以最早开始 = 12;关键路径总长 27,结点 4 的最迟开始是 27 − 6 = 21,于是 d 的最迟开始 = 21 − 7 = 14。
⚠️ 原图缺失,这里给的是官方答案与计算口径。
八、用有向无环图表示表达式(2019(6))
2019(6) :表达式 (x+y)*((x+y)/x) 用有向无环图描述,最少需要几个顶点?答案 A(5)。
做法:
-
先把它画成一棵二叉树 :根是
*,左子树x+y,右子树(x+y)/x -
数一下不同的东西:
*、+、/、x、y→ 重头戏是(x+y)出现了两次,可以共用同一个子树 -
同样两个
x也能共用
去重之后顶点就是:*、+(同时服务左右两边)、/、x、y ------ 共 5 个。
(如果展开成树,那棵二叉树有 9 个顶点;DAG 的价值就在"公共子表达式只存一份"。)
九、时间复杂度汇总
| 算法 | 邻接矩阵 | 邻接表 |
|---|---|---|
| BFS / DFS 遍历 | O(n²) | O(n + e) |
| 拓扑排序 | O(n²) | O(n + e) |
| Prim | O(n²) | O(n log n)(堆优化) |
| Kruskal | --- | O(e log e)(排序占主导) |
| Dijkstra | O(n²) | O(n log n)(堆优化) |
只需记牢两条(2012(5)、2016(7) 都考过):
-
邻接表下,BFS / DFS / 拓扑排序 都是 O(n + e)
-
邻接矩阵下是 O(n²)(因为要扫整个矩阵找邻接点)
十、速查表(文末收藏版)
度与边
| 项目 | 结论 |
|---|---|
| 无向图度 | 邻接矩阵该行非零个数 |
| 有向图度 | 出度(行)+ 入度(列) |
| 握手定理 | Σ度 = 2 |
| n 点保证连通 | C(n−1, 2) + 1 条边 |
| 一定不连通 | |V| > |E| + 1 |
遍历与算法
| 项目 | 结论 |
|---|---|
| BFS | 队列,一层层铺开;边权全为 1 时可求最短路 |
| DFS | 栈/递归,一条路走到底 |
| DFS 逆序打印 | = 逆拓扑序列 |
| 邻接表复杂度 | BFS / DFS / 拓扑都是 O(n + e) |
| Prim | 从一点滚雪球,选"已选集合↔未选点"的最便宜边 |
| Kruskal | 边按权排序,不成环就加,选够 n−1 条 |
| 最小生成树 | 代价唯一,树形可能不唯一 |
| Dijkstra | 每趟选 dist 最小加入 S 再松弛;不能处理负权 |
| 拓扑排序 | 每步选入度 0 并删其出边;卡住 = 有环 |
| 关键路径 | 权值和最大的路径;多条时必须全部缩短才有效 |
| 时间余量 | vl(终点) − ve(起点) − 活动时长 |
真题考频(2009---2025,共 31 道)
| 考点 | 年份(题号) |
|---|---|
| 度的计算 / 握手定理 | 2009(7)、2013(7)、2017(7) |
| 连通性与边数 | 2010(7)、2022(6)、2025(6)、2011(8) |
| 存储结构与复杂度 | 2012(5)、2016(7)、2024(4) |
| BFS / DFS 遍历 | 2013(8)、2015(5)、2016(6)、2020(6)、2023(6) |
| 最小生成树 | 2012(8)、2015(6)、2020(7)、2023(6) |
| 拓扑排序 | 2010(8)、2012(6)、2014(7)、2018(7)、2021(7) |
| 最短路径 | 2012(7)、2016(8)、2021(8) |
| 关键路径 AOE | 2013(9)、2019(5)、2020(8)、2022(7) |
| DAG 表示表达式 | 2019(6) |
六个最容易错的地方
-
邻接矩阵先看成不对称------不对称就是有向图,度要行 + 列
-
Prim / Kruskal 求的是最小生成树,不是最短路径
-
关键路径是"权值和最大"不是"边数最多"
-
多条关键路径时,只缩短一条不能缩短工期
-
Dijkstra 每趟确定一个顶点后要整体松弛 dist,不是只改一个位置
-
BFS 序列不回退、DFS 序列不跳层------判定题就靠这两句话
⚠️ 关于本篇的真题说明
图论题大量依赖原图,而 2010(8)、2013(8)、2014(7)、2015(6)、2016(6)、2016(8)、2018(7)、2019(5)、2020(7)、2021(7)、2021(8)、2022(7)、2024(4) 等题目的原图在题库中缺失或为 OCR 乱码 。这些题本文只提供官方答案与通用判定方法,不做图形还原------考场上请以试卷上的图为准,方法部分可以直接复用。
