专题:图的存储与第一次遍历 —— 先把”图”变成数组,再走通它

本讲定位:图论系列第一讲,全系列的地基。这一讲解决两件事:题目里画的那堆圈圈和连线怎么放进 C++ 的数组里,以及怎么把它完整地走一遍。之所以把”存”和”走”放在一讲,是因为它们咬得太紧——存法的写法直接决定遍历的写法,而学完存图却不会遍历,一道题也做不了。本讲用实测数据回答三个学生总是含糊过去的问题:邻接矩阵到底 $n$ 多大就存不下(实测 $n=10^4$ 的 int 矩阵要 381 MB)、vector 邻接表和链式前向星到底谁快(实测建图前向星快 8 倍,遍历 vector 快 29 倍,与流传的”前向星更快”并不一致)、前向星省的那点空间值不值(实测 $n=10^5,m=10^6$ 带权时 23.8 MB vs 29.3 MB)。文中所有代码经 g++ -O2 -std=c++14 -Wall 编译运行,三种存法互相交叉验证 3000 组随机图,B3643 与 P5318 两道例题与洛谷官方样例逐字符核对。

前置知识:数组、二维数组、结构体、vector 的 push_back 与 size、递归函数、queue 的 push/front/pop/empty(见顶层 06-linear-structures.md)。不需要任何图论基础,本讲从”什么是点、什么是边”开始。

适合对象:CSP-J 复赛备考起步。本讲全部内容标 J,是后面 15 讲的公共前提,没有可跳过的部分。第 4 节的链式前向星如果第一遍看不懂,可以先只用 vector 邻接表写题,等到学第 06 讲负环判定或番外 B 网络流时再回来补。


1. 从一道题说起:选错存法,一行没写错也会挂

P5318【深基18.例3】查找文献:给一个 $n$ 个点、$m$ 条边的有向图($n \le 10^5$,$m \le 10^6$),从 1 号点出发,分别输出字典序最小的 DFS 遍历序和 BFS 遍历序。

先不管算法。第一个问题是:这 $10^5$ 个点、$10^6$ 条边,你打算存在哪?

初学者的第一反应几乎总是二维数组:

bool g[100005][100005];      // "第 i 行第 j 列表示 i 到 j 有没有边"

这一行代码在思路上完全正确,但它需要 $10^5 \times 10^5 = 10^{10}$ 个 bool,也就是 9.3 GB。洛谷的内存限制通常是 128 MB 或 256 MB。这份代码不会 WA,会编译都过不了(数组太大)或者直接 MLE——你连”我的算法对不对”这个问题都还没机会问。

而如果换成下面这行,同一张图只要 16.9 MB(实测值,见 5.3):

vector<int> g[100005];       // g[i] 存 i 的所有出边终点

这就是本讲前半部分存在的理由:图论题的第一个决策点不是选算法,是选存法;而选存法的唯一依据是数据范围。这个决策做错,后面写得再对也没用。

存好之后呢?这道题要求”遍历序”——存进去只是第一步,把它走一遍才有意义。所以本讲后半(第 7 节)会给出 DFS 和 BFS 两份最简模板,第 8.2 节回过头把这道题完整做掉。


2. 术语:把题面翻译成图的三步

2.1 最小术语表

术语 含义 在题面里长什么样
点(顶点,vertex/node) 图里的一个个体 城市、路口、文章、人、棋盘上的格子、程序的一个状态
边(edge) 两个点之间的一条关系 道路、航线、“A 引用了 B”、“A 认识 B”、一步操作
$n$ / $m$ 点数 / 边数 几乎所有图论题的第一行输入
有向边 只能从 $u$ 走到 $v$ “单行道”、“A 引用 B”、“先修课”、“A 打赢了 B”
无向边 两个方向都能走 “双向道路”、“A 和 B 是朋友”、“用铁路相连”
边权(weight) 边上带的一个数 距离、花费、时间、容量
度(degree) 与该点相连的边数 无向图:$deg(u)$;有向图分出度与入度
自环(self-loop) 起点终点是同一个点的边 $u = v$ 的输入
重边(multi-edge) 两点之间有多条边 同一对 $(u,v)$ 输入了多次
简单图 无自环、无重边 题面常明说”无重边无自环”
路径 一串首尾相接的边 “从 1 走到 $n$”
连通(无向图) $u$ 能走到 $v$ “任意两个城市都能互相到达”
连通块(连通分量) 极大的互相连通的一坨点 “有几片湖”“有几个细胞”“分成几个团”

2.2 读题时提取三个要素

拿到一道疑似图论题,按顺序问自己三个问题,答案就是建图方案:

  1. 谁是点? —— 题目里”有编号的那类东西”通常就是点。注意点不一定是题面直接给的实体:网格图里每个格子是点(第 7.3 节),八数码里每个局面是点(第 03 讲)。