7-3 连通分量

连通分量(Connected Components)

连通分量(Connected Components)是无向图(Undirected Graph)中的一个重要概念。一个连通分量是图中顶点的一个极大子集(Maximal Subset),其中任意两个顶点之间都存在路径(Path)。一个图可能包含多个连通分量,每个连通分量内部互相可达,不同连通分量之间不可达。

寻找所有连通分量有助于理解图的整体结构,广泛应用于网络分析(Network Analysis)、图像分割(Image Segmentation)和聚类(Clustering)等领域。

下面使用一个非连通图作为示例:

0 — 1 — 2    5 — 6    8
|       |
3 — 4   7

邻接表(Adjacency List)表示为:{0:[1,3], 1:[0,2], 2:[1,7], 3:[0,4], 4:[3], 5:[6], 6:[5], 7:[2], 8:[]}

该图包含 3 个连通分量:{0,1,2,3,4,7}{5,6}{8}


使用 DFS 找连通分量

深度优先搜索(DFS, Depth-First Search)是找连通分量的经典方法。基本思路是:遍历所有顶点,对于每个未访问的顶点,启动一次 DFS,DFS 过程中访问到的所有顶点构成一个连通分量。

具体步骤如下:

  1. 维护一个 visited 数组,记录每个顶点是否已被访问
  2. 从顶点 0 开始,依次检查每个顶点
  3. 如果顶点未被访问,则从该顶点启动 DFS,所有被访问到的顶点属于同一个连通分量
  4. DFS 递归访问当前顶点的所有未访问邻居
  5. DFS 结束后,连通分量编号加一

C++ 实现

#include <iostream>
#include <vector>
using namespace std;

// DFS to visit all vertices in one connected component
void dfs(const vector<vector<int>>& graph, int node, vector<bool>& visited, vector<int>& component) {
    visited[node] = true;
    component.push_back(node);

    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            dfs(graph, neighbor, visited, component);
        }
    }
}

int main() {
    // Adjacency list representation of the disconnected graph
    vector<vector<int>> graph = {
        {1, 3},    // 0
        {0, 2},    // 1
        {1, 7},    // 2
        {0, 4},    // 3
        {3},       // 4
        {6},       // 5
        {5},       // 6
        {2},       // 7
        {}         // 8 (isolated vertex)
    };

    int n = graph.size();
    vector<bool> visited(n, false);

    cout << "DFS Connected Components:" << endl;
    int componentId = 0;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            vector<int> component;
            dfs(graph, i, visited, component);

            cout << "Component " << componentId << ": {";
            for (int j = 0; j < (int)component.size(); j++) {
                cout << component[j];
                if (j < (int)component.size() - 1) cout << ",";
            }
            cout << "}" << endl;
            componentId++;
        }
    }

    return 0;
}

C 实现

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_VERTICES 20

int component[MAX_VERTICES];  // Store vertices in current component
int compSize;                 // Current component size

// DFS to visit all vertices in one connected component
void dfs(int graph[][MAX_VERTICES], int graphSize[], int node, bool* visited) {
    visited[node] = true;
    component[compSize++] = node;

    for (int i = 0; i < graphSize[node]; i++) {
        int neighbor = graph[node][i];
        if (!visited[neighbor]) {
            dfs(graph, graphSize, neighbor, visited);
        }
    }
}

int main() {
    // Build adjacency list using 2D array + size array
    int graph[MAX_VERTICES][MAX_VERTICES] = {0};
    int graphSize[MAX_VERTICES] = {0};

    // Edge list
    int edges[][2] = {{0,1},{0,3},{1,2},{2,7},{3,4},{5,6}};
    int edgeCount = 6;
    int n = 9;  // Total vertices

    // Build adjacency list
    for (int i = 0; i < edgeCount; i++) {
        int u = edges[i][0], v = edges[i][1];
        graph[u][graphSize[u]++] = v;
        graph[v][graphSize[v]++] = u;
    }
    // Vertex 8 has no edges (isolated)

    bool visited[MAX_VERTICES] = {false};

    printf("DFS Connected Components:\n");
    int componentId = 0;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            compSize = 0;
            dfs(graph, graphSize, i, visited);

            printf("Component %d: {", componentId);
            for (int j = 0; j < compSize; j++) {
                printf("%d", component[j]);
                if (j < compSize - 1) printf(",");
            }
            printf("}\n");
            componentId++;
        }
    }

    return 0;
}

Python 实现

# DFS to visit all vertices in one connected component
def dfs(graph, node, visited, component):
    visited[node] = True
    component.append(node)

    for neighbor in graph[node]:
        if not visited[neighbor]:
            dfs(graph, neighbor, visited, component)


def find_components_dfs(graph):
    n = len(graph)
    visited = [False] * n
    components = []

    for i in range(n):
        if not visited[i]:
            component = []
            dfs(graph, i, visited, component)
            components.append(component)

    return components


