本章介绍图的存储方式。
边集
方法
使用一个数组来存储边,数组中每个元素都包含一条边的起点和终点(带边权的图还包括边权)。
(或者使用多个数组来分别存储起点、终点和边权)。
复杂度
- 查询是否存在某条边:𝑂(𝑚)
- 遍历一个点的所有出边:𝑂(𝑚)
- 遍历整张图:𝑂(𝑛𝑚)
- 空间复杂度:𝑂(𝑚)
应用
由于直接存边的遍历效率低下,一般不用于遍历图.
在 Kruskal 算法 中,由于需要将边按边权排序,需要直接存边.
在有的题目中,需要多次建图(如建一遍原图,建一遍反图),此时既可以使用多个其它数据结构来同时存储多张图,也可以将边直接存下来,需要重新建图时利用直接存下的边来建图.
邻接矩阵
TODO
邻接表
链式前向星(静态邻接表)
链式前向星是一种利用数组模拟链表来实现的图存储结构,也常被称为“静态邻接表”。它完美结合了邻接表(节省空间、方便遍历出边)和数组(内存连续、常数极小)的优点,是算法竞赛和图论编程中最常用的建图方式之一。
1. 核心数据结构
链式前向星由一个边计数器和若干数组组成。理解关键在于分清下标:head 按节点索引,其余按边索引。
cnt(边计数器):当前边数,用于分配下一条边的编号,通常从0起。
| 数组 | 下标 | 存值 | 作用 |
|---|---|---|---|
head |
节点 u |
边编号 i |
节点 u 出边链表的入口(桥接到 to/next) |
to |
边 i |
节点 v |
第 i 条边的终点 |
next |
边 i |
边编号 j |
当前边 i(以 head[u] 为共同起点)的下一条边 |
weight |
边 i |
边权 | 第 i 条边的权重(可选) |
注:
u可从1开始,故head大小至少为N + 1(竞赛常取N + 5防越界,如MAXN = 10005)。注:节点编号若是巨大离散数(如
1000000007)或字符串(如"Beijing"),不能直接作数组下标,需先离散化:
- 用哈希表(如
std::unordered_map<string, int>)给每个新节点分配连续整数 ID(从0或1起);- 用该 ID 作为
u,再代入head建图。
2. 举例说明
我们以一个包含 4个节点、5条边 的例子,通过图解直观地“看”到链式前向星在内存中的样子和逻辑上的链表样子。
1. 逻辑上的图
这是我们人类大脑中理解的图结构(括号内为边的编号 cnt):
{ rank=same; 1 }
{ rank=same; 2; 3 }
{ rank=same; 4 }
1 -> 2 [label=<边0>];
2 -> 3 [label=<边1>];
1 -> 3 [label=<边2>];
3 -> 4 [label=<边3>];
1 -> 4 [label=<边4>];
}
2. 内存中的物理结构(数组存储)
在计算机内存中,它们是三排紧挨着的格子。我们把相同下标(边编号)的格子对齐:
起点的头指针(head):
1 | 节点 u: [ 1 ] [ 2 ] [ 3 ] [ 4 ] |
边的属性(平行数组 to, weight, next):
1 | 边编号 i: [ 0 ] [ 1 ] [ 2 ] [ 3 ] [ 4 ] |
3. 链式前向星的灵魂:指针跳转图解
这是最核心的部分。我们将 head 数组作为入口,next 数组作为指针,把上面的物理内存在逻辑上连成多条单链表。
每一个方块代表一条边,格式为 [ 边编号 | 终点 to | 权重 w | 下一条边 next ]。
🔹 节点 1 的出边链表(倒序连接)
当你访问 head[1] 时,顺藤摸瓜的轨迹如下:
1 | +-------------+ +-------------+ +-------------+ |
你可以看到,由于是“头插法”,最后输入的“边4”排在链表最前面,它指向“边2”,然后指向最早输入的“边0”。
🔹 节点 2 的出边链表
1 | +-------------+ |
🔹 节点 3 的出边链表
1 | +-------------+ |
🔹 节点 4 的出边链表
1 | head[4] |
4. 建图过程
核心代码
1 | // head[u] 和 cnt 的初始值都为 -1 |
参考代码
1 |
|