图的基本概念 知识点讲解

信息学竞赛(CSP-J/S、NOIP、GESP)高频考点精讲:从基础规则到推演与易错点,配真题例题。学完可一键去 OI伴学在线刷题。

去 OI伴学在线练习 →

本讲学习目标

概念

在信息学竞赛里,图(Graph) 就像一张错综复杂的“关系网”或“地图”。想象一下:

这些场景本质上都在描述“点与点之间的连接关系”。如果只用数组存数字,那是“线性”的;如果存矩阵,那是“二维”的。但现实世界往往是非线性的网状结构,这时候就需要图论登场。

为了让你直观感受图的威力,我们先看一个极小的暴力例子:

假设你有 3 个任务 A、B、C,任务 A 必须在 B 之前做,B 必须在 C 之前做。

一句话:图就是“点 + 边”。后面所有图论算法(最短路、最小生成树、拓扑、网络流)都建立在这套基本概念和存储方式之上。先把“怎么存、怎么遍历”学扎实。

触发清单(看到这些信号第一时间想到本知识点)

题干信号优先考虑什么对应思路
“关系 / 连接 / 网络 / 通路”建图定义点和边,选择邻接表或矩阵存储
“能不能互相到达 / 有几个连通块”遍历DFS 或 BFS 搜索
“必须先…才能… / 依赖”有向图 + 拓扑建边并计算入度,进行拓扑排序
“最短步数(无权)”遍历BFS(队列)
“最长链(DAG)”遍历 + 计数拓扑排序 + DP

定义

图(graph) 由两部分组成:

按边有没有方向分:

存储方式:邻接矩阵 vs 邻接表

邻接矩阵(适合稠密图,n 小):

用二维数组 g[u][v] 表示 u 到 v 的边权(若无边则设为无穷大 INF)。

int g[1005][1005]; // g[u][v] = 边权,无边设 INF
g[u][v] = w; // 无向图还要 g[v][u] = w

它的优点是查询两点间是否有直接边非常快(O(1)),但缺点是空间复杂度为 O(n²),当 n 很大时(如 10⁵)会爆内存。

邻接表(适合稀疏图,n 大,推荐):

用数组里的 vector 来存,每个点挂一条“邻边列表”。

vector<pair<int,int>> adj[1005]; // <到达点, 边权>
adj[u].push_back({v, w}); // 无向图 adj[v].push_back({u, w}) 也加

它的优点是只存真实存在的边,总空间复杂度为 O(n+m)(m 为边数),对于竞赛中常见的稀疏图(m ≈ n 量级)非常节省空间且遍历效率高。

度数与握手定理

连通性

无向图中任意两点可达 → 连通图;有向图中任意两点互相可达 → 强连通。没有环的连通图就是树(n 个点 n−1 条边)。

推演:为什么这些写法是对的(以及它们能用来干什么)

推演 1 · 为什么邻接表比邻接矩阵省空间

n 个点,邻接矩阵开 n×n 个格子,无论边多少都占 O(n²)。但竞赛图常常是稀疏的(边数 m ≈ n 量级),邻接表只存“真实存在的边”,总空间 O(n+m)。所以当边不多时用邻接表,空间和遍历都更优。稠密图(m ≈ n²)两者差不多,矩阵还方便查“两点间是否直接相连”。

用法:看到 n 很大(1e5)基本用邻接表;n ≤ 2000 且要频繁查边可用矩阵。

推演 2 · 为什么握手定理成立

每条无向边连接两个顶点,统计“所有顶点度数之和”时,这条边会被它的两个端点各计一次,所以总和恰好是边数的 2 倍。推论:无向图度数之和为偶数,且奇数度顶点个数必为偶数——判图合法性常用。

用法:握手定理能快速检查“度数数据是否自洽”,也是欧拉通路/度数类题的出发点。

推演 3 · 为什么 DFS/BFS 能不重不漏遍历全图