# Adjacency list representation
graph = {
    0: [1, 3],
    1: [0, 2],
    2: [1, 7],
    3: [0, 4],
    4: [3],
    5: [6],
    6: [5],
    7: [2],
    8: []       # Isolated vertex
}

print("DFS Connected Components:")
components = find_components_dfs(graph)
for i, comp in enumerate(components):
    print(f"Component {i}: {{{','.join(map(str, comp))}}}")

Go 实现

package main

import "fmt"

// dfs visits all vertices in one connected component
func dfs(graph [][]int, node int, visited []bool, component *[]int) {
	visited[node] = true
	*component = append(*component, node)

	for _, neighbor := range graph[node] {
		if !visited[neighbor] {
			dfs(graph, neighbor, visited, component)
		}
	}
}

func main() {
	// Adjacency list representation
	graph := [][]int{
		{1, 3}, // 0
		{0, 2}, // 1
		{1, 7}, // 2
		{0, 4}, // 3
		{3},    // 4
		{6},    // 5
		{5},    // 6
		{2},    // 7
		{},     // 8 (isolated vertex)
	}

	n := len(graph)
	visited := make([]bool, n)

	fmt.Println("DFS Connected Components:")
	componentID := 0

	for i := 0; i < n; i++ {
		if !visited[i] {
			var component []int
			dfs(graph, i, visited, &component)

			fmt.Printf("Component %d: {", componentID)
			for j, v := range component {
				if j > 0 {
					fmt.Print(",")
				}
				fmt.Print(v)
			}
			fmt.Println("}")
			componentID++
		}
	}
}

Go 版本使用切片 []int 存储邻接表,通过指向切片的指针 *[]int 在 DFS 递归中累积连通分量的顶点。visited 数组在 dfs 函数间共享,无需返回值。

以上四个版本都使用相同的 DFS 策略:外层循环遍历所有顶点,遇到未访问的顶点就启动 DFS,DFS 递归访问所有可达的邻居,这些顶点构成一个连通分量。

运行该程序将输出:

DFS Connected Components:
Component 0: {0,1,2,7,3,4}
Component 1: {5,6}
Component 2: {8}

注意 DFS 遍历的顺序与邻接表中邻居的排列有关,因此 Component 0 的顶点顺序可能是 {0,1,2,7,3,4} 而非 {0,1,2,3,4,7},但它们包含的顶点集合是一致的。


使用 BFS 找连通分量

广度优先搜索(BFS, Breadth-First Search)同样可以用来找连通分量,思路与 DFS 完全一致,只是将遍历方式从深度优先改为广度优先。BFS 使用队列(Queue)逐层访问邻居,适合需要按距离层次遍历的场景。

C++ 实现

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

// BFS to visit all vertices in one connected component
vector<int> bfs(const vector<vector<int>>& graph, int start, vector<bool>& visited) {
    vector<int> component;
    queue<int> q;

    visited[start] = true;
    q.push(start);

    while (!q.empty()) {
        int node = q.front();
        q.pop();
        component.push_back(node);

        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }

    return component;
}

int main() {
    vector<vector<int>> graph = {
        {1, 3},    // 0
        {0, 2},    // 1
        {1, 7},    // 2
        {0, 4},    // 3
        {3},       // 4
        {6},       // 5
        {5},       // 6
        {2},       // 7
        {}         // 8
    };

    int n = graph.size();
    vector<bool> visited(n, false);

    cout << "BFS Connected Components:" << endl;
    int componentId = 0;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            vector<int> component = bfs(graph, i, visited);

            cout << "Component " << componentId << ": {";
            for (int j = 0; j < (int)component.size(); j++) {
                cout << component[j];
                if (j < (int)component.size() - 1) cout << ",";
            }
            cout << "}" << endl;
            componentId++;
        }
    }

    return 0;
}

C 实现

#include <stdio.h>
#include <stdbool.h>

#define MAX_VERTICES 20

// BFS to visit all vertices in one connected component
void bfs(int graph[][MAX_VERTICES], int graphSize[], int start, bool* visited, int* component, int* compSize) {
    int queue[MAX_VERTICES];
    int front = 0, rear = 0;

    visited[start] = true;
    queue[rear++] = start;
    *compSize = 0;

    while (front < rear) {
        int node = queue[front++];
        component[(*compSize)++] = node;

        for (int i = 0; i < graphSize[node]; i++) {
            int neighbor = graph[node][i];
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                queue[rear++] = neighbor;
            }
        }
    }
}

