본문 바로가기

알고리즘/Do it! 알고리즘 코딩 테스트

플로이드-워셜

플로이드-워셜 (floyd-warshall)

그래프에서 최단 거리를 구하는 알고리즘. 주요 특징은 다음과 같다.

 

기능 특징 시간 복잡도 (노드 수: V)
모든 노드 간에 최단 경로 탐색 - 음수 가중치 에지가 있어도 수행할 수 있음.
- 동적 계획법의 원리를 이용해 알고리즘에 접근
O(V^3)

 

 

플로이드-워셜의 핵심 이론

플로이드-워셜 알고리즘을 도출하는 가장 핵심적인 원리는 A 노드에서 B 노드까지 최단 경로를 구했다고 가정했을 때 최단 경로 위에 K 노드가 존재한다면 그것을 이루는 부분 경로 역시 최단 경로라는 것이다.

 

색칠된 에지 경로가 1 -> 5 최단 경로라면 1 -> 4 최단 경로와 4 -> 5 최단 경로 역시 색칠된 에지로 이뤄질 수밖에 없다. 즉, 전체 경로의 최단 경로는 부분 경로의 최단 경로의 조합으로 이뤄진다는 의미가 된다. 이 원리로 다음과 같은 점화식 도출이 가능하다.

도출한 플로이드-워셜 점화식
- D[S][E] = Math.min(D[S][E], D[S][K] + D[K][E])

 

 

 

플로이드-워셜 알고리즘 구현 방법은 다음과 같다.

 

1. 배열을 선언하고 초기화하기

D[S][E]는 노드 S에서 노드 E까지의 최단 거리를 저장하는 배열이라 정의한다. S와 E의 값이 같은 칸은 0, 다른 칸은 로 초기화한다. 여기에서 S == E는 자기 자신에게 가는 데 걸리는 최단 경로값을 의미하기 때문이다.

 

 

2. 최단 거리 배열에 그래프 데이터 저장하기

출발 노드는 S, 도착 노드는 E, 이 에지의 가중치는 W라고 했을 때 D[S][E] = W로 에지의 정보를 배열에 입력한다. 이로써 플로이드-워셜 알고리즘은 그래프를 인접 행렬로 표현한다는 것을 알 수 있다.

 

3. 점화식으로 배열 업데이트하기

기존에 구했던 점화식을 3중 for 문의 형태로 반복하며 배열의 값을 업데이트한다.

플로이드-워셜 알고리즘 로직

for 경유지 K에 관해 (1 ~ N)       // N : 노드 개수
    for 출발 노드 S에 관해 (1 ~ N)
        for 도착 노드 E에 관해 (1 ~ N)
        D[S][E] = Math.min(D[S][E], D[S][K] + D[K][E])

 

완성된 배열은 모든 노드 간의 최단 거리를 알려준다. 예를 들어 1번 노드에서 5번 노드까지 가는 최단 거리는 D[1][5] = 6으로 나타낸다. 

 

 

 

플로이드-워셜 알고리즘은 모든 노드 간의 최단 거리를 확인해 주기 때문에 시간 복잡도가 O(V^3)으로 빠르지 않은 편이다. 그래서 일반적으로 플로이드-워셜 알고리즘을 사용해야 하는 문제가 나오면 노드의 개수의 범위가 다른 그래프에 비해 적게 나타난다.

 


 

[백준 11404번] 플로이드

https://www.acmicpc.net/problem/11404

 

 

모든 도시의 쌍과 관련된 최솟값을 찾아야 하는 문제이다. 그래프에서 시작점을 지정하지 않고, 모든 노드와 관련된 최소 경로를 구하는 알고리즘이 바로 플로이드-워셜 알고리즘이다. 도시의 최대 개수가 100개로 매우 작은 편이므로 O(N^3) 시간 복잡도의 플로이드-워셜 알고리즘으로 해결할 수 있다. 

 

 

1. 버스 비용 정보를 인접 행렬에 저장한다. 먼저 인접 행렬을 초기화하는데, 연결 도시가 같으면 (i==j) 0, 아니면 충분히 큰 수로 값을 초기화한다. 그리고 주어진 버스 비용 데이터 값을 인접 행렬에 저장한다.

 

 

 

2. 플로이드-워셜 알고리즘을 수행한다. 다음 점화식을 활용한 3중 for 문으로 모든 중간 경로를 탐색한다.

// 두 도시의 연결 비용 중 최솟값
Math.min(distance[S][E], distance[S][K] + distance[K][E])

 

 

