포스트

연결 요소(Connected Component)

그래프 중에서는 위 그림과 같이 여러 개로 나누어져 있을 수도 있다. 위 그림을 보고 두 개의 그래프라고 볼 수도 있지만, 하나의 그래프에 두 개의 연결 요소를 가진다고 볼 수도 있다. 그 이유는 정점 사이에 겹치는 것이 없기 때문이다. 연결 요소로 본다면, 나누어진 각각의 그래프를 연결 요소라고 한다. 이때 연결 요소가 될 조건은 다음과 같다.

  • 연결 요소에 속한 모든 정점을 연결하는 경로가 있어야 한다.
  • 또 다른 연결 요소에 속한 정점과 연결하는 경로가 있으면 안 된다.

그러므로, 위 그림은 2개의 연결 요소로 이루어져 있다고 볼 수 있다. 연결 요소를 구하는 것은 DFS나 BFS 탐색을 이용해서 할 수 있다.


연결 요소를 조금 더 정확히 보기

두 조건을 합치면 연결 요소는 “서로 경로로 이어진 정점들을 최대한 넓게 묶은 덩어리”다. 첫 번째 조건만 있으면 덩어리의 일부만 떼어 내도 조건을 만족하므로, 두 번째 조건이 “더 넓힐 수 없을 때까지” 묶으라는 뜻을 더한다. 그래서 모든 정점은 정확히 하나의 연결 요소에 속하고, 간선이 하나도 없는 정점은 그 자체로 연결 요소 하나가 된다.

여기서 말하는 연결 요소는 방향 없는 그래프(무방향 그래프) 기준이다. 방향 그래프에서는 A에서 B로 갈 수 있어도 B에서 A로 못 갈 수 있으므로 “연결되어 있다”의 뜻이 달라지고, 서로 오갈 수 있는 정점끼리 묶는 강한 연결 요소(strongly connected component)라는 별도의 개념을 쓴다.


왜 DFS나 BFS로 구할 수 있는가

DFS나 BFS는 시작 정점에서 경로로 닿을 수 있는 모든 정점을 방문하고, 닿을 수 없는 정점은 방문하지 않는다. 이것이 정확히 시작 정점이 속한 연결 요소다. 따라서 다음 과정을 반복하면 된다.

  1. 아직 방문하지 않은 정점을 하나 고른다.
  2. 그 정점에서 DFS(또는 BFS)를 돌려 닿는 정점을 모두 방문 처리한다. 이 정점들이 연결 요소 하나다.
  3. 연결 요소 개수를 1 늘리고, 방문하지 않은 정점이 남아 있으면 1로 돌아간다.

탐색을 시작한 횟수가 곧 연결 요소의 개수다. 한 번 방문 처리된 정점은 다시 탐색의 시작점이 되지 않으므로, 같은 연결 요소를 두 번 세는 일이 없다.


<hr />


연결 요소 관련 문제

BOJ 11724. 연결 요소의 개수

  • 문제링크 : https://www.acmicpc.net/problem/11724

방향 없는 그래프가 주어졌을 때, 연결 요소 (Connected Component)의 개수를 구하는 프로그램을 작성하시오.

정점은 최대 1,000개이고 간선은 같은 간선이 한 번씩만 주어진다. 정점 번호는 1부터 N까지다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <cstdio>
#include <vector>
using namespace std;
vector<int> a[1001];
bool check[1001];

void dfs(int node) {
    check[node] = true;
    for (int i=0; i<a[node].size(); i++) {
        int next = a[node][i];
        if (check[next] == false) {
            dfs(next);
        }
    }
}
int main() {
    int n, m;
    scanf("%d %d",&n,&m);
    for (int i=0; i<m; i++) {
        int u,v;
        scanf("%d %d",&u,&v);
        a[u].push_back(v);
        a[v].push_back(u);
    }
    int components = 0;
    for (int i=1; i<=n; i++) {		// 모든 인접 리스트를 순회하면서
        if (check[i] == false) {	// 방문하지 않은 노드가 있다면
            dfs(i);			// 하나의 연결 요소 모두 방문
            components += 1;		// 연결 요소의 수 증가
        }
    }
    printf("%d\n",components);
    return 0;
}

코드 읽기

  • vector<int> a[1001]은 인접 리스트다. 정점 번호가 1부터 시작하므로 크기를 N+1인 1001로 잡았다. 그래프 글에서 정리했듯이 인접 리스트는 실제 간선 수만큼만 공간을 쓴다.
  • 입력에서 a[u].push_back(v)와 a[v].push_back(u)를 둘 다 하는 이유는 방향 없는 그래프이기 때문이다. 한쪽만 넣으면 v에서 u로 가는 길이 없는 것처럼 탐색된다.
  • dfs(node)는 정점을 방문 처리한 뒤, 인접한 정점 중 방문하지 않은 곳으로 재귀 호출한다. 호출이 끝나면 node가 속한 연결 요소의 모든 정점이 check에 표시되어 있다.
  • main의 마지막 반복문이 위의 1~3단계다. 1번부터 N번까지 차례로 보면서 아직 방문하지 않은 정점을 만날 때마다 DFS를 한 번 돌리고 개수를 1 늘린다.

예를 들어 정점이 6개이고 간선이 1-2, 2-5, 5-1, 3-4, 4-6이라면, i=1에서 DFS가 1, 2, 5를 방문하고 개수가 1이 된다. i=2는 이미 방문했으므로 건너뛴다. i=3에서 DFS가 3, 4, 6을 방문하고 개수가 2가 된다. 나머지는 모두 방문되어 있으므로 답은 2다.

시간 복잡도

모든 정점은 정확히 한 번 방문 처리되고, 각 정점에서 인접 리스트를 한 번씩 훑는다. 방향 없는 그래프에서는 간선 하나가 양쪽 인접 리스트에 한 번씩 들어가므로 인접 리스트 전체 길이는 2M이다. 따라서 전체 시간은 O(N + M)이다. 인접 행렬로 저장했다면 정점마다 N칸을 모두 확인해야 하므로 O(N²)이 된다.

재귀 깊이

재귀 DFS의 호출 깊이는 최악의 경우 정점 수만큼 깊어진다. 정점이 일렬로 이어진 그래프라면 1,000단계까지 들어간다. 이 문제의 제한에서는 문제가 없지만, 정점이 수십만 개인 문제에서는 스택 오버플로가 날 수 있다. 그럴 때는 BFS를 쓰거나, 명시적인 스택으로 DFS를 반복문으로 바꾸면 된다. 연결 요소의 개수만 필요하다면 어떤 순서로 방문하든 결과는 같다.


<hr />

참고한 자료외부 출처 1 · 블로그 글 1

외부 출처

이 블로그의 관련 글

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다