int main() {
    int graph[MAX_VERTICES][MAX_VERTICES] = {0};
    int graphSize[MAX_VERTICES] = {0};

    int edges[][2] = {{0,1},{0,3},{1,2},{2,7},{3,4},{5,6}};
    int edgeCount = 6;
    int n = 9;

    for (int i = 0; i < edgeCount; i++) {
        int u = edges[i][0], v = edges[i][1];
        graph[u][graphSize[u]++] = v;
        graph[v][graphSize[v]++] = u;
    }

    bool visited[MAX_VERTICES] = {false};
    int component[MAX_VERTICES];
    int compSize;

    printf("BFS Connected Components:\n");
    int componentId = 0;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            bfs(graph, graphSize, i, visited, component, &compSize);

            printf("Component %d: {", componentId);
            for (int j = 0; j < compSize; j++) {
                printf("%d", component[j]);
                if (j < compSize - 1) printf(",");
            }
            printf("}\n");
            componentId++;
        }
    }

    return 0;
}

Python 实现

from collections import deque

# BFS to visit all vertices in one connected component
def bfs(graph, start, visited):
    component = []
    queue = deque([start])
    visited[start] = True

    while queue:
        node = queue.popleft()
        component.append(node)

        for neighbor in graph[node]:
            if not visited[neighbor]:
                visited[neighbor] = True
                queue.append(neighbor)

    return component


def find_components_bfs(graph):
    n = len(graph)
    visited = [False] * n
    components = []

    for i in range(n):
        if not visited[i]:
            component = bfs(graph, i, visited)
            components.append(component)

    return components


graph = {
    0: [1, 3],
    1: [0, 2],
    2: [1, 7],
    3: [0, 4],
    4: [3],
    5: [6],
    6: [5],
    7: [2],
    8: []
}

print("BFS Connected Components:")
components = find_components_bfs(graph)
for i, comp in enumerate(components):
    print(f"Component {i}: {{{','.join(map(str, comp))}}}")

Go 实现

package main

import "fmt"

// bfs visits all vertices in one connected component
func bfs(graph [][]int, start int, visited []bool) []int {
	var component []int
	queue := []int{start}
	visited[start] = true

	for len(queue) > 0 {
		node := queue[0]
		queue = queue[1:]
		component = append(component, node)

		for _, neighbor := range graph[node] {
			if !visited[neighbor] {
				visited[neighbor] = true
				queue = append(queue, neighbor)
			}
		}
	}

	return component
}

func main() {
	graph := [][]int{
		{1, 3}, // 0
		{0, 2}, // 1
		{1, 7}, // 2
		{0, 4}, // 3
		{3},    // 4
		{6},    // 5
		{5},    // 6
		{2},    // 7
		{},     // 8
	}

	n := len(graph)
	visited := make([]bool, n)

	fmt.Println("BFS Connected Components:")
	componentID := 0

	for i := 0; i < n; i++ {
		if !visited[i] {
			component := bfs(graph, i, visited)

			fmt.Printf("Component %d: {", componentID)
			for j, v := range component {
				if j > 0 {
					fmt.Print(",")
				}
				fmt.Print(v)
			}
			fmt.Println("}")
			componentID++
		}
	}
}

Go 版本的 BFS 使用切片模拟队列:通过 queue[0] 取队首元素,queue[1:] 弹出队首。虽然这不是最高效的队列实现(每次弹出都会创建新切片),但对于小规模图完全够用,且代码简洁。

BFS 与 DFS 的区别仅在于遍历策略:DFS 使用栈(递归调用栈),BFS 使用队列。两者找到的连通分量集合相同,但顶点的访问顺序不同。BFS 按层次访问,因此离起点近的顶点会先被访问到。

运行该程序将输出:

BFS Connected Components:
Component 0: {0,1,3,2,4,7}
Component 1: {5,6}
Component 2: {8}

使用并查集(Union-Find)找连通分量

并查集(Union-Find / Disjoint Set Union, DSU)是另一种高效找连通分量的方法。它维护一组不相交的集合,支持两种核心操作:

  • 查找(Find):确定某个元素属于哪个集合,即找到该集合的代表元素(根节点)
  • 合并(Union):将两个元素所在的集合合并为一个集合

通过路径压缩(Path Compression)和按秩合并(Union by Rank)两种优化,并查集的每次操作时间复杂度接近 O(1)。

具体步骤如下:

  1. 初始化时,每个顶点自成一个集合(parent[i] = i
  2. 遍历所有边,对每条边的两个端点执行 Union 操作
  3. 最终统计有多少个不同的根节点,即为连通分量的数量
  4. 将具有相同根节点的顶点归为一组

C++ 实现

#include <iostream>
#include <vector>
#include <map>
using namespace std;

class UnionFind {
private:
    vector<int> parent;
    vector<int> rank_;

public:
    // Initialize: each vertex is its own parent (separate set)
    UnionFind(int n) {
        parent.resize(n);
        rank_.resize(n, 0);
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // Find with path compression
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // Path compression
        }
        return parent[x];
    }

    // Union by rank
    void unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX == rootY) return;  // Already in same set

        // Attach smaller rank tree under root of larger rank tree
        if (rank_[rootX] < rank_[rootY]) {
            parent[rootX] = rootY;
        } else if (rank_[rootX] > rank_[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank_[rootX]++;
        }
    }
};

