省份数量
给定一个 $n \times n$ 的矩阵 isConnected,其中 isConnected[i][j] = 1 表示城市 $i$ 和城市 $j$ 直接相连,为 $0$ 则不直接相连。如果城市 $a$ 与 $b$ 相连,$b$ 与 $c$ 相连,则 $a$ 与 $c$ 间接相连,这些相互直接或间接相连的城市共同组成一个“省份”。你需要返回矩阵中省份(独立的连通城市区域)的总数量。
LeetCode2分钟阅读

题目核心是求无向图中的连通分量个数。每个城市看作图中的一个节点,如果两个城市相连(isConnected[i][j] == 1),则它们属于同一个连通分量(省份)。
常见的解题思路有以下三种:
图中的每个城市可以看作一个节点,若 isConnected[i][j] == 1,则表示城市 i 和城市 j 之间存在一条无向边。DFS 的核心在于“顺藤摸瓜”,尽量深地搜索图的分支。
- 维护访问状态:
- 维护一个布尔数组
visited(大小为 ),记录每个城市是否已经被访问过,防止重复计算和陷入死循环。
- 维护一个布尔数组
- 遍历所有节点:
- 依次遍历每一个城市 ()。
- 如果城市 未被访问过,说明它属于一个新的省份:
- 将省份计数器
count加 1。 - 以城市 为起点发起一次 DFS 递归搜索,将与城市 直接或间接相连的所有城市都在
visited中标记为true(即把这整块连通区域一次性吃透)。
- 将省份计数器
- 递归搜索(DFS 过程):
- 在对城市 进行 DFS 时,遍历所有可能的城市 。
- 若
isConnected[i][j] == 1(二者相连)且visited[j]为false( 未被访问):- 将 标记为已访问
visited[j] = true。 - 递归调用 DFS,继续搜索以 为起点相连的其他城市。
- 将 标记为已访问
- 返回结果:
- 主循环遍历完成后,所有的连通块都被识别出来,此时
count的值即为省份数量。
- 主循环遍历完成后,所有的连通块都被识别出来,此时
#include <vector>using namespace std;
class Solution {private: // DFS 辅助函数,递归标记与当前城市相连的所有城市 void dfs(const vector<vector<int>>& isConnected, vector<bool>& visited, int i) { // 将当前城市标记为已访问 visited[i] = true;
// 遍历所有可能的城市 j for (int j = 0; j < isConnected.size(); ++j) { // 如果城市 i 和城市 j 相连,且城市 j 尚未访问过,则继续递归 DFS if (isConnected[i][j] == 1 && !visited[j]) { dfs(isConnected, visited, j); } } }
public: int findCircleNum(vector<vector<int>>& isConnected) { int n = isConnected.size(); vector<bool> visited(n, false); // 记录每个城市是否被访问 int provinceCount = 0; // 省份数量计数器
// 遍历每一个城市 for (int i = 0; i < n; ++i) { // 如果城市 i 还没被访问过,说明找到了一个新的省份 if (!visited[i]) { provinceCount++; dfs(isConnected, visited, i); // DFS 标记出该省份包含的所有城市 } }
return provinceCount; }};- 时间复杂度:,其中 是城市的数量。我们需要遍历每个节点一次,而在 DFS 内部,对于每个节点都需要检查它与其他 个节点是否有边连接,因此总时间复杂度为 。
- 空间复杂度:,主要是
visited数组的大小为 ,以及递归调用栈的最大深度最坏情况下为 (图退化为一条链时)。
BFS 的思路可以类比为“水滴涟漪”:从一个未访问的城市(起点)开始,先将其标记并放入队列,然后不断从队列中取出城市,将其所有直接相连的未访问城市都加入队列,直到该省份内的所有城市都被连带遍历完毕。
- 状态维护:
- 布尔数组
visited(大小为 ):记录城市是否已被搜索过。 - 队列
queue:用于暂存待处理的城市节点。
- 布尔数组
- 主流程遍历:
- 依次遍历每个城市 ()。
- 如果城市 未被访问过:
- 说明找到了一个新的连通块(省份),省份计数
count加 1。 - 将 标记为已访问
visited[i] = true,并将 入队,开始 BFS 拓展。
- 说明找到了一个新的连通块(省份),省份计数
- 队列拓展(BFS 过程):
- 当队列不为空时,弹出队头城市 。
- 遍历所有可能与 相连的城市 。
- 若
isConnected[curr][j] == 1且visited[j]为false:- 将城市 标记为已访问
visited[j] = true(注意:入队时立即标记,防止重复入队)。 - 将城市 压入队列。
- 将城市 标记为已访问
- 重复上述过程,直到队列清空,表示当前省份的所有城市已全部找到。
- 返回结果:
- 遍历完所有城市后,返回累加的省份数量。
#include <vector>#include <queue>using namespace std;
class Solution {public: int findCircleNum(vector<vector<int>>& isConnected) { int n = isConnected.size(); vector<bool> visited(n, false); // 记录城市是否被访问 int provinceCount = 0; // 省份数量 queue<int> q; // BFS 队列
for (int i = 0; i < n; ++i) { // 发现未访问的城市,说明存在一个新的省份 if (!visited[i]) { provinceCount++; visited[i] = true; q.push(i); // 将起点放入队列
// 开始 BFS 遍历当前省份包含的所有城市 while (!q.empty()) { int curr = q.front(); q.pop();
for (int j = 0; j < n; ++j) { // 如果 curr 和 j 相连,且 j 尚未访问 if (isConnected[curr][j] == 1 && !visited[j]) { visited[j] = true; // 标记已访问 q.push(j); // 入队继续拓展 } } } } }
return provinceCount; }};- 时间复杂度:,其中 为城市数量。图中的每个节点最多入队和出队一次,处理每个节点时都需要扫描长度为 的整行邻接矩阵。
- 空间复杂度:,主要由
visited数组和辅助队列q产生,队列中最多同时保存 个节点。
在这道题中,矩阵 isConnected 中的每个城市代表一个节点。并查集的核心思想是通过“合并”与“查找”操作,把所有连通的城市放到同一个集合(省份)中。
- 初始化:
- 假设有 个城市,初始时每个城市各自独立,为一个单独的集合。
- 建立父节点数组
parent,初始时parent[i] = i,表示每个节点的根节点就是自己。 - 初始化省份总数为 。
- 合并集合(Union):
- 遍历邻接矩阵
isConnected的上三角部分(因为矩阵关于对角线对称)。 - 若
isConnected[i][j] == 1,说明城市 和 相连。 - 分别寻找 和 的根节点:
- 如果两者的根节点不同,说明它们原本属于不同省份,此时将两个集合合并(例如将 的根节点指向 的根节点),并将总省份数 减 1。
- 如果根节点相同,说明它们已经在同一个省份中,无需操作。
- 遍历邻接矩阵
- 查找根节点(Find + 路径压缩):
- 顺着
parent数组向上寻找节点的最终根节点。 - 为了防止树退化成链表,在查找过程中顺便进行路径压缩(即在递归寻找根节点时,将当前节点直接指向根节点),将查找的时间复杂度降至近乎 。
- 顺着
- 返回结果:
- 遍历完所有边后,最终剩余的独立集合数量即为省份数量。
#include <vector>using namespace std;
class UnionFind {private: vector<int> parent; // 记录每个节点的父节点 int count; // 记录连通分量(省份)的数量
public: // 初始化并查集 UnionFind(int n) { count = n; parent.resize(n); for (int i = 0; i < n; ++i) { parent[i] = i; // 初始时每个节点的根节点是自身 } }
// 查找节点 x 的根节点(带路径压缩) int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); // 路径压缩,直接指向根节点 } return parent[x]; }
// 合并节点 x 和节点 y 所在的集合 void unite(int x, int y) { int rootX = find(x); int rootY = find(y);
/* * 说明:为什么 rootX == rootY 时不需要 count-- ? * 1. count 维护的是独立集合(连通分量)的总数,初始为 n。 * 2. 当 rootX != rootY 时:说明 x 和 y 属于两个不同的集合,合并它们会让总集合数减少 1,因此 count--。 * 3. 当 rootX == rootY 时:说明 x 和 y 在此之前已经直接或间接相连(在同一个集合内)。 * 此时再加一条边只是在集合内部连线(形成环),并没有融合新的独立集合,因此集合总数不变,无需 count--。 */ if (rootX != rootY) { parent[rootY] = rootX; // 将 rootY 的父节点设为 rootX count--; // 成功合并两个不同的集合,省份数量减 1 } }
// 获取当前连通分量数量 int getCount() { return count; }};
class Solution {public: int findCircleNum(vector<vector<int>>& isConnected) { int n = isConnected.size(); UnionFind uf(n);
// 遍历矩阵上三角部分,合并有边连接的节点 for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { if (isConnected[i][j] == 1) { uf.unite(i, j); } } }
return uf.getCount(); }};- 时间复杂度:,其中 为城市数量, 是反阿克曼函数(增长极其缓慢,通常可看作常数 )。我们需要遍历对称矩阵的半边 ,每次合并和查找耗时接近 。
- 空间复杂度:,主要是存储节点父节点信息的
parent数组占用的空间。
| 方法 | 实现难度 | 适用场景 |
|---|---|---|
| DFS | 简单(最推荐) | 代码量最少,适合面试快速书写 |
| BFS | 简单 | 迭代实现,避免递归过深导致的栈溢出 |
| 并查集 | 中等 | 适合处理动态连通性问题或大规模图结构 |