벨만-포드 알고리즘
개념
최단 경로를 구하는 알고리즘으로 음의 가중치가 있는 경우에도 사용할 수 있는 알고리즘이다.
주로 음수 가중치가 있는 그래프에서 최단 경로를 찾거나, 사이클 유무를 판별하는 데 사용한다.
작동 원리
- 반복적인 최단 거리 갱신: 각 간선을 반복적으로 확인하고, 최단 경로가 발견될 때마다 경로를 갱신한다.
- 음수 사이클 탐지:
N-1
번의 반복 후에도 경로가 더 줄어드는 경우, 음수 사이클이 존재한다고 판단한다. - 시간 복잡도:
O(V * E)
로, 모든 간선을 반복 확인하기 때문에 크기가 큰 그래프에서는 효율성이 떨어진다.
구현 코드 - JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for (int i = 0; i < V - 1; i++) { // V-1번(자신의 노드를 제외한 탐색) 반복
for (Edge edge : edges) {
int u = edge.start; // 시작점
int v = edge.end; // 도착지
int weight = edge.weight; // 가중치
// 경유지 경로가 있고, 도착지로 바로 가는 것보다 경유지를 거쳐 가는 비용이 더 적을 경우
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
// 음수 사이클 체크
for (Edge edge : edges) {
int u = edge.start;
int v = edge.end;
int weight = edge.weight;
// 이미 최소거리를 구했는데 한 번 더 업데이트가 된다면 음수 사이클이 존재함.
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
System.out.println("음수 사이클이 존재합니다.");
break;
}
}
문제
해결 방법
- 도로를 입력받으므로 무방향 그래프처럼 양쪽에 입력한다.
- 웜홀의 경우 단방향 그래프로 한쪽에만 입력한다.
- 벨만-포드 알고리즘을 사용하여 한 위치에서 시작하는 최단 거리를 구한다.
- 하나의 노드에서만 탐색해도 음의 사이클이 존재하는지 확인할 수 있으므로 시간초과를 막기 위해 1번 노드에서 탐색하도록 구현한다.
- 최단 거리로 업데이트된 내용에서 탐색을 한 번 더 진행하여 값이 변경된다면 음의 사이클을 가지고 있으므로
YES
를 출력하도록 한다.
정답 코드 - JAVA
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
81
82
83
84
85
86
87
88
89
90
91
92
import java.io.*;
import java.util.*;
public class Main {
static int N;
static int M;
static int W;
static int[][] graph;
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringBuilder sb = new StringBuilder();
int TC = Integer.parseInt(br.readLine());
while (TC > 0) {
StringTokenizer st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
W = Integer.parseInt(st.nextToken());
graph = new int[N + 1][N + 1];
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
graph[i][j] = (int)1e9;
}
}
for (int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
int c = Integer.parseInt(st.nextToken());
graph[a][b] = Math.min(graph[a][b], c);
graph[b][a] = Math.min(graph[b][a], c);
}
for (int i = 0; i < W; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
int c = Integer.parseInt(st.nextToken());
graph[a][b] = Math.min(graph[a][b], -c);
}
if (bellmanFord()) {
sb.append("YES").append("\n");
} else {
sb.append("NO").append("\n");
}
TC--;
}
bw.write(String.valueOf(sb));
bw.newLine();
bw.flush();
}
static boolean bellmanFord() {
int[] dist = new int[N + 1];
for (int i = 1; i <= N; i++) {
dist[i] = Integer.MAX_VALUE;
}
dist[1] = 0;
boolean isUpdated = false; // 경로 갱신 여부
for (int i = 1; i < N; i++) { // 현재 노드를 제외한 나머지 노드만큼 반복
isUpdated = false;
for (int v = 1; v <= N; v++) { // 경유지
for (int u = 1; u <= N; u++) { // 도착점
if (dist[u] > dist[v] + graph[v][u]) {
dist[u] = dist[v] + graph[v][u];
isUpdated = true; // 경로가 갱신됨
}
}
}
// 경로가 갱신된적이 없다면 불가능하므로 탐색종료
if (!isUpdated) {
return false;
}
}
if (isUpdated) { // 갱신되었다면 음의 사이클 확인
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
if (dist[i] + graph[i][j] < dist[j]) {
return true;
}
}
}
}
return false;
}
}
오늘의 회고
벨만-포드 알고리즘에 대해 다시 공부하고 이를 문제에 적용했다. 처음에는 전체 노드에서 탐색을 시도하다 시간 초과가 발생하였고, 인터넷의 도움을 받아 하나의 노드에서만 검색해도 음의 사이클이 존재하는지 확인할 수 있다는 것을 알게 되었다.