int main() {
    int n = 9;
    UnionFind uf(n);

    // Edge list
    vector<pair<int,int>> edges = {
        {0,1}, {0,3}, {1,2}, {2,7}, {3,4}, {5,6}
    };

    // Union all edges
    for (auto& e : edges) {
        uf.unionSets(e.first, e.second);
    }

    // Group vertices by root
    map<int, vector<int>> components;
    for (int i = 0; i < n; i++) {
        components[uf.find(i)].push_back(i);
    }

    cout << "Union-Find Connected Components:" << endl;
    int componentId = 0;
    for (auto& [root, vertices] : components) {
        cout << "Component " << componentId << ": {";
        for (int j = 0; j < (int)vertices.size(); j++) {
            cout << vertices[j];
            if (j < (int)vertices.size() - 1) cout << ",";
        }
        cout << "}" << endl;
        componentId++;
    }

    return 0;
}

C 实现

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int* parent;
    int* rank;
    int n;
} UnionFind;

// Initialize Union-Find structure
UnionFind* createUF(int n) {
    UnionFind* uf = (UnionFind*)malloc(sizeof(UnionFind));
    uf->n = n;
    uf->parent = (int*)malloc(n * sizeof(int));
    uf->rank = (int*)calloc(n, sizeof(int));
    for (int i = 0; i < n; i++) {
        uf->parent[i] = i;
    }
    return uf;
}

// Find with path compression
int find(UnionFind* uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = find(uf, uf->parent[x]);
    }
    return uf->parent[x];
}

// Union by rank
void unionSets(UnionFind* uf, int x, int y) {
    int rootX = find(uf, x);
    int rootY = find(uf, y);

    if (rootX == rootY) return;

    if (uf->rank[rootX] < uf->rank[rootY]) {
        uf->parent[rootX] = rootY;
    } else if (uf->rank[rootX] > uf->rank[rootY]) {
        uf->parent[rootY] = rootX;
    } else {
        uf->parent[rootY] = rootX;
        uf->rank[rootX]++;
    }
}

// Free Union-Find structure
void freeUF(UnionFind* uf) {
    free(uf->parent);
    free(uf->rank);
    free(uf);
}

int main() {
    int n = 9;
    UnionFind* uf = createUF(n);

    int edges[][2] = {{0,1},{0,3},{1,2},{2,7},{3,4},{5,6}};
    int edgeCount = 6;

    // Union all edges
    for (int i = 0; i < edgeCount; i++) {
        unionSets(uf, edges[i][0], edges[i][1]);
    }

    // Group vertices by root
    int roots[MAX_VERTICES];
    int rootCount = 0;
    int vertexToComponent[MAX_VERTICES];

    for (int i = 0; i < n; i++) {
        int r = find(uf, i);
        int found = -1;
        for (int j = 0; j < rootCount; j++) {
            if (roots[j] == r) {
                found = j;
                break;
            }
        }
        if (found == -1) {
            roots[rootCount] = r;
            vertexToComponent[i] = rootCount;
            rootCount++;
        } else {
            vertexToComponent[i] = found;
        }
    }

    printf("Union-Find Connected Components:\n");
    for (int c = 0; c < rootCount; c++) {
        printf("Component %d: {", c);
        int first = 1;
        for (int i = 0; i < n; i++) {
            if (vertexToComponent[i] == c) {
                if (!first) printf(",");
                printf("%d", i);
                first = 0;
            }
        }
        printf("}\n");
    }

    freeUF(uf);
    return 0;
}

Python 实现

class UnionFind:
    def __init__(self, n):
        # Each vertex is its own parent initially
        self.parent = list(range(n))
        self.rank = [0] * n

    # Find with path compression
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    # Union by rank
    def union(self, x, y):
        root_x = self.find(x)
        root_y = self.find(y)

        if root_x == root_y:
            return

        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1


n = 9
uf = UnionFind(n)

edges = [(0,1), (0,3), (1,2), (2,7), (3,4), (5,6)]

# Union all edges
for u, v in edges:
    uf.union(u, v)

# Group vertices by root
from collections import defaultdict
components = defaultdict(list)
for i in range(n):
    components[uf.find(i)].append(i)

print("Union-Find Connected Components:")
for i, (root, vertices) in enumerate(components.items()):
    print(f"Component {i}: {{{','.join(map(str, vertices))}}}")

Go 实现

package main

import "fmt"

// UnionFind represents the disjoint set union data structure
type UnionFind struct {
	parent []int
	rank   []int
}

