FB4-求无向图的连通分量个数
[[#并查集方式]]
代码实现
DFS
| C |
|---|
| /**
* 求无向图的连通分量个数
*
* 定义:连通分量是无向图中极大连通子图的个数
* 在无向图中,如果任意两个顶点之间都存在路径,则称该图为连通图
* 否则,该图由多个连通分量组成
*
* 算法思想:
* 1. 遍历所有顶点
* 2. 对于每个未被访问的顶点,进行一次DFS/BFS遍历
* 3. 每次完整的DFS/BFS遍历对应一个连通分量
* 4. 统计遍历次数即为连通分量个数
*
* 核心思路:
* - 利用图遍历的特性:一次遍历可以访问一个连通分量中的所有顶点
* - 通过访问标记避免重复访问
* - 每发现一个未访问顶点,就意味着发现了一个新的连通分量
*
* @param G 无向图的邻接表表示
* @return 连通分量的个数
*/
int countConnectedComponents(Graph G) {
// 全局访问标记数组,记录所有顶点的访问状态
int visit[maxsize] = {0};
// 连通分量计数器
int count = 0;
// 遍历所有顶点
for (int i = 0; i < G.numver; ++i) {
// 如果顶点i未被访问,说明发现了一个新的连通分量
if (visit[i] == 0) {
// 从顶点i开始进行DFS遍历,访问整个连通分量
DFS(G, i, visit);
// 连通分量计数加1
count++;
}
}
// 返回连通分量总数
return count;
}
/**
* 图的深度优先搜索遍历(辅助函数)
*
* @param G 图的邻接表表示
* @param v 起始顶点
* @param visit 访问标记数组(全局共享)
*/
void DFS(Graph G, int v, int visit[]) {
// 标记当前顶点为已访问
visit[v] = 1;
// 遍历当前顶点的所有邻接顶点
Node p = G.adjlist[v].firstarc;
for (; p != NULL; p = p->nextarc) {
// 如果邻接顶点未被访问,则递归访问
if (visit[p->adjvex] == 0) {
DFS(G, p->adjvex, visit);
}
}
}
|
BFS
| C |
|---|
| /**
* 使用BFS实现的连通分量计算
*
* 优点:在某些情况下BFS可能有更好的性能表现
*/
int countConnectedComponents_BFS(Graph G) {
int visit[maxsize] = {0};
int count = 0;
for (int i = 0; i < G.numver; ++i) {
if (visit[i] == 0) {
// 使用BFS遍历整个连通分量
BFS(G, i, visit);
count++;
}
}
return count;
}
/**
* BFS辅助函数
*/
void BFS(Graph G, int v, int visit[]) {
int que[maxsize];
int front = 0, rear = 0;
// 起始顶点入队并标记
que[rear++] = v;
visit[v] = 1;
// BFS遍历
while (front != rear) {
v = que[front++];
Node p = G.adjlist[v].firstarc;
for (; p != NULL; p = p->nextarc) {
if (visit[p->adjvex] == 0) {
visit[p->adjvex] = 1;
que[rear++] = p->adjvex;
}
}
}
}
|
并查集方式
| C |
|---|
| /**
* 使用并查集(Union-Find)计算图的连通分量数量
*
* 算法思路:
* 1. 初始化并查集,每个顶点初始时独立成一个集合。
* 2. 遍历图中每一条边 (u, v),将两个端点合并到同一集合。
* - 由于是无向图,每条边只需处理一次即可保证连通性正确合并。
* 3. 最终统计并查集中根节点的个数,即为连通分量的数量。
*
* 时间复杂度:O(E × α(V)),其中 α 是阿克曼函数的反函数,实际中接近常数。
* 空间复杂度:O(V),来自并查集的 parent 数组。
*
* 优点:
* - 支持动态添加边后实时查询连通性。
* - 路径压缩与按大小合并使操作高效。
*
* @param G 图的邻接表表示
* @return 连通分量的个数
*/
int countConnectedComponents_UnionFind(Graph G) {
// 步骤1:初始化并查集,每个顶点自成一个集合
init(); // 将 parent[i] 初始化为 -1
// 步骤2:遍历每个顶点及其邻接边,合并相连的顶点
for (int i = 0; i < G.numver; i++) {
Node* p = G.adjlist[i].firstarc; // 获取顶点 i 的第一条边
while (p != NULL) {
// 将顶点 i 和其邻接点 p->adjvex 合并到同一集合
// 注意:无向图中边 (i, p->adjvex) 只需处理一次
unionSet(i, p->adjvex);
p = p->nextarc; // 移动到下一条边
}
}
// 步骤3:统计当前并查集中根节点的数量,即连通分量个数
return countSet();
}
|
| C |
|---|
| int parent[MAX_SIZE]; // parent[i] < 0 表示 i 是根,其绝对值为集合大小
/**
* 初始化:每个元素自成一个集合
*/
void init() {
for (int i = 0; i < MAX_SIZE; ++i) {
parent[i] = -1; // 初始大小为 1(用 -1 表示)
}
}
/**
* 查找元素 x 的根,并进行路径压缩
* @param x 元素下标
* @return 根节点下标
*/
int find(int x) {
int root = x;
// 找根
while (parent[root] >= 0) {
root = parent[root];
}
// 路径压缩:所有节点直接连到根
while (x != root) {
int temp = parent[x];
parent[x] = root;
x = temp;
}
return root;
}
/**
* 合并元素 x 和 y 所在的集合(按大小合并)
* @param x 元素 x
* @param y 元素 y
*/
void unionSet(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return; // 已在同一集合,无需合并
// 按大小合并:小树合并到大树
if (parent[rootY] > parent[rootX]) {
// rootY 的树更小(负得少),挂到 rootX 下
parent[rootX] += parent[rootY]; // 更新大小
parent[rootY] = rootX;
} else {
// rootX 更小或相等,挂到 rootY 下
parent[rootY] += parent[rootX];
parent[rootX] = rootY;
}
}
/**
* 统计当前集合的个数(即根节点的个数)
* @return 集合数量
*/
int countSet() {
int count = 0;
for (int i = 0; i < MAX_SIZE; ++i) {
if (parent[i] < 0) { // 根节点的标志
count++;
}
}
return count;
}
|
复杂度分析
时间复杂度:O(V + E)
- V: 顶点数,E: 边数
- 每个顶点被访问一次:O(V)
- 每条边被检查一次:O(E)
- 总时间复杂度:O(V + E)
注意:原代码注释中的O(V×(V+E))是错误的,正确的时间复杂度应该是O(V+E)
空间复杂度:O(V)
visit[]数组:O(V)
- DFS递归调用栈:最坏情况O(V)
- 总空间复杂度:O(V)
图解说明
| Text Only |
|---|
| 示例无向图结构(包含3个连通分量):
连通分量1: 0-1-2 连通分量2: 3-4 连通分量3: 5
0 ── 1 3 ── 4 5
│
2
执行过程:
初始状态:
visit: [0,0,0,0,0,0]
count: 0
检查顶点0:
visit[0] = 0 → 发现新连通分量
DFS(0): 访问 0→1→2
visit: [1,1,1,0,0,0]
count: 1
检查顶点1,2:
visit[1] = 1, visit[2] = 1 → 已访问,跳过
检查顶点3:
visit[3] = 0 → 发现新连通分量
DFS(3): 访问 3→4
visit: [1,1,1,1,1,0]
count: 2
检查顶点4:
visit[4] = 1 → 已访问,跳过
检查顶点5:
visit[5] = 0 → 发现新连通分量
DFS(5): 访问 5
visit: [1,1,1,1,1,1]
count: 3
最终结果:连通分量个数为3
|
相关知识点延伸
1. 连通分量的应用场景
| C |
|---|
| // 社交网络分析
int analyzeSocialNetwork(UserGraph G) {
int components = countConnectedComponents(G);
if (components == 1) {
printf("社交网络是连通的\n");
} else {
printf("社交网络包含%d个孤立群体\n", components);
}
return components;
}
// 计算机网络连通性检查
int checkNetworkConnectivity(NetworkGraph G) {
return countConnectedComponents(G);
}
// 岛屿计数问题(二维网格)
int countIslands(char grid[][MAX], int rows, int cols) {
// 将二维网格转换为图,然后计算连通分量
Graph G = convertGridToGraph(grid, rows, cols);
return countConnectedComponents(G);
}
|
2. 特殊图类型的处理
| C |
|---|
| // 树的连通分量(总是1)
int countTreeComponents(Graph tree) {
return 1; // 树本身就是连通的
}
// 完全图的连通分量(总是1)
int countCompleteGraphComponents(Graph completeGraph) {
return 1; // 完全图是连通的
}
// 空图的连通分量
int countEmptyGraphComponents(Graph emptyGraph) {
return emptyGraph.numver; // 每个孤立顶点是一个连通分量
}
|
3. 连通分量的进一步分析
| C |
|---|
| // 找出最大的连通分量
int findLargestComponent(Graph G) {
int visit[maxsize] = {0};
int maxSize = 0;
for (int i = 0; i < G.numver; ++i) {
if (visit[i] == 0) {
int componentSize = 0;
DFS_Count(G, i, visit, &componentSize);
if (componentSize > maxSize) {
maxSize = componentSize;
}
}
}
return maxSize;
}
// 带计数功能的DFS
void DFS_Count(Graph G, int v, int visit[], int* count) {
visit[v] = 1;
(*count)++;
Node p = G.adjlist[v].firstarc;
for (; p != NULL; p = p->nextarc) {
if (visit[p->adjvex] == 0) {
DFS_Count(G, p->adjvex, visit, count);
}
}
}
|
4. 动态连通性问题
| C |
|---|
| // 支持动态添加边的连通分量计算
typedef struct {
Graph G;
int componentCount;
UnionFind uf;
} DynamicConnectivity;
void initDynamicConnectivity(DynamicConnectivity* dc, int vertexCount) {
dc->componentCount = vertexCount;
initUnionFind(&dc->uf, vertexCount);
}
void addEdge(DynamicConnectivity* dc, int u, int v) {
if (unionSets(&dc->uf, u, v)) {
dc->componentCount--; // 合并了两个不同的连通分量
}
}
int getComponentCount(DynamicConnectivity* dc) {
return dc->componentCount;
}
|
5. 错误处理和边界情况
| C |
|---|
| // 完整的错误处理版本
int countConnectedComponents_Robust(Graph G) {
// 边界情况处理
if (G.numver <= 0) {
return 0;
}
if (G.numver == 1) {
return 1;
}
// 正常处理
int visit[maxsize] = {0};
int count = 0;
for (int i = 0; i < G.numver; ++i) {
if (visit[i] == 0) {
DFS(G, i, visit);
count++;
}
}
return count;
}
|
连通分量是图论中最基础也是最重要的概念之一,广泛应用于网络分析、社交计算、图像处理等领域。理解这个算法对于掌握图论知识具有重要意义。