알고리즘

자바 - 백준 1865 / 웜홀 / Floyd-Warshall, Bellman-Ford 비교

kdozlo 2024. 10. 24. 10:35

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

골드3

구현 방법

"한 지점에서 출발을 하여서 시간여행을 하기 시작하여 다시 출발을 하였던 위치로 돌아왔을 때, 출발을 하였을 때보다 시간이 되돌아가 있는 경우" -> 음의 사이클이 있는지 판단해 달라는 문제였다. 벨만-포드 알고리즘을 사용하면 되지만, 문제 조건을 보면 정점의 범위가 1~500 사이이고, 테스트케이스 범위가 1 ~5이다. 따라서 O(n^3)의 시간 복잡도를 가진 플로이드-워셜 알고리즘으로도 충분히 풀 수 있다고 판단했다.(500 * 500 * 500 * 5 = 625,000,000)

 

Floyd-Warshall 풀이 방법

주의할 점

"두 지점을 연결하는 도로가 한 개보다 많을 수도 있다. "

=> 임의의 두 정점 사이의 가중치가 여러개 입력값으로 줄 수 있는 경우가 있어서 그 중 최솟값만 인접행렬에 업데이트 해야한다.

 

Bellman-Ford

주의할 점

음수 가중치의 경우 단방향이기 때문에 모든 정점을 시작점으로 음수 사이클을 찾아야 한다.

가중치 갱신 과정에서 (N -1)이하에서 갱신이 일어나지 않는다면 사이클이 존재하지 않는 것으로 판단할 수 있다. -> 속도 올라감

 

코드

Floyd-Warshall

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

/**
* 백준 1865
* 웜홀
* 골드3
* https://www.acmicpc.net/problem/1865
* Floyd-Warshall
*/
public class Boj1865_FloydWarshall {

    private static int INF = 10001;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();

        int TC = Integer.parseInt(br.readLine());

        while(TC-- > 0) {
            st = new StringTokenizer(br.readLine());

            int N = Integer.parseInt(st.nextToken()); //지점 수
            int M = Integer.parseInt(st.nextToken()); //도로 개수
            int W = Integer.parseInt(st.nextToken()); //웜홀 개수

            int[][] dist = new int[N + 1][N + 1];
            for(int i = 1; i <= N; i++) {
                Arrays.fill(dist[i], INF);
                dist[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 cost = Integer.parseInt(st.nextToken());

                dist[a][b] = Math.min(dist[a][b], cost);
                dist[b][a] = Math.min(dist[b][a], cost);
            }

            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 cost = Integer.parseInt(st.nextToken());

                dist[a][b] = -cost;
            }

            floydWarshall(dist, N);

            boolean isNegativeCycle = false;
            for(int i = 1; i <= N; i++) {
                if(dist[i][i] < 0) {
                    isNegativeCycle = true;
                    break;
                }
            }

            if(isNegativeCycle)
                sb.append("YES\n");
            else
                sb.append("NO\n");

        }

        System.out.print(sb);
    }


    public static void floydWarshall(int[][] dist, int n) {
        for(int k = 1; k <= n; k++) {
            for(int i = 1; i <= n; i++) {
                if (dist[i][k] == INF) continue;
                for(int j = 1; j <= n; j++) {
                    dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }
    }
}

 

Bellman-Ford

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

/**
 * 백준 1865
 * 웜홀
 * 골드3
 * https://www.acmicpc.net/problem/1865
 * Bellman-Ford
 */
public class Boj1865_BellmanFord {

    private static class Edge {
        int u; //시작 정점
        int v; //끝 정점
        int w; //가중치

        Edge(int u, int v, int w) {
            this.u = u;
            this.v = v;
            this.w = w;
        }
    }

    private static List<Edge> g;

    private static int INF = 10001;
    private static int N;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();

        int TC = Integer.parseInt(br.readLine());

        while(TC-- > 0) {
            st = new StringTokenizer(br.readLine());

            N = Integer.parseInt(st.nextToken()); //지점 수
            int M = Integer.parseInt(st.nextToken()); //도로 개수
            int W = Integer.parseInt(st.nextToken()); //웜홀 개수

            g = new ArrayList<>();

            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 cost = Integer.parseInt(st.nextToken());

                g.add(new Edge(a, b, cost));
                g.add(new Edge(b, a, cost));
            }

            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 cost = Integer.parseInt(st.nextToken());

                g.add(new Edge(a, b, -cost));
            }

            if(bellmanFord())
                sb.append("YES\n");
            else
                sb.append("NO\n");
        }

        System.out.print(sb);
    }

    public static boolean bellmanFord() {

        int[] dist = new int[N + 1];
        Arrays.fill(dist, INF);

        for(int i = 0; i < N - 1; i++) {
            boolean updated = false;
            for(Edge e : g) {
                if(dist[e.u] + e.w < dist[e.v]) {
                    dist[e.v] = dist[e.u] + e.w;
                    updated = true;
                }
            }

            if(!updated)
                return false;
        }

        for(Edge e : g) {
            if(dist[e.u] != INF && dist[e.u] + e.w < dist[e.v])
                return true;
        }

        return false;
    }
}