// NewUnionFind creates a new Union-Find with n elements
func NewUnionFind(n int) *UnionFind {
	uf := &UnionFind{
		parent: make([]int, n),
		rank:   make([]int, n),
	}
	for i := 0; i < n; i++ {
		uf.parent[i] = i
	}
	return uf
}

// Find with path compression
func (uf *UnionFind) Find(x int) int {
	if uf.parent[x] != x {
		uf.parent[x] = uf.Find(uf.parent[x])
	}
	return uf.parent[x]
}

// Union by rank
func (uf *UnionFind) Union(x, y int) {
	rootX := uf.Find(x)
	rootY := uf.Find(y)

	if rootX == rootY {
		return
	}

	if uf.rank[rootX] < uf.rank[rootY] {
		uf.parent[rootX] = rootY
	} else if uf.rank[rootX] > uf.rank[rootY] {
		uf.parent[rootY] = rootX
	} else {
		uf.parent[rootY] = rootX
		uf.rank[rootX]++
	}
}

func main() {
	n := 9
	uf := NewUnionFind(n)

	edges := [][2]int{{0, 1}, {0, 3}, {1, 2}, {2, 7}, {3, 4}, {5, 6}}

	// Union all edges
	for _, e := range edges {
		uf.Union(e[0], e[1])
	}

	// Group vertices by root
	type void struct{}
	rootSet := make(map[int]void)
	for i := 0; i < n; i++ {
		rootSet[uf.Find(i)] = void{}
	}

	// Build component map
	components := make(map[int][]int)
	for i := 0; i < n; i++ {
		root := uf.Find(i)
		components[root] = append(components[root], i)
	}

	fmt.Println("Union-Find Connected Components:")
	componentID := 0
	for _, vertices := range components {
		fmt.Printf("Component %d: {", componentID)
		for j, v := range vertices {
			if j > 0 {
				fmt.Print(",")
			}
			fmt.Print(v)
		}
		fmt.Println("}")
		componentID++
	}
}

Go 版本的并查集使用结构体 UnionFind 封装 parentrank 切片。Find 方法通过递归实现路径压缩,Union 方法使用按秩合并策略。Go 的 map 天然适合按根节点分组。

并查集方法的优势在于:不需要显式构建完整的邻接表,只需要遍历所有边即可。对于边稀疏的图,这种方法更加高效。路径压缩让 find 操作几乎接近 O(1),按秩合并确保树的高度始终较低。

运行该程序将输出:

Union-Find Connected Components:
Component 0: {0,1,2,3,4,7}
Component 1: {5,6}
Component 2: {8}

完整实现

以下程序将三种方法整合在一起,对同一个非连通图分别使用 DFS、BFS 和并查集(Union-Find)查找连通分量,并对比输出结果。

C++ 实现

#include <iostream>
#include <vector>
#include <queue>
#include <map>
using namespace std;

// ============ DFS Method ============
void dfs(const vector<vector<int>>& graph, int node, vector<bool>& visited, vector<int>& component) {
    visited[node] = true;
    component.push_back(node);
    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {
            dfs(graph, neighbor, visited, component);
        }
    }
}

vector<vector<int>> findComponentsDFS(const vector<vector<int>>& graph) {
    int n = graph.size();
    vector<bool> visited(n, false);
    vector<vector<int>> components;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            vector<int> component;
            dfs(graph, i, visited, component);
            components.push_back(component);
        }
    }
    return components;
}

// ============ BFS Method ============
vector<vector<int>> findComponentsBFS(const vector<vector<int>>& graph) {
    int n = graph.size();
    vector<bool> visited(n, false);
    vector<vector<int>> components;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            vector<int> component;
            queue<int> q;
            visited[i] = true;
            q.push(i);

            while (!q.empty()) {
                int node = q.front();
                q.pop();
                component.push_back(node);

                for (int neighbor : graph[node]) {
                    if (!visited[neighbor]) {
                        visited[neighbor] = true;
                        q.push(neighbor);
                    }
                }
            }
            components.push_back(component);
        }
    }
    return components;
}

// ============ Union-Find Method ============
class UnionFind {
private:
    vector<int> parent;
    vector<int> rank_;
public:
    UnionFind(int n) : parent(n), rank_(n, 0) {
        for (int i = 0; i < n; i++) parent[i] = i;
    }

    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }

    void unionSets(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank_[rx] < rank_[ry]) parent[rx] = ry;
        else if (rank_[rx] > rank_[ry]) parent[ry] = rx;
        else { parent[ry] = rx; rank_[rx]++; }
    }
};

vector<vector<int>> findComponentsUF(int n, const vector<pair<int,int>>& edges) {
    UnionFind uf(n);
    for (auto& e : edges) uf.unionSets(e.first, e.second);

    map<int, vector<int>> groups;
    for (int i = 0; i < n; i++) groups[uf.find(i)].push_back(i);

    vector<vector<int>> components;
    for (auto& [root, verts] : groups) components.push_back(verts);
    return components;
}

