前几章的结构都是「规矩的」:线性表排成队,树是分层的。图把最后一条限制也撤了:任何东西和任何东西都可以有关系。城市间的高速公路、社交网络的好友关系、编译器的模块依赖、操作系统的进程等待图(操作系统课第 5 章的死锁检测就是在图上找环),全是图。图论算法多而杂,本章的主线只有两条:遍历(DFS/BFS,一切的起点)和最优化(最小生成树、最短路径,贪心思想的高光时刻)。先从一个真实的建模问题开始。
要给「校园导航」建模:路口是顶点,道路是带长度(权)的边。第一个决定是存法,它直接决定后面每个算法的性能:
邻接矩阵:V×V 的二维数组,g[u][v] 存边权(无边存无穷)。判断「u 到 v 有没有路」O(1),但空间 O(V²),10000 个路口的地图就是 10⁸ 格,九成九是无穷。邻接表:每个顶点挂一条出边链表(第 2 章的链表),空间 O(V+E),遍历 u 的邻居只走 u 自己的链;代价是判边要扫链表 O(deg)。
typedef struct Edge { int to, w; struct Edge *next; } Edge;
Edge *adj[MAXV]; // adj[u]: u 的出边链表头
void add_edge(int u, int v, int w) {
Edge *e = malloc(sizeof *e);
e->to = v; e->w = w; e->next = adj[u];
adj[u] = e; // 头插,O(1)
}
选法口诀按密度分:稀疏图(真实世界绝大多数,边数和顶点同量级)用邻接表;稠密图、需要频繁判边、或要用 Floyd 这类矩阵型算法时用邻接矩阵。第 11 章的并查集、第 12 章的内存视角会回到这张结构对照表。
DFS(深度优先):沿着一条路走到黑,走不动了回溯换岔路。递归实现三行:
int visited[MAXV];
void dfs(int u) {
visited[u] = 1;
for (Edge *e = adj[u]; e; e = e->next)
if (!visited[e->to]) dfs(e->to);
}
BFS(广度优先):逐层扩散,第 3 章的队列直接上场:起点入队,出队时把未访问邻居入队。两者都 O(V+E)(每顶点每边各经过一次),但「形状」完全不同,这个差异就是它们各自的饭碗:
一个工程细节:递归 DFS 在深图(比如 10 万个节点串成一条链)会爆栈(栈帧是有限的,操作系统课第 2 章),生产实现要改成显式栈的迭代版,这正是第 3 章「栈」的又一用武之地。
n 个村庄,村间候选道路各有造价,要让所有村连通且总造价最小——选出的 n-1 条边就是最小生成树(MST)。两种贪心,贪的东西不同:
Kruskal(从边下手):所有边按权排序,从小到大尝试,只要不形成环就收入,收满 n-1 条为止。「判环」靠并查集(第 11 章):一条边两端已在同一集合,加入必成环。O(E log E),稀疏图首选。
Prim(从点下手):任选起点,反复把「离已连区域最近的未连点」拉进来,「找最近」用第 5 章的堆,O(E log V),稠密图顺手。
两者的贪心正确性都靠「切分定理」背书(任何横切图的最轻边必在某棵 MST 里),第一次理解这个证明是图论入门的高光时刻,建议拿一条具体图亲手验证。真实投影:电网规划、网络布线、聚类分析的层次聚类,全是 MST 变装。
导航的核心问题。Dijkstra 管单源(一个起点到所有点),贪心策略和 Prim 神似:每次把「当前距离最小的未确定点」敲定为终点,再松弛它的邻居:
dist[] 全设 ∞,dist[源] = 0
反复:取未确定点中 dist 最小的 u(堆,第 5 章)
对 u 的每条边 (u,v,w):若 dist[u]+w < dist[v],更新 dist[v]
O(E log V)。它有个必须刻在脑子里的前提:边权非负。贪心「敲定就不改」的正确性依赖「路越走越长」,负权边会打破它(走了负边回头更便宜),那时换 Bellman-Ford 或 SPFA。
Floyd 管多源(所有点对之间),代码短到不像算法,三重循环:
for (int k = 0; k < V; k++)
for (int i = 0; i < V; i++)
for (int j = 0; j < V; j++)
if (d[i][k] + d[k][j] < d[i][j])
d[i][j] = d[i][k] + d[k][j];
含义:「允许经过前 k 个中转点的 i 到 j 最短路」逐层放宽,是动态规划(第 10 章)在图上的样板。O(V³) 的代价决定它只配小图(几百个顶点),但支持负权边、一次算清全部点对,小型城市地图、航空中转分析仍常用。
两张算法分工表收束本章:MST 问题,稀疏图 Kruskal、稠密图 Prim;最短路,单源非负 Dijkstra、全对稠密 Floyd、有负权 Bellman-Ford。它们的公共精神是贪心 + 正确性证明(MST 的切分定理、Dijkstra 的非负前提),「贪心敢不敢用」永远是「能否证明贪了不吃亏」,这句话到第 10 章贪心章会再深挖。