Prim 알고리즘
프림 알고리즘은 무향 그래프에서 MST(최소 스패닝 트리)를 찾는 알고리즘이다. 시작점에서 정점을 추가해 가면서 트리를 확장한다.
동작
정점 탐색 시 인접 정점 중 비용이 가장 작은 간선으로 연결된 정점을 선택해 연결한다
- 시작 정점을 MST에 추가한다
- MST 집합에 인접한 노드 중 최소 비용을 가지는 간선으로 연결된 노드를 선택, MST에 추가한다
- MST가 정점개수 - 1 개의 간선을 가질 때까지 반복한다
프림 알고리즘은 인접 행렬과 우선순위 큐를 사용해서 구현할 수 있다. 이번 포스팅에서는 SWEA 1251 하나로 문제로 우선순위 큐를 사용한 프림 알고리즘 코드를 구현해 본다
문제에서 요구하는 것은 환경 부담금을 최소로 하는 섬들의 연결을 찾아 총 환경 부담금을 찾는 문제였다. 다른 그래프 문제와 다른 점은 정점들 사이에 간선을 직접 구해야 한다. 하나의 정점과 나머지 정점 사이의 모든 간선을 고려해서 MST를 생성해야 하는 문제였다.
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
public class Solution {
static int tc, N;
static double E;
static double[][] matrix;
static boolean[] visited;
static class Node {
int no;
double weight;
public Node(int no, double weight) {
this.no = no;
this.weight = weight;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
tc = Integer.parseInt(br.readLine());
matrix = new double[2][N];
for (int i = 1; i <= tc; i++) {
N = Integer.parseInt(br.readLine()); // 노드 개수
for (int j = 0; j < 2; j++) { // 각 노드 입력 받기
matrix[j] = Arrays.stream(br.readLine().split(" ")).mapToDouble(Double::parseDouble).toArray();
}
E = Double.parseDouble(br.readLine()); // 가중치
// 입력 끝
List<Node>[] queues = new ArrayList[N];
visited = new boolean[N];
for (int j = 0; j < N; j++) {
queues[j] = new ArrayList<>();
for (int k = 0; k < N; k++) {
double dist = weightCalculate(matrix[0][j], matrix[1][j], matrix[0][k], matrix[1][k]); // 간선 값
queues[j].add(new Node(k, dist));
}
} // 각 점에 모든 점과의 거리 저장
Queue<Node> queue = new PriorityQueue<>((o1, o2) -> (int) (o1.weight - o2.weight));
queue.offer(new Node(0, 0));
int cnt = 0;
double result = 0;
while (!queue.isEmpty()) {
// 신장 트리의 구성에 포함되지 않은 정점 중 최소 비용 선택
Node miniVertex = queue.poll();
if (visited[miniVertex.no]) continue; // 방문 되었으면 다음 값 보기
// 신장 트리에 추가
visited[miniVertex.no] = true; // 방문 처리
result += miniVertex.weight; // 가중치 더해주기
if (++cnt == N) break; // 모든 노드를 확인 했다
// 신장 트리에 새롭게 추가되는 정점과 신장트리에 포함되지 않은 정점들의 기존 최소 비용과 비교
for (int j = 0; j < queues[miniVertex.no].size(); j++) {
Node tmp = queues[miniVertex.no].get(j);
if (!visited[tmp.no]) {
queue.add(tmp);
}
}
}
System.out.printf("#%d %.0f\n", i, result * E);
}
}
static double weightCalculate(double x, double y, double x2, double y2) {
double X = Math.abs(x - x2);
double Y = Math.abs(y - y2);
return X * X + Y * Y;
}
}
정점 개수 만큼의 연결리스트를 만들어 주어 하나의 정점과 나머지 정점 사이의 거리를 모두 계산해 준다.
- MST를 위한 우선 순위 큐를 생성해 주고 0번 노드를 추가한 후 진행한다.
- 방문한 노드 번호는 방문 처리를 해주고 가중치에 더해준다. 노드의 개수가 만족되면 종료해 준다.
- 현재 노드 번호의 연결 리스트에 담겨 있는 노드들을 확인하면서 방문되지 않은 노드들을 추가해 준다.
- 모든 노드들을 확인하게 되면 가중치의 최소 값이 나오게 된다.
Dijkstra 알고리즘
다익스트라 알고리즘은 DP를 사용하는 최단 경로 알고리즘이다. 최단 거리를 구하기 위해 이전 값을 사용하는 특징을 가지고 있다.
동작
- 시작 노드를 정한다
- 시작 노드를 기준으로 인접 노드의 최소 비용을 저장한다
- 방문하지 않은 노드 중에서 가장 적은 비용을 가진 노드를 선택한다
- 해당 노드를 거쳐 다음 노드로 가는 경우의 최소 비용을 갱신한다
- 3과 4를 반복한다
다익스트라 알고리즘을 사용하여 풀었던 백준 4485 녹색 옷 입은 애가 젤다지? 문제를 풀어본다.
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.StringTokenizer;
public class Main {
static int N, tc, X, Y;
static int[][] map;
static int[] dy = {0, 1, 0, -1};
static int[] dx = {1, 0, -1, 0};
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = -1;
while (true) {
tc++;
int N = Integer.parseInt(br.readLine());
if (N == 0) break;
map = new int[N][N];
for (int i = 0; i < N; i++) {
StringTokenizer st = new StringTokenizer(br.readLine(), " ");
for (int j = 0; j < N; j++) {
map[i][j] = Integer.parseInt(st.nextToken());
} // 각 테스트 케이스 입력 끝
}
Y = 0;
X = 0;
int D[][] = new int[N][N];
boolean[][] visited = new boolean[N][N];
for (int[] line :
D) {
Arrays.fill(line, Integer.MAX_VALUE);
}
D[Y][X] = map[Y][X];
Queue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
queue.offer(new int[]{Y, X, map[Y][X]});
visited[Y][X] = true;
int cnt = 0;
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int curY = cur[0];
int curX = cur[1];
int curVal = map[curY][curX];
for (int j = 0; j < 4; j++) {
int ny = curY + dy[j];
int nx = curX + dx[j];
if (ny >= 0 && nx >= 0 && ny < N && nx < N) {
if (D[ny][nx] > D[curY][curX] + map[ny][nx]) {
D[ny][nx] = D[curY][curX] + map[ny][nx];
queue.offer(new int[]{ny, nx, curVal + map[ny][nx]});
}
}
}
}
System.out.println("Problem " + tc + ":" + " " + D[N - 1][N - 1]);
}
}
}
이 문제는 주어진 지도에서 목적지(N-1, N-1)까지 최소의 비용으로 도달 하는 것이 목적이다. 시작 지점은 (0, 0)이다. 테스트 케이스 개수가 따로 주어지지 않으므로 while 문을 사용해 N이 0이 될때 종료하도록 해준다.
- 시작점은 0, 0이다
- 다익스트라 배열을 2차원 배열로 초기화 한다
- 우선 순위 큐에 시작 점과 빼야할 값을 함께 저장한다
- 4 방향 탐색을 하면서 다음 갈 다익스트라 배열에 저장된 거리와 현재 다익스트라 배열 거리 + 지도의 다음 거리를 비교해 다음 좌표가 더 가까우면(작으면) 다익스트라 배열을 갱신해 주고 큐에 다음 좌표를 추가해 준다.
- 반복해 주면 N-1, N-1 까지 가면서 최소 비용을 구할 수 있다.
- 다익스트라 배열의 N-1, N-1이 답이 된다.
Kruskal 알고리즘
크루스칼 알고리즘은 주어진 모든 정점을 가장 적은 비용으로 연결할 수 있다. 정점 사이에 사이클이 발생하면 안된다는 특징이 있다. 프로그래머스 섬 연결하기 문제로 크루스칼 알고리즘을 알아본다.
섬 연결하기 문제는 (start, end, 간선) 으로 정보가 주어진다. 간선의 최소를 만들 수 있도록 크루스칼 알고리즘을 사용한다
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.Arrays;
public class Solution {
static int n, cnt, cost;
static int[] cycle;
public static void main(String[] args) {
n = 4;
int[][] costs = \{\{0, 1, 1}, {0, 2, 2}, {1, 2, 5}, {1, 3, 1}, {2, 3, 8}};
cnt = 0;
// 사이클 테이블 선언
cycle = new int[n];
for (int i = 0; i < n; i++) {
cycle[i] = i;
}
// 비용 순으로 정렬
Arrays.sort(costs, ((o1, o2) -> o1[2] - o2[2]));
for (int i = 0; i < costs.length; i++) {
int[] item = costs[i];
// 사이클 테이블 갱신
int start = item[0]; // source
int end = item[1]; // dest
cost = item[2];
// 만약에 부모가 다르면 사이클이 안생기고 cost 를 더해주면서 연결해주면뎀
if (find(start) != find(end)) {
cnt += cost;
union(start, end);
}
}
System.out.println(cnt);
// return cnt;
}
private static int find(int node) {
if (cycle[node] == node) return node;
return cycle[node] = find(cycle[node]);
}
private static void union(int start, int end) {
int startParent = find(start);
int endParent = find(end);
if (startParent > endParent) cycle[startParent] = endParent;
else cycle[endParent] = startParent;
}
}
크루스칼 알고리즘은 부모 노드 확인을 위한 parent 배열과 노드를 연결 시킬 union-find 알고리즘을 사용한다
- 노드 개수 만큼 parent 배열을 초기화 해 준다. parent 배열의 초기화는 노드 자기 자신의 값으로 초기화 해준다
- 정보에 start와 end가 주어지는데 각 정점의 부모를 찾아 다르면 작은 부모 노드로 갱신하게 된다
- find 메서드로 현재 노드 값과 부모 값이 같으면 반환하고 그렇지 않으면 다시 부모를 찾는다
- start와 end의 부모가 같지 않으면, union(합집합) 메서드를 사용해 연결해 준다. 연결과 동시에 간선을 더해 준다
- union 연산은 각 노드의 부모를 찾은 후 더 작은 부모로 갱신한다
- 모든 동작이 끝나게 되면 parent 배열의 값이 하나로 통일 된다(모두 연결이 가능한 경우)