// ============ Helper ============
void printComponents(const string& method, const vector<vector<int>>& components) {
    cout << method << ":" << endl;
    for (int i = 0; i < (int)components.size(); i++) {
        cout << "  Component " << i << ": {";
        for (int j = 0; j < (int)components[i].size(); j++) {
            cout << components[i][j];
            if (j < (int)components[i].size() - 1) cout << ",";
        }
        cout << "}" << endl;
    }
}

int main() {
    vector<vector<int>> graph = {
        {1, 3}, {0, 2}, {1, 7}, {0, 4}, {3}, {6}, {5}, {2}, {}
    };

    vector<pair<int,int>> edges = {
        {0,1}, {0,3}, {1,2}, {2,7}, {3,4}, {5,6}
    };

    int n = graph.size();

    cout << "=== Comparing Three Methods ===" << endl;
    cout << "Graph: 9 vertices, 6 edges" << endl;
    cout << "Expected: 3 connected components" << endl << endl;

    printComponents("DFS Method", findComponentsDFS(graph));
    cout << endl;

    printComponents("BFS Method", findComponentsBFS(graph));
    cout << endl;

    printComponents("Union-Find Method", findComponentsUF(n, edges));

    return 0;
}

C 实现

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

#define MAX_V 20

// ============ DFS Method ============
int compBuf[MAX_V], compLen;

void dfs(int g[][MAX_V], int gs[], int node, bool* vis) {
    vis[node] = true;
    compBuf[compLen++] = node;
    for (int i = 0; i < gs[node]; i++) {
        if (!vis[g[node][i]]) dfs(g, gs, g[node][i], vis);
    }
}

// ============ BFS Method ============
void bfs(int g[][MAX_V], int gs[], int start, bool* vis, int* comp, int* clen) {
    int q[MAX_V], front = 0, rear = 0;
    vis[start] = true;
    q[rear++] = start;
    *clen = 0;
    while (front < rear) {
        int node = q[front++];
        comp[(*clen)++] = node;
        for (int i = 0; i < gs[node]; i++) {
            int nb = g[node][i];
            if (!vis[nb]) { vis[nb] = true; q[rear++] = nb; }
        }
    }
}

// ============ Union-Find Method ============
int ufParent[MAX_V], ufRank[MAX_V];

int ufFind(int x) {
    if (ufParent[x] != x) ufParent[x] = ufFind(ufParent[x]);
    return ufParent[x];
}

void ufUnion(int x, int y) {
    int rx = ufFind(x), ry = ufFind(y);
    if (rx == ry) return;
    if (ufRank[rx] < ufRank[ry]) ufParent[rx] = ry;
    else if (ufRank[rx] > ufRank[ry]) ufParent[ry] = rx;
    else { ufParent[ry] = rx; ufRank[rx]++; }
}

// ============ Print Helper ============
void printComp(const char* method, int comps[][MAX_V], int compSizes[], int count) {
    printf("%s:\n", method);
    for (int c = 0; c < count; c++) {
        printf("  Component %d: {", c);
        for (int j = 0; j < compSizes[c]; j++) {
            printf("%d", comps[c][j]);
            if (j < compSizes[c] - 1) printf(",");
        }
        printf("}\n");
    }
}

int main() {
    int g[MAX_V][MAX_V] = {0}, gs[MAX_V] = {0};
    int edges[][2] = {{0,1},{0,3},{1,2},{2,7},{3,4},{5,6}};
    int n = 9, ec = 6;

    for (int i = 0; i < ec; i++) {
        int u = edges[i][0], v = edges[i][1];
        g[u][gs[u]++] = v;
        g[v][gs[v]++] = u;
    }

    printf("=== Comparing Three Methods ===\n");
    printf("Graph: 9 vertices, 6 edges\n");
    printf("Expected: 3 connected components\n\n");

    // DFS
    bool vis[MAX_V] = {false};
    int comps[MAX_V][MAX_V], csizes[MAX_V], ccnt = 0;
    for (int i = 0; i < n; i++) {
        if (!vis[i]) {
            compLen = 0;
            dfs(g, gs, i, vis);
            csizes[ccnt] = compLen;
            for (int j = 0; j < compLen; j++) comps[ccnt][j] = compBuf[j];
            ccnt++;
        }
    }
    printComp("DFS Method", comps, csizes, ccnt);
    printf("\n");

    // BFS
    for (int i = 0; i < n; i++) vis[i] = false;
    ccnt = 0;
    for (int i = 0; i < n; i++) {
        if (!vis[i]) {
            int clen;
            bfs(g, gs, i, vis, comps[ccnt], &clen);
            csizes[ccnt] = clen;
            ccnt++;
        }
    }
    printComp("BFS Method", comps, csizes, ccnt);
    printf("\n");

    // Union-Find
    for (int i = 0; i < n; i++) { ufParent[i] = i; ufRank[i] = 0; }
    for (int i = 0; i < ec; i++) ufUnion(edges[i][0], edges[i][1]);

    // Group by root
    int roots[MAX_V], rootCnt = 0, v2c[MAX_V];
    for (int i = 0; i < n; i++) {
        int r = ufFind(i), found = -1;
        for (int j = 0; j < rootCnt; j++) {
            if (roots[j] == r) { found = j; break; }
        }
        if (found == -1) { roots[rootCnt] = r; v2c[i] = rootCnt; rootCnt++; }
        else { v2c[i] = found; }
    }
    ccnt = rootCnt;
    for (int c = 0; c < ccnt; c++) csizes[c] = 0;
    for (int i = 0; i < n; i++) {
        comps[v2c[i]][csizes[v2c[i]]++] = i;
    }
    printComp("Union-Find Method", comps, csizes, ccnt);

    return 0;
}