3. 알고리즘으로 변경된 인접 행렬을 출력한다. 인접 행렬 자체가 모든 쌍의 최단 경로를 나타내는 정답 배열이므로 정답 배열을 그대로 출력하되, 문제의 요구사항에 따라 두 도시가 도달하지 못할 때()는 0, 아닐 때는 배열의 값을 출력한다.

 

 

[정답]

import java.io.*;
import java.util.*;

public class Main {
    static int INF = 10000000;      // 충분히 큰 수로 저장
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        int n = Integer.parseInt(br.readLine());
        int m = Integer.parseInt(br.readLine());
        int[][] distance = new int[n+1][n+1];
        // 인접 행렬 초기화하기
        for (int[] d : distance) {
            Arrays.fill(d, INF);
        }
        for (int i = 1; i <= n; i++) {
            distance[i][i] = 0;
        }
        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());
            distance[a][b] = Math.min(distance[a][b], c);
        }

        // 플로이드-워셜 알고리즘 수행하기
        for (int k = 1; k <= n; k++) {
            for (int s = 1; s <= n; s++) {
                for (int e = 1; e <= n; e++) {
                    distance[s][e] = Math.min(distance[s][e], distance[s][k] + distance[k][e]);
                }
            }
        }

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if(distance[i][j] == INF) System.out.print(0 + " ");
                else System.out.print(distance[i][j] + " ");
            }
            System.out.println();
        }
    }
}

 

여기서 주의할 점은 INF의 값을 Integer.MAX_VALUE로 하면 이 값을 다른 값과 더하며 오버플로우가 발생해 마이너스 값이 나온다는 것이다. 최대 비용이 100,000 이니까 100001로 잡으면 틀리게 되는데, 만약 A -> B의 비용이 100000 이고 B -> C의 비용이 1이라고 했을 때 A -> C 의 경로값이 100001, 즉 INF로 인식하게 되어 버리기 때문이다. 그러므로 충분히 큰 값으로 INF를 잡아야 한다.

 


 

[백준 11403번] 경로 찾기

https://www.acmicpc.net/problem/11403

 

 

플로이드-워셜 알고리즘을 이해하고 있으면 쉽게 풀 수 있다.

 

 

[정답]

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        int[][] distance = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {      // 입력되는 인접 행렬의 값을 그대로 저장
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 1; j <= N; j++) {
                distance[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        for (int k = 1; k <= N; k++) {  // 변형된 플로이드-워셜 알고리즘 수행
            for (int s = 1; s <= N; s++) {
                for (int e = 1; e <= N; e++) {
                    if(distance[s][k] == 1 && distance[k][e] == 1)
                        distance[s][e] = 1;
                }
            }
        }

        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                System.out.print(distance[i][j] + " ");
            }
            System.out.println();
        }
    }
}

 

 


 

[백준 1389번] 케빈 베이컨의 6단계 법칙

https://www.acmicpc.net/problem/1389

 

 

BFS 탐색 알고리즘을 이용해도 해결할 수 있는 문제이지만 유저의 최대 수가 100 정도로 작기 때문에 플로이드-워셜 알고리즘으로도 해결할 수 있다. 

 

 

 

[정답]

import java.io.*;
import java.util.*;

public class Main {
    static int INF = 100000;    // 충분히 큰 수
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());
        int[][] distance = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {      // 인접 행렬 초기화
            for (int j = 1; j <= N; j++) {
                if(i == j) distance[i][j] = 0;
                else distance[i][j] = INF;
            }
        }
        for (int i = 0; i < M; i++) {   // 친구 관계이므로 양방향 저장, 가중치 1로
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            distance[a][b] = 1;
            distance[b][a] = 1;
        }

        for (int k = 1; k <= N; k++) {  // 플로이드-워셜 알고리즘 수행
            for(int s = 1; s <= N; s++) {
                for (int e = 1; e <= N; e++) {
                    distance[s][e] = Math.min(distance[s][e], distance[s][k] + distance[k][e]);
                }
            }
        }
        int bacon = INF;
        int bacon_person = -1;
        for (int i = 1; i <= N; i++) {
            int total = 0;
            for (int j = 1; j <= N; j++) {
                total += distance[i][j];
            }
            if(bacon > total) {     // 가장 작은 케빈 베이컨의 수를 지니고 있는 i 찾기
                bacon = total;
                bacon_person = i;
            }
        }
        System.out.println(bacon_person);
    }
}

 

 

 

 

[참고 자료]

- Do it! 알고리즘 코딩 테스트 자바편

'알고리즘 > Do it! 알고리즘 코딩 테스트' 카테고리의 다른 글

벨만-포드  (2) 2024.06.21
다익스트라  (1) 2024.06.15
위상 정렬  (1) 2024.06.11
유니온 파인드  (0) 2024.06.07
그래프의 표현  (1) 2024.06.04