본문 바로가기

Dot Algo∙ DS/PS

[BOJ] 백준 1238번 파티 (Java)

#1238 파티

난이도 : 골드 3

유형 : 그래프 이론/ 다익스트라(권장) || 플로이드 와샬

 

1238번: 파티

첫째 줄에 N(1 ≤ N ≤ 1,000), M(1 ≤ M ≤ 10,000), X가 공백으로 구분되어 입력된다. 두 번째 줄부터 M+1번째 줄까지 i번째 도로의 시작점, 끝점, 그리고 이 도로를 지나는데 필요한 소요시간 Ti가 들어

www.acmicpc.net

▸ 문제

N개의 숫자로 구분된 각각의 마을에 한 명의 학생이 살고 있다.

어느 날 이 N명의 학생이 X (1 ≤ X ≤ N)번 마을에 모여서 파티를 벌이기로 했다. 이 마을 사이에는 총 M개의 단방향 도로들이 있고 i번째 길을 지나는데 Ti(1 ≤ Ti ≤ 100)의 시간을 소비한다.

각각의 학생들은 파티에 참석하기 위해 걸어가서 다시 그들의 마을로 돌아와야 한다. 하지만 이 학생들은 워낙 게을러서 최단 시간에 오고 가기를 원한다.

이 도로들은 단방향이기 때문에 아마 그들이 오고 가는 길이 다를지도 모른다. N명의 학생들 중 오고 가는데 가장 많은 시간을 소비하는 학생은 누구일지 구하여라.

 입력

첫째 줄에 N(1 ≤ N ≤ 1,000), M(1 ≤ M ≤ 10,000), X가 공백으로 구분되어 입력된다. 두 번째 줄부터 M+1번째 줄까지 i번째 도로의 시작점, 끝점, 그리고 이 도로를 지나는데 필요한 소요시간 Ti가 들어온다. 시작점과 끝점이 같은 도로는 없으며, 시작점과 한 도시 A에서 다른 도시 B로 가는 도로의 개수는 최대 1개이다.

모든 학생들은 집에서 X에 갈수 있고, X에서 집으로 돌아올 수 있는 데이터만 입력으로 주어진다.

 출력

첫 번째 줄에 N명의 학생들 중 오고 가는데 가장 오래 걸리는 학생의 소요시간을 출력한다.

 

문제 풀이 

N명의 학생이 X번 마을을 각자 최단 시간으로 오고가는데 가장 오래걸리는 사람을 구하는 문제이다. 이 부분을 읽고 바로 들어야하는 생각은 최단경로 알고리즘인 다익스트라 알고리즘과 플로이드 와샬 알고리즘이다.

 

🧑‍🏫 참고 (링크)

  • 다익스트라는 한 정점에서 다른 모든 정점의 최단 거리를 구하는 알고리즘
  • 플로이드 와샬은 모든 정점에서 다른 모든 정점의 최단 거리를 구하는 알고리즘

 

조건

  • N명 학생 : 1<= N <=1,000
  • M개 도로 : 1<= M <=10,000
  • T 시간 :  1<= T <=100

 

 

해당 문제에서는 한 정점(X번 마을)이 주어졌고 왕복이니 구해야 할 경로는 두 가지이다.

   1) 다른 모든 마을 -> X번 마을의 최단 거리,

   2) X번 마을 -> 다른 모든 마을의 최단거리

 

간선은 단방향이므로 다익스트라로 구하면 한 정점 -> 모든 정점이기 때문에 1), 2)를 각각 따로 계산해야 한다. 

   1)는 그대로 X번 마을로의 최단거리를 계산하고, 

   2)는 간선의 방향을 바꿔서 최단 거리를 구해야 한다.

 

☛ 왜냐하면 X번 마을에서 다른 모든 마을의 최단 거리를 그대로 구하면 N-1번의 연산을 돌려야하는데 바꿔서 생각하면 방향만 바꾸면 1)의 방식과 똑같이 X번 마을에서 다른 모든 마을의 최단거리를 한 번의 연산에 구할 수 있다.

 

그냥 플로이드 와샬 알고리즘을 사용하면 안되나? 지양해야한다. 플로이드 와샬은 모든 정점에서 다른 모든 정점의 최단거리를 한 번에 구해주는 알고리즘이기 때문이다. 단점이있다면 시간이 오래걸리는 것이다. O(n^3)이라서 그래프의 크기가 크면 시간초과가 발생할 것이다.

 

조건을 보고 최악의 경우 시간이 1,000^3 = 10억... O(n^3)은 n값이 최대 500정도까지가 안전하다고 하다. 코테나 효율성을 따져야하는 과정에서는 정점의 크기가 너무 큰 경우 플로이드 와샬 알고리즘은 사용하지 않는 것이 낫다.

 > 물론 무조건적인 것은 없다. 정점의 크기가 적당하고 모든 정점 최단 경로를 구하는 문제면 플로이드 와샬이 더 어울리는 경우도 있다.

 > 플로이드와샬로 풀기 최적화된 문제 보러가기

 

그래서 해당 문제는 다익스트라 알고리즘을 사용을 권장한다. 직접 플로이드 와샬과 다익스트라 알고리즘을 둘 다 사용하여 효율성을 비교해보았다. 

 

플로이드 와샬 풀이   

i : 시작 정점, j : 도착 정점, k : 중간 정점  

for(int k=1; k<n+1; k++) {
	for(int i=1; i<n+1; i++) {
		for(int j=1; j<n+1; j++) {
			if(memo[i][k] + memo[k][j] < memo[i][j]) {
				memo[i][j] = memo[i][k] + memo[k][j];
			}
		}
	}
}

 

플로이드 와샬은 3중 반복문으로 i -> k -> j의 경로를 모두 조회하여 최단 거리를 갱신해서 저장해준다. 시간복잡도는 O(n^3)을 가진다.

 

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

public class Main {

	static int INF = 1_000_000_000;
	
	public static void main(String[] args) throws IOException{
		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 x =  Integer.parseInt(st.nextToken());
		
		int[][] memo = new int[n+1][n+1];
		
		for(int i=1; i<n+1; i++) {
			for(int j=1; j<n+1; j++) {
				if(i==j) {
					memo[i][j] =0;
				}else {
					memo[i][j] = INF;
				}
			}
		}
		
		for(int i=0; i<m; i++) {
			st = new StringTokenizer(br.readLine());
			int from = Integer.parseInt(st.nextToken());
			int to = Integer.parseInt(st.nextToken());
			int cost = Integer.parseInt(st.nextToken());
			
			memo[from][to] = cost;
		}
		
		
		for(int k=1; k<n+1; k++) {
			for(int i=1; i<n+1; i++) {
				for(int j=1; j<n+1; j++) {
					if(memo[i][k] + memo[k][j] < memo[i][j]) {
						memo[i][j] = memo[i][k] + memo[k][j];
					}
				}
			}
		}
        
		int res = Integer.MIN_VALUE;
		for(int i=1; i<n+1; i++) {
			int dis = memo[i][x] + memo[x][i];
			if(dis >res ) {
				res = dis;
			}
		}
		
		System.out.println(res);
				
	}
	
	
}

 

 

 

다익스트라 풀이 (권장) 

한 정점을 시작으로 그에 대한 다른 모든 정점의 최단 거리를 구한다. 우선순위 큐를 사용하여 거리가 짧은 정점부터 조회하여 최단 거리를 찾는다.

static int[] dijkstra(List<Town>[] list, int start) {
	Queue<Town> q = new PriorityQueue<>();
	boolean[] check = new boolean[n+1];
	int[] dp = new int[n+1];
	Arrays.fill(dp, INF);
		
	q.add(new Town(start, 0));
	dp[start]=0;
		
	while(!q.isEmpty()) {
		Town pos = q.poll();
			
		int to = pos.to;
			
		if(check[to]) continue;		
		check[to] = true;
			
		for(Town next : list[to]) {
			if(dp[to] + next.cost < dp[next.to]) {
				dp[next.to] = dp[to] + next.cost;
				q.add(new Town(next.to, dp[next.to]));
			}
		}
	}		
	return dp;
}

 

 

미방문 노드 중 출발점으로부터 현재까지 계산된 최단 거리를 가지는 노드를 찾는데 O(VlogV)의 시간이 필요하고 각 노드마다 이웃한 노드의 최단 거리를 갱신할 때 O(ElogV)의 시간이 필요하여 총 시간 복잡도 O((V+E)logV)를 가진다.

 

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

class Town implements Comparable<Town>{
	int to;
	int cost;
	
	public Town(int to, int cost) {
		this.to = to;
		this.cost = cost;
	}

	@Override
	public int compareTo(Town o) {
		return this.cost - o.cost;
	}
	
}

public class Main {

	static int n,m,x;
	static List<Town>[] nList;
	static List<Town>[] rList;
	static int INF = 1_000_000_000;
	
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(br.readLine());
		
		n = Integer.parseInt(st.nextToken());
		m = Integer.parseInt(st.nextToken());
		x =  Integer.parseInt(st.nextToken());
		
		nList = new ArrayList[n+1];
		rList = new ArrayList[n+1];
		for(int i=0; i<n+1; i++) {
			nList[i] = new ArrayList<>();
			rList[i] = new ArrayList<>();
		}
		
		for(int i=0; i<m; i++) {
			st = new StringTokenizer(br.readLine());
			int from = Integer.parseInt(st.nextToken());
			int to = Integer.parseInt(st.nextToken());
			int cost = Integer.parseInt(st.nextToken());
			
			nList[from].add(new Town(to, cost));
			rList[to].add(new Town(from, cost));
		}
		
		// 다른 모든 마을 -> X 마을 거리
		int[] go = dijkstra(nList, x);
		// X 마을 -> 다른 모든 마을 
		int[] back = dijkstra(rList, x);
		
		int res = Integer.MIN_VALUE;
		for(int i=1; i<n+1; i++) {
			int dis = go[i] + back[i];
			
			if(dis > res) { 
				res = dis;
			}
		}
		
		System.out.println(res);
				
	}
	
	static int[] dijkstra(List<Town>[] list, int start) {
		Queue<Town> q = new PriorityQueue<>();
		boolean[] check = new boolean[n+1];
		int[] dp = new int[n+1];
		Arrays.fill(dp, INF);
		
		q.add(new Town(start, 0));
		dp[start]=0;
		
		while(!q.isEmpty()) {
			Town pos = q.poll();
			
			int to = pos.to;
			
			if(check[to]) continue;
			
			check[to] = true;
			
			for(Town next : list[to]) {
				if(dp[to] + next.cost < dp[next.to]) {
					dp[next.to] = dp[to] + next.cost;
					q.add(new Town(next.to, dp[next.to]));
				}
			}
		}
		
		return dp;
		
	}
	
	
}


 

실행결과

플로이드와샬 풀이 채점결과

 

다익스트라 풀이 채점결과

 

역시 예상대로 다익스트라 알고리즘이 효율성이 높은 것을 알 수 있다. 걸리는 시간의 차이가 무려 10배정도 플로이드 와샬 알고리즘이 느리다.