← 目录 / 算法文档 · 模块十五 图论基础 / 15.1 图的存储

15.1 图的存储

树只是图的一种特殊情况——去掉"不能有环、必须连通"这些限制,就得到了更一般的图。存好图,是后面最短路、生成树这些算法的第一步。

本页目录
① 从树到图:更一般的结构

11.1 节讲过,树是"连通、没有环"的一种特殊结构——任意两个节点之间只有一条唯一的路径。如果去掉这些限制:允许存在环,允许一个节点连着好几条能到达同一个地方的路,甚至允许某些节点之间根本无法互相到达——就得到了更一般的结构:图(Graph)。图论要研究的问题(怎么找最短路、怎么用最少的边连通所有点……)都是建立在"图存好了"这个前提之上的,本节就是图论的第一课:怎么把一张图存进程序里。

② 图的基本概念

一张图由顶点(节点)组成,记作 G = (V, E)。围绕"边"有几组基本分类,贯穿整个图论部分:

分类含义
无向图 / 有向图无向图的边没有方向(A-B 和 B-A 是同一条边);有向图的边有方向(A→B 不代表 B→A 也存在)
带权图 / 无权图带权图的每条边有一个数值(比如距离、代价);无权图的边只表示"连通",没有额外数值
简单图没有自环(自己连自己)、没有重边(两点之间不止一条边)的图;本节的例子都是简单图

本节统一用这张无向带权图做例子(4 个顶点,5 条边):

示例图(顶点 A B C D,边上数字是权值)
4 1 2 5 8 A B C D
5 条边:A-B(4)A-C(1)B-C(2)B-D(5)C-D(8)。下面用两种不同的方式把这张图存进程序里。
③ 存储方式一:邻接矩阵

邻接矩阵:开一个 V×V 的二维数组 gg[i][j] 直接存"顶点 i 到顶点 j 这条边的权值";如果两点之间没有边,就存一个"不可能出现的数"(通常用一个很大的数表示,记作 INF,代表"不可达")。

示例图的邻接矩阵
ABCD
A041INF
B4025
C1208
DINF580
因为是无向图,矩阵沿对角线左右对称——g[A][B]g[B][A] 存的是同一条边,值相等。对角线上 g[i][i]=0 表示"自己到自己"距离为 0;A-D 之间没有直接的边,存 INF,而不是 00 会被误认为"存在一条权值为 0 的边")。
C++ · 邻接矩阵
1const int INF = 0x3f3f3f3f;
2int g[MAXN][MAXN]; // g[i][j]:i 到 j 这条边的权值,INF 表示没有边
3
4void InitGraph(int n)
5{
6 for (int i = 1; i <= n; i++)
7 for (int j = 1; j <= n; j++)
8 g[i][j] = (i == j) ? 0 : INF; // ★ 默认全部设成 INF(不可达),对角线是 0
9}
10
11void AddEdge(int u, int v, int w)
12{
13 g[u][v] = w;
14 g[v][u] = w; // 无向图:两个方向都要存;有向图只存 g[u][v] 这一个方向
15}
④ 存储方式二:邻接表

邻接表:给每个顶点开一个"列表",只记录它真正连着的那些邻居(以及边权),而不是像矩阵那样把"所有顶点两两之间"的关系都存一遍。C++ 里最方便的写法是每个顶点对应一个 vector,存"邻居编号 + 边权"这一对信息。

示例图的邻接表
A
(B, 4)
(C, 1)
B
(A, 4)
(C, 2)
(D, 5)
C
(A, 1)
(B, 2)
(D, 8)
D
(B, 5)
(C, 8)
每个顶点只列出真正相连的邻居——A 只存了 (B,4)(C,1) 两项,完全不需要提到"A 和 D 没有边"这件事。这正是邻接表比邻接矩阵省空间的原因:矩阵会把"没有边"的关系也存一遍(存成 INF),邻接表则压根不提。
C++ · 邻接表
1vector<pair<int,int>> adj[MAXN]; // adj[u] 里的每一项 (v, w) 表示 u 到 v 有一条权值为 w 的边
2
3void AddEdge(int u, int v, int w)
4{
5 adj[u].push_back({v, w});
6 adj[v].push_back({u, w}); // 无向图:两个方向各存一次;有向图只存这一行
7}
8
9// 遍历顶点 u 的所有邻居:
10for (auto [v, w] : adj[u])
11 cout << "邻居 " << v << ",边权 " << w << endl;
⑤ 两种存储方式怎么选

关键看图稀疏还是稠密——顶点数是 V,边数是 E

对比项邻接矩阵邻接表
空间开销O(V²),不管边多边少都要开这么大O(V+E),只存真正存在的边
查询"u、v 之间有没有边"O(1),直接看 g[u][v]O(度数),要遍历 u 的邻居列表
遍历"u 的所有邻居"O(V),要扫一整行(哪怕大部分是 INF)O(度数),只遍历真正的邻居,不浪费
适合场景顶点数较少、边很稠密(接近 V² 条边)顶点数较多、边比较稀疏(远小于 V² 条边)
💡
竞赛中的经验法则:大多数题目给出的图都是稀疏图(边数和点数是同一个数量级,而不是点数的平方),邻接表几乎总是更省空间、更高效的选择。邻接矩阵的优势主要在于"query 单条边是否存在"特别方便,以及后面 15.2 节 Floyd 算法这种本身就需要用矩阵来表达"任意两点间距离"的场景——两种存储方式并不是谁淘汰谁,而是配合不同算法各自发挥所长。
⑥ 常见陷阱
无向图只存了一个方向:无向图的一条边 A-B 其实包含"A 能到 B"和"B 能到 A"两条信息,邻接矩阵要设置 g[A][B]g[B][A] 两个位置,邻接表要往 adj[A]adj[B] 各插入一次——只存一个方向,会导致后续算法(比如从 B 出发做 BFS/DFS)漏掉这条边,图变成"看起来"是有向的。
用 0 表示"没有边",和"边权恰好是 0"混淆:邻接矩阵初始化时,"没有边"应该用一个远大于所有可能边权之和的数(INF)表示,而不是 0——如果题目里真的存在权值为 0 的边,用 0 表示"不可达"会让程序把这条边误判成"没有这条边",或者反过来把"不可达"误判成"有一条权值为 0 的边",后续最短路算法全盘出错。
重边、自环没有特殊处理:如果题目的图存在重边(两点之间有多条边)或自环(自己连自己),邻接矩阵默认写法会用"最后一次赋值"覆盖之前的边权——如果题目要求"取重边中权值最小的那条",需要在 AddEdge 里改成 g[u][v] = min(g[u][v], w);邻接表天然支持重边(每条边都是列表里单独的一项),但涉及自环时要注意部分算法(比如后面的最小生成树)可能需要提前把自环过滤掉。
🏆
接下来:本节的两种存储方式会贯穿整个图论基础模块——15.2 节的 Floyd 算法直接在邻接矩阵上做三重循环;15.3 节的 Dijkstra、15.5 节的拓扑排序则更常搭配邻接表使用。选对存储方式,是写对图论算法的第一步。