遍历时用 vis[] 标记“已访问”。每到一个点,沿它的邻边走到未访问的邻居;vis 保证每个点只进一次。DFS 用递归/栈往深处走(一条路走到底再回溯),BFS 用队列一层层扩散(先近后远)。两者都保证在 O(n+m) 内访问完连通块内所有点。

用法:判连通、数连通块、求最短路(BFS 用于无权图)、搜路径,全靠这套遍历。

技巧速查表(背下来)

想做的事写法说明
存稠密图 (n≤2000)int g[N][N]查询 O(1),但占 O(n²) 空间
存稀疏图 (n 大)vector<pair> adj[N]只存真实边,O(n+m) 空间,遍历快
无向边建图adj[u].pb({v,w}); adj[v].pb({u,w});必须两边都加,否则单向
有向边建图adj[u].pb({v,w});只加一次,方向即依赖
判连通块数for(i=1..n) if(!vis[i]) dfs(i), cnt++每次从新点出发 DFS 就是一个块
无权最短路BFS 队列层层扩散,保证第一次访问即最短
拓扑排序队列存入度 0 点,删边更新入度有环则无法排完所有点
度数统计deg[u] = adj[u].size() (无向)有向图需单独算入度出度
奇数度点个数统计后对 2 取模应为 0握手定理推论
矩阵下标开到 n+1题目常从 1 编号,0 号会越界

例题

例 1:统计无向图有多少个连通块

① 题目描述

给定一个无向图,包含 $n$ 个点和 $m$ 条边。请计算该图中共有多少个连通块。

② 审题分析

③ 解题思路

1. 使用邻接表存储图(因为通常图是稀疏的)。

2. 初始化 vis 数组全为 false。

3. 从 1 到 $n$ 遍历每个点:

- 如果 vis[i] 为 false,说明这是一个新连通块的起点。

- 调用 dfs(i) 将该连通块内所有点标记为 true。

- 计数器 blocks 加 1。

4. 输出 blocks。

④ C++ 完整参考代码

#include <iostream>
#include <vector>
using namespace std;

// 全局变量:邻接表和访问标记数组
// 使用 vector<int> 存无向图的邻居,bool vis 标记是否访问过
vector<int> adj[1005];
bool vis[1005];

// DFS 函数:从点 u 开始,递归访问所有能到达的点
void dfs(int u) {
 vis[u] = true; // 标记 u 已访问,防止重复进入
 for (int v : adj[u]) { // 遍历 u 的所有邻居
 if (!vis[v]) { // 如果邻居 v 还没被访问过
 dfs(v); // 递归进入 v,继续深搜
 }
 }
}

int main() {
 // 优化输入输出效率
 ios::sync_with_stdio(false);
 cin.tie(nullptr);

 int n, m;
 if (!(cin >> n >> m)) return 0; // 读取点数 n 和边数 m

 // 读入 m 条边
 for (int i = 0; i < m; i++) {
 int u, v;
 cin >> u >> v;
 // 无向图:边 (u, v) 意味着 u 能去 v,v 也能去 u
 // 必须两边都加,否则只能单向走,连通块会算错
 adj[u].push_back(v);
 adj[v].push_back(u);
 }

 int blocks = 0;
 // 遍历所有点,寻找未访问的点作为新连通块的入口
 for (int i = 1; i <= n; i++) {
 if (!vis[i]) {
 dfs(i); // 启动 DFS 标记整个连通块
 blocks++; // 发现了一个新连通块
 }
 }

 cout << blocks << '\n';
 return 0;
}

⑤ 代码解读(含真实运行输出)

假设输入如下:

6 5
1 2
2 3
4 5
5 6
3 4

这是一个由两个连通块组成的图:1-2-3-4-5-6 实际上是一条链,但题目给的边是 1-2, 2-3, 4-5, 5-6, 3-4,连起来是 1-2-3-4-5-6 一个连通块。等等,我重新构造一个有两个连通块的例子以便演示:

6 4
1 2
2 3
4 5
5 6

这里 1-2-3 是一个块,4-5-6 是另一个块。

运行结果:

2

解读:

图 1 同一个无向图:左为邻接矩阵(稠密),右为邻接表(只存真实边,省空间)

例题复盘总结

归纳题:

共性特征:

各自技巧:

审题模板(固定思考流程):

1. 点是谁、边是谁、有向还是无向? → 决定建图方式(邻接表/矩阵,加边方向)。

2. 要遍历/判连通? → 选 DFS 或 BFS。

3. 有依赖顺序? → 拓扑排序。

4. 求最短步数(无权)? → BFS。

5. 求最长链(DAG)? → 拓扑 + DP。

错误清单(特有):

写法总表

技法分层解决什么典型写法
底层存储把图放进程序邻接矩阵 g[N][N] 或 邻接表 vector<pair> adj[N]
基础遍历访问所有点/判连通DFS 递归/栈,BFS 队列,配合 vis[]
度数/拓扑有向图顺序/依赖入度/出度统计,Kahn 算法拓扑排序
路径计数DAG 上最长链/路径数拓扑排序后 DP 累加
最短路无权图最短步数BFS 层层扩散

分工说明:

常见错误

1. 无向图只加一条边:无向边要 adj[u].push_back(v) 和 adj[v].push_back(u) 都加,否则只能单向走。

2. 矩阵大小开错:点编号 1..n,数组要开到 n+1;g[u][v] 下标超界会 RE。

3. 遍历忘打 vis:同一点反复进,死循环或 TLE;每个点只应进一次。

4. 把有向当无向:有向边只加 adj[u].push_back(v),反向不可达,别顺手加两次。

5. 度数统计漏边:无向图一条边贡献两个端点的度数,别只算一端。

6. 拓扑前没算入度 / 入队条件错:拓扑排序要先把所有入度为 0 的点入队,每次删边后更新入度。

7. 混淆 DFS 与 BFS 用途:求无权最短步数用 BFS(队列),DFS 只保证访问不保证“最近”。

自测

1. 邻接矩阵和邻接表分别在什么情况下更合适?空间复杂度各是多少?

2. 无向图握手定理是什么?它能推出什么关于“奇数度顶点个数”的结论?

3. 无向图建图时为什么一条边要加两次?有向图呢?

4. DFS 和 BFS 各用什么结构实现?分别适合解决什么问题?

5. 遍历图时 vis 数组的作用是什么?不写会怎样?

6. 有向图的入度和出度分别怎么定义?拓扑排序为什么要看入度?

7. 给一个 n 点 m 边的稀疏图(m≈n),你会选哪种存储?为什么?

举一反三 · 审题流水线

第一步:定模型。看到“关系/连接/网络/通路”,立刻在脑海里画点连线。确认是无向还是单向(“依赖”、“必须在前”就是有向)。

第二步:选存储。点数 $n$ 是多少?边数 $m$ 是多少?若 $n$ 很大(如 $10^5$)或 $m \ll n^2$,直接上邻接表;若 $n$ 很小(如 $200$)且要频繁查两点是否相连,考虑邻接矩阵。

第三步:定操作。

第四步:防坑。无向边两边加!矩阵下标开 n+1!拓扑前初始化入度为 0 的点入队!

练手题

题 A:无向图的连通块计数

给定 $n$ 个点和 $m$ 条边,求连通块数量。

输入:第一行 $n, m$;接下来 $m$ 行每行 $u, v$。

输出:一个整数。

题 B:有向图的拓扑排序

给定 $n$ 个点和 $m$ 条有向边,表示任务依赖关系。若存在合法的执行顺序,输出按拓扑序排列的点编号(任意一种合法顺序均可);否则输出 -1。

输入:第一行 $n, m$;接下来 $m$ 行每行 $u, v$。

输出:一行 $n$ 个整数或 -1。

学完这个考点,用历年真题检验一下

OI伴学 把「图的基本概念」在 CCF-GESP / CSP / NOIP 真题里出现过的题目汇总到一页,可按考点横向刷;每题都带答案、解题思维和常见错误。

看 图的基本概念 历年真题汇总 → 去平台刷题 →