Python 实现

from collections import defaultdict, deque


def find_components_dfs(graph):
    n = len(graph)
    visited = [False] * n
    components = []

    def dfs(node):
        visited[node] = True
        comp.append(node)
        for nb in graph[node]:
            if not visited[nb]:
                dfs(nb)

    for i in range(n):
        if not visited[i]:
            comp = []
            dfs(i)
            components.append(comp)
    return components


def find_components_bfs(graph):
    n = len(graph)
    visited = [False] * n
    components = []

    for i in range(n):
        if not visited[i]:
            comp = []
            q = deque([i])
            visited[i] = True
            while q:
                node = q.popleft()
                comp.append(node)
                for nb in graph[node]:
                    if not visited[nb]:
                        visited[nb] = True
                        q.append(nb)
            components.append(comp)
    return components


class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return
        if self.rank[rx] < self.rank[ry]:
            self.parent[rx] = ry
        elif self.rank[rx] > self.rank[ry]:
            self.parent[ry] = rx
        else:
            self.parent[ry] = rx
            self.rank[rx] += 1


def find_components_uf(n, edges):
    uf = UnionFind(n)
    for u, v in edges:
        uf.union(u, v)
    groups = defaultdict(list)
    for i in range(n):
        groups[uf.find(i)].append(i)
    return list(groups.values())


def print_components(method, components):
    print(f"{method}:")
    for i, comp in enumerate(components):
        print(f"  Component {i}: {{{','.join(map(str, comp))}}}")


graph = {0:[1,3], 1:[0,2], 2:[1,7], 3:[0,4], 4:[3], 5:[6], 6:[5], 7:[2], 8:[]}
edges = [(0,1), (0,3), (1,2), (2,7), (3,4), (5,6)]
n = 9

print("=== Comparing Three Methods ===")
print("Graph: 9 vertices, 6 edges")
print("Expected: 3 connected components\n")

print_components("DFS Method", find_components_dfs(graph))
print()
print_components("BFS Method", find_components_bfs(graph))
print()
print_components("Union-Find Method", find_components_uf(n, edges))

Go 实现

package main

import "fmt"

// ============ DFS Method ============
func dfsFind(graph [][]int) [][]int {
	n := len(graph)
	visited := make([]bool, n)
	var components [][]int

	var dfs func(node int, comp *[]int)
	dfs = func(node int, comp *[]int) {
		visited[node] = true
		*comp = append(*comp, node)
		for _, nb := range graph[node] {
			if !visited[nb] {
				dfs(nb, comp)
			}
		}
	}

	for i := 0; i < n; i++ {
		if !visited[i] {
			var comp []int
			dfs(i, &comp)
			components = append(components, comp)
		}
	}
	return components
}

// ============ BFS Method ============
func bfsFind(graph [][]int) [][]int {
	n := len(graph)
	visited := make([]bool, n)
	var components [][]int

	for i := 0; i < n; i++ {
		if !visited[i] {
			var comp []int
			queue := []int{i}
			visited[i] = true

			for len(queue) > 0 {
				node := queue[0]
				queue = queue[1:]
				comp = append(comp, node)

				for _, nb := range graph[node] {
					if !visited[nb] {
						visited[nb] = true
						queue = append(queue, nb)
					}
				}
			}
			components = append(components, comp)
		}
	}
	return components
}

// ============ Union-Find Method ============
type UnionFind struct {
	parent []int
	rank   []int
}

func NewUF(n int) *UnionFind {
	uf := &UnionFind{parent: make([]int, n), rank: make([]int, n)}
	for i := range uf.parent {
		uf.parent[i] = i
	}
	return uf
}

func (uf *UnionFind) Find(x int) int {
	if uf.parent[x] != x {
		uf.parent[x] = uf.Find(uf.parent[x])
	}
	return uf.parent[x]
}

func (uf *UnionFind) Union(x, y int) {
	rx, ry := uf.Find(x), uf.Find(y)
	if rx == ry {
		return
	}
	if uf.rank[rx] < uf.rank[ry] {
		uf.parent[rx] = ry
	} else if uf.rank[rx] > uf.rank[ry] {
		uf.parent[ry] = rx
	} else {
		uf.parent[ry] = rx
		uf.rank[rx]++
	}
}

func ufFind(n int, edges [][2]int) [][]int {
	uf := NewUF(n)
	for _, e := range edges {
		uf.Union(e[0], e[1])
	}
	groups := make(map[int][]int)
	for i := 0; i < n; i++ {
		r := uf.Find(i)
		groups[r] = append(groups[r], i)
	}
	var components [][]int
	for _, verts := range groups {
		components = append(components, verts)
	}
	return components
}

// ============ Helper ============
func printComponents(method string, components [][]int) {
	fmt.Printf("%s:\n", method)
	for i, comp := range components {
		fmt.Printf("  Component %d: {", i)
		for j, v := range comp {
			if j > 0 {
				fmt.Print(",")
			}
			fmt.Print(v)
		}
		fmt.Println("}")
	}
}

func main() {
	graph := [][]int{
		{1, 3}, {0, 2}, {1, 7}, {0, 4}, {3}, {6}, {5}, {2}, {},
	}
	edges := [][2]int{{0, 1}, {0, 3}, {1, 2}, {2, 7}, {3, 4}, {5, 6}}
	n := 9

	fmt.Println("=== Comparing Three Methods ===")
	fmt.Println("Graph: 9 vertices, 6 edges")
	fmt.Println("Expected: 3 connected components")
	fmt.Println()

	printComponents("DFS Method", dfsFind(graph))
	fmt.Println()
	printComponents("BFS Method", bfsFind(graph))
	fmt.Println()
	printComponents("Union-Find Method", ufFind(n, edges))
}

Go 版本将三种方法封装为独立函数:dfsFindbfsFindufFind,各自返回 [][]int 类型的连通分量列表。printComponents 辅助函数统一输出格式,方便对比三种方法的结果。

三种方法的核心差异在于数据结构的选择:DFS 使用递归调用栈,BFS 使用显式队列,并查集使用父指针数组。对于同一个图,三种方法找到的连通分量集合完全一致,只是顶点的访问顺序可能不同。

运行该程序将输出:

=== Comparing Three Methods ===
Graph: 9 vertices, 6 edges
Expected: 3 connected components

DFS Method:
  Component 0: {0,1,2,7,3,4}
  Component 1: {5,6}
  Component 2: {8}

BFS Method:
  Component 0: {0,1,3,2,4,7}
  Component 1: {5,6}
  Component 2: {8}

Union-Find Method:
  Component 0: {0,1,2,3,4,7}
  Component 1: {5,6}
  Component 2: {8}

三种方法都正确识别出 3 个连通分量。DFS 和 BFS 的顶点顺序受遍历策略影响,而并查集的顶点顺序取决于 map 的迭代顺序。但无论顺序如何,每个连通分量包含的顶点集合完全相同。


连通分量的性质

时间复杂度

方法 时间复杂度 空间复杂度 适用场景
DFS O(V + E) O(V) 递归实现简洁,适合一般图
BFS O(V + E) O(V) 需要按层次遍历时使用
并查集(Union-Find) O(E * alpha(V)) 约 O(E) O(V) 动态加边、在线查询连通性

其中 V 是顶点数(Vertex),E 是边数(Edge),alpha 是反阿克曼函数(Inverse Ackermann Function),在实际应用中可视为常数(小于 5)。

方法对比

  • DFS / BFS:需要先构建邻接表,然后遍历所有顶点。适合一次性查找所有连通分量的场景。时间复杂度为 O(V + E),是遍历图的最优复杂度。
  • 并查集:只需要遍历所有边即可,不需要显式存储邻接表。特别适合动态加边的场景——当边逐步加入时,可以实时查询两个顶点是否在同一个连通分量中。路径压缩和按秩合并使得每次操作接近 O(1)。

应用场景

  • 网络分析(Network Analysis):在社交网络中,连通分量代表一个个社群(Community),分析连通分量有助于理解网络的拓扑结构
  • 图像分割(Image Segmentation):将图像的像素视为图的顶点,相邻且颜色相近的像素之间连边,连通分量即为分割出的区域
  • 聚类(Clustering):将数据点视为图的顶点,相似度高的点之间连边,连通分量即为聚类结果
  • 迷宫生成与求解:连通分量可以判断迷宫中两点之间是否存在通路
  • 电路设计:判断电路板上的元件是否连通,检测孤立回路
posted @ 2026-04-17 07:55  游翔  阅读(18)  评论(0)    收藏  举报