알고리즘 정리

Floyd-Warshall

- 모든 정점에서 모든 정점까지 최단 거리를 구함

- 음수 가중치 허용, 음수 사이클 비허용

- 시간복잡도 : O(n^3)

- 인접행렬로 표현

- 음수 사이클 탐지 가능 -> 대신 이 경우 올바른 최단 경로 계산 불가

  - 음수 사이클이 있는 경우 최단 경로 계산 불가

  - 어디서 음수 사이클이 발생하는지는 알 수 없음

  - 시작 정점과 끝 정점은 음수 사이클이 없는 이상 비용이 0 하지만 음수 사이클이 존재하는 경우 마이스너가 됨

     -> 이걸로 음수 사이클 판단

 

Bellman-Ford

- 음수 가중치 허용

- 시작 정점으로부터 각 점점까지 최단 경로 구하는 기법

- 음수 사이클 탐지 가능

- 시작점을 지정한 경우 -> 음의 사이클 판단 가능, 시작점 기준 최단 거리 구하기 가능

private static class Edge {
	int u; //시작 정점
	int v; //끝 정점
	int w; //가중치
	
	Edge (int u, int v, int w) {
	    this.u = u;
	    this.v = v;
	    this.w = w;
  }
}

// 시작점을 지정한 경우 "dist[e.u] != INF"가 필수!!
// 이유 : 비용 갱신 시 시작점으로 출발 했는지 확인하기 때문, 시작점의 비용은 0이기 때문
public static boolean bellmanFord(int start) {

        int[] dist = new int[N + 1];
        Arrays.fill(dist, INF);
        dist[start] = 0;

        for(int i = 0; i < N - 1; i++) {
            boolean updated = false;
            for(Edge e : g) {
                if(dist[e.u] != INF && [dist[e.u] + e.w < dist[e.v]) {
                    dist[e.v] = dist[e.u] + e.w;
                    updated = true;
                }
            }

            if(!updated)
                return false;
        }

        for(Edge e : g) {
            if(dist[e.u] != INF && dist[e.u] + e.w < dist[e.v])
                return true;
        }

        return false;
    }

 - 시작점 미지정 -> 음의 사이클 판단 가능, 최단 거리는 구할 수 있지만 어디서 출발한건지 모름

// 시작점 미지정, 주로 음의 사이클 판단용으로만 쓸 때 유용
public static boolean bellmanFord() {

        int[] dist = new int[N + 1];
        Arrays.fill(dist, INF);

        for(int i = 0; i < N - 1; i++) {
            boolean updated = false;
            for(Edge e : g) {
                if(dist[e.u] + e.w < dist[e.v]) {
                    dist[e.v] = dist[e.u] + e.w;
                    updated = true;
                }
            }

            if(!updated)
                return false;
        }

        for(Edge e : g) {
            if(dist[e.u] != INF && dist[e.u] + e.w < dist[e.v])
                return true;
        }

        return false;
    }

 

 

참고. 유니온 파인드 -> 사이클 판단 가능, 음수 사이클 판단은 불가

'알고리즘' 카테고리의 다른 글

자바 - 백준 2098 / 외판원 순회  (0) 2024.12.05
자바 - 코드트리 / 빙산의 일각  (0) 2024.10.29
자바 - 백준 2758 / 로또  (0) 2024.10.22
자바 - 백준 1106 / 호텔  (1) 2024.10.19
자바 - 백준 16120 / PPAP  (1) 2024.10.16