1. 문제
https://www.acmicpc.net/problem/11724
문제
방향 없는 그래프가 주어졌을 때, 연결 요소 (Connected Component)의 개수를 구하는 프로그램을 작성하시오.
입력
첫째 줄에 정점의 개수 N과 간선의 개수 M이 주어진다. (1 ≤ N ≤ 1,000, 0 ≤ M ≤ N×(N-1)/2) 둘째 줄부터 M개의 줄에 간선의 양 끝점 u와 v가 주어진다. (1 ≤ u, v ≤ N, u ≠ v) 같은 간선은 한 번만 주어진다.
출력
첫째 줄에 연결 요소의 개수를 출력한다.
예제 입력 1 복사
6 5
1 2
2 5
5 1
3 4
4 6
예제 출력 1 복사
2
예제 입력 2 복사
6 8
1 2
2 5
5 1
3 4
4 6
5 4
2 4
2 3
예제 출력 2 복사
1
2. 코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
static boolean[][] lines;
static boolean[] visited;
public static void main(String[] args) throws NumberFormatException, IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer token = new StringTokenizer(br.readLine());
int N = Integer.parseInt(token.nextToken());
int M = Integer.parseInt(token.nextToken());
lines = new boolean[N + 1][N + 1];
visited = new boolean[N + 1];
for (int i = 0; i < M; i++){
token = new StringTokenizer(br.readLine());
int a = Integer.parseInt(token.nextToken());
int b = Integer.parseInt(token.nextToken());
lines[a][b] = lines[b][a] = true;
}
int count = 0;
for (int i = 1; i <= N; i++){
if (!visited[i]){
count++;
dfs(i);
}
}
System.out.println(count);
}
private static void dfs(int n) {
visited[n] = true;
for (int i = 1; i < lines[n].length; i++) {
if (lines[n][i] && !visited[i]) {
dfs(i);
}
}
}
}
3. 해설
- dfs 방식을 활용하여 연결된 선을 재귀적 방식으로 모두 방문하고 카운팅한다
4. 다른 풀이
유니온 파인드(Union-Find) 알고리즘 활용
유니온 파인드 알고리즘의 핵심 개념:
- Find: 특정 노드가 속한 집합의 루트 노드를 찾음.
- Union: 두 노드를 같은 집합으로 합침 (즉, 두 노드가 같은 연결 요소에 속하도록 만듦).
- Path Compression: Find 연산 시, 트리의 높이를 줄여주는 최적화 방법.
- Union by Rank: 트리의 깊이를 줄이기 위해 작은 트리를 큰 트리에 연결하는 방법.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
static int[] parent;
static int[] rank;
public static void main(String[] args) throws NumberFormatException, IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer token = new StringTokenizer(br.readLine());
int N = Integer.parseInt(token.nextToken());
int M = Integer.parseInt(token.nextToken());
parent = new int[N + 1];
rank = new int[N + 1];
// 각 노드의 부모를 자기 자신으로 초기화하고, 랭크를 0으로 초기화
for (int i = 1; i <= N; i++) {
parent[i] = i;
rank[i] = 0;
}
// 간선 정보 입력 및 유니온 처리
for (int i = 0; i < M; i++) {
token = new StringTokenizer(br.readLine());
int a = Integer.parseInt(token.nextToken());
int b = Integer.parseInt(token.nextToken());
union(a, b);
}
// 연결 요소 개수 세기
int count = 0;
for (int i = 1; i <= N; i++) {
if (find(i) == i) { // 루트 노드일 경우 count 증가
count++;
}
}
System.out.println(count);
}
// Find 함수: 경로 압축을 적용하여 부모를 찾아주는 함수
private static int find(int x) {
if (parent[x] == x) {
return x;
}
return parent[x] = find(parent[x]); // 경로 압축
}
// Union 함수: 두 집합을 랭크에 따라 병합
private static void union(int a, int b) {
int rootA = find(a);
int rootB = find(b);
if (rootA != rootB) {
// 랭크를 비교하여 더 작은 트리를 더 큰 트리 밑에 병합
if (rank[rootA] > rank[rootB]) {
parent[rootB] = rootA; // rootB를 rootA 밑에 둠
} else if (rank[rootA] < rank[rootB]) {
parent[rootA] = rootB; // rootA를 rootB 밑에 둠
} else {
// 랭크가 같으면 아무 쪽이나 병합하고, 병합된 쪽의 랭크를 1 증가시킴
parent[rootB] = rootA;
rank[rootA]++;
}
}
}
}
- union()을 수행하면 똑같은 root를 바라보게 됨
- find() root를 반환함
예 : parent[1] = 1, parent[4] = 1, parent[5] = 4 인 상황에서 find(5)를 호출하면 5 → 4 → 1 즉, 1을 반환한다
이 부분에서 경로 압축이 발생함 - 랭크 개념 :
- 랭크 = 깊이 라고 생각할 수 있고, 같은 깊이끼리 병합할 때만 깊이가 한칸 깊어지도록 설계한다
- 만약 다른 랭크 끼리 병합한다면 더 큰 쪽으로 병합해버리면 깊이는 깊어지지 않는다
예를 들어 랭크 4까지 유니온과 랭크 2짜리 유니온이 병합할 때 2짜리 유니온이 루트아래로 붙어도 깊이는 3밖에 되지 않으니 랭크가 더 깊어지지 않는다.
'Java > 알고리즘(코테)' 카테고리의 다른 글
백준 14940번 : 쉬운 최단거리 (1) | 2024.11.09 |
---|---|
백준 1074번 : Z (0) | 2024.11.06 |
백준 30504번 : 세과영엔 슬픈 전설이 있어 (2) | 2024.10.13 |
백준 2805번 : 나무 자르기 (0) | 2024.10.09 |
백준 2630번 : 색종이 만들기 (0) | 2024.10.04 |