Appearance
连通分量计数:DFS 数一遍
本文属于「408 算法拓展训练」——考纲内、真题代码题尚未考过的经典题。连通分量是「图的基本概念」这一考点里最常在选择题里出现的概念之一,DFS/BFS 遍历图则是算法大题里反复用到的基本功。它是经典训练题,不是真题,也不押题。
题目
给定一个含 n 个顶点的无向图 G(邻接矩阵或邻接表存储),设计算法求出 G 中连通分量的个数。
题面同样没有出现「尽可能高效」。按拓展训练导语的分类,这道题属于「直观即满分」型——直白的 DFS 遍历就是标准答案,力气花在把遍历写全、边界写对上。
直观解法:每个未访问顶点起一次 DFS
连通分量的定义就是「极大连通子图」。一次 DFS 从某个顶点出发,能走到的所有顶点都属于同一个连通分量。于是思路很直接:维护一个 visited 数组,依次检查每个顶点,遇到没访问过的就说明发现了一个新的连通分量,对它发起一次 DFS 把整块标记完,分量计数加一;直到所有顶点都被访问过。
c
#define MAXV 100
typedef struct {
int numVertices;
int Edge[MAXV][MAXV]; // 无向图邻接矩阵,Edge[i][j]==1 表示 i-j 之间有边
} MGraph;
void DFS(MGraph G, int v, int visited[]) {
visited[v] = 1;
for (int j = 0; j < G.numVertices; j++)
if (G.Edge[v][j] && !visited[j])
DFS(G, j, visited);
}
int countComponents(MGraph G) {
int n = G.numVertices;
int visited[MAXV] = {0};
int count = 0;
for (int v = 0; v < n; v++) { // 外层遍历所有顶点,一个不漏
if (!visited[v]) {
DFS(G, v, visited); // 从 v 出发标记整个连通块
count++; // 起了一次 DFS,就多一个连通分量
}
}
return count;
}拿一个 6 顶点的图走一遍:{0,1,2} 三点互连、{3,4} 一条边、5 是孤立点。外层从 v=0 开始,DFS 把 0、1、2 全标记,count=1;v=1、v=2 已访问过跳过;v=3 未访问,DFS 把 3、4 标记,count=2;v=5 未访问,DFS 只标记它自己,count=3。最终返回 3,和直觉一致。
复杂度:邻接矩阵下,所有 DFS 调用加起来恰好把
为什么它就是满分解
用总纲的「找浪费三问」审一遍:
- 有没有重复扫描? 没有。visited 数组保证每个顶点只被标记一次、每条边只沿着遍历方向经过一次,不会有顶点被 DFS 进两次。
- 有没有重算已知的东西? 没有。一个顶点一旦被标记,后续外层循环直接跳过,不会对它再发起一次 DFS。
- 有没有放着性质不用? 没有。题目没给任何额外结构(有序、稀疏之类),邻接矩阵/邻接表已经是最基本的存储形式。
更根本的一条:要确认每个顶点属于哪个连通分量,至少要把图完整遍历一遍——邻接矩阵下这是
真正的难点在这里
分不在优化上,在三处容易漏的细节:
- visited 数组必须全局共享,不能在 DFS 里重新声明。 如果把 visited 定义成 DFS 函数的局部变量,每次递归调用都会拿到一份「干净」的数组,不仅起不到标记作用,还会造成无穷递归。visited 要在 countComponents 里定义一次,整个遍历过程共用同一份。
- 外层循环必须扫过所有顶点,一个不能漏。 图不一定连通,漏掉某个顶点的检查,就会漏数一个连通分量——尤其是孤立顶点(没有任何边的顶点),它自己就是一个独立的连通分量,DFS 对它调用一次、只标记它自己,同样要计入 count。
- 别在同一个连通块里重复计数。 count++ 只应该发生在「外层循环发现一个未访问顶点」的那一刻,DFS 递归过程本身不能触发计数——递归调用只负责标记,不负责计数,这两件事分给了两处不同的代码,混在一起写容易多算。
DFS 数连通分量,看一遍
用题面描述的 6 顶点图搭一个例子:0-1、1-2 构成一个三角块的两条边,3-4 是另一条独立边,顶点 5 完全孤立。演示外层依次检查顶点、DFS 标记连通块,最终数出 3 个连通分量:
演示为 JavaScript 版本,逻辑与上文 C 代码一一对应。
别画蛇添足
- 别为了「优化」换成并查集。 并查集在处理动态加边(边一条条给出、随时要判断连通性)时确实比反复 DFS 更划算,但这道题的图是一次性给定的静态图,DFS/BFS 遍历一遍已经是下界,换成并查集不会更快,只会多写一套 find/union。
- 别对每个顶点单独发起一次全图 DFS。 如果不用 visited 数组去重,而是「对每个顶点都从头 DFS 一遍、数它能到达多少点」,同一个连通分量会被反复遍历 n 次,退化成
——纯属浪费。
下一步
- 看 DFS 遍历图的交互演示:DFS 可视化
- 回到方法论源头:「暴力解到最优解」真题专题总纲
相关真题(1题)
> **总思路**:题面看似考网络,其实考的是**图的抽象 + 邻接表设计 + Dijkstra**——把 4 个路由器和 4 个直连子网当作图的顶点,链路当作