본문 바로가기

Dot Algo∙ DS/PS

[BOJ] 백준 10159번 저울 (Java)

#10159 저울

난이도 : 골드 3

유형 : 그래프/ 플로이드-와샬

 

10159번: 저울

첫 줄에는 물건의 개수 N 이 주어지고, 둘째 줄에는 미리 측정된 물건 쌍의 개수 M이 주어진다. 단, 5 ≤ N ≤ 100 이고, 0 ≤ M ≤ 2,000이다. 다음 M개의 줄에 미리 측정된 비교 결과가 한 줄에 하나씩

www.acmicpc.net

▸ 문제

무게가 서로 다른 N 개의 물건이 있다. 각 물건은 1부터 N 까지 번호가 매겨져 있다. 우리는 일부 물건 쌍에 대해서 양팔 저울로 어떤 것이 무거운 것인지를 측정한 결과표를 가지고 있다. 이 결과표로부터 직접 측정하지 않은 물건 쌍의 비교 결과를 알아낼 수도 있고 알아내지 못할 수도 있다. 예를 들어, 총 6개의 물건이 있고, 다음 5개의 비교 결과가 주어졌다고 가정하자. ([1]은 1번 물건의 무게를 의미한다.)

[1]>[2], [2]>[3], [3]>[4], [5]>[4], [6]>[5]

우리는 [2]>[3], [3]>[4]로부터 [2]>[4]라는 것을 알 수 있다. 하지만, 물건 2와 물건 6을 비교하는 경우, 앞서의 결과만으로는 어느 것이 무거운지 알 수 없다. 이와 같이, 물건 2는 물건 1, 3, 4와의 비교 결과는 알 수 있지만, 물건 5, 6과의 비교 결과는 알 수 없다. 물건 4는 모든 다른 물건과의 비교 결과를 알 수 있다. 

비교 결과가 모순되는 입력은 없다고 가정한다. 위 예제의 기존 측정 결과에 [3]>[1]이 추가되었다고 가정하자. 이 경우 [1]>[2], [2]>[3]이므로 우리는 [1]>[3]이라는 것을 예측할 수 있는데, 이는 기존에 측정된 결과 [3]>[1]과 서로 모순이므로 이러한 입력은 가능하지 않다. 

물건의 개수 N 과 일부 물건 쌍의 비교 결과가 주어졌을 때, 각 물건에 대해서 그 물건과의 비교 결과를 알 수 없는 물건의 개수를 출력하는 프로그램을 작성하시오. 

 

 입력

첫 줄에는 물건의 개수 N 이 주어지고, 둘째 줄에는 미리 측정된 물건 쌍의 개수 M이 주어진다. 단, 5 ≤ N ≤ 100 이고, 0 ≤ M ≤ 2,000이다. 다음 M개의 줄에 미리 측정된 비교 결과가 한 줄에 하나씩 주어진다. 각 줄에는 측정된 물건 번호를 나타내는 두 개의 정수가 공백을 사이에 두고 주어지며, 앞의 물건이 뒤의 물건보다 더 무겁다.

 

 출력

여러분은 N개의 줄에 결과를 출력해야 한다. i 번째 줄에는 물건 i 와 비교 결과를 알 수 없는 물건의 개수를 출력한다.

 

 

 

 

문제 풀이 🖋 

처음 풀이 설계는 위상정렬과 dfs탐색을 사용하여 해결하려고 했다. 그런데 시간초과를 당해버렸다. 

 

 

위상 순서를 메긴 다음에 루트 노드들을 뽑아서 DFS탐색을 통해 해당 루트 노드로 저울을 잴 수 있는 집합을 구하였다.

문제의 예시를 예로들면 루트 노드는 1과 6으로 나와서 DFS탐색을 통해 (1-2-3-4), (6-5-4)을 구하였는데 아마 DFS에서 시간 초과를 당한 것 같다. 

* 최악의 경우 조합(dfs코드)은 nCr에서 r의 크기가 커질수록 시간복잡도가 기하급수적으로 올라가기 떄문에 n값이 20이상 넘어가면 위험하다고 한다.

- 조합 시간 복잡도 O(n!)

 

↓ 실패 코드

더보기
import java.io.*;
import java.util.*;

public class Main {

	static int n;
	static int[] res;
	static List<Integer>[] list;
	static boolean[] check, dCheck;
	
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = null;
		StringBuilder sb = new StringBuilder();
		
		n = Integer.parseInt(br.readLine());
		int m = Integer.parseInt(br.readLine());
		
		int[] ranks = new int[n+1];
		res = new int[n+1];
		list = new ArrayList[n+1];
		for(int i=1; i<n+1; i++) {
			list[i] = new ArrayList<>();
			res[i] = n;
		}
		
		for(int i=0; i<m; i++) {
			 st = new StringTokenizer(br.readLine());
			 int a1 = Integer.parseInt(st.nextToken());
			 int a2 = Integer.parseInt(st.nextToken());
			 
			 list[a1].add(a2);
			 ranks[a2] += ranks[a1]+1;
		}
		
		dCheck= new boolean[n+1];
		for(int i=1; i<n+1; i++) {
			if(ranks[i] ==0) {
				check = new boolean[n+1];
				check[i] = true;
				dfs(i,1);
				check[i] = false;
			}
		}
        
		for(int i=1; i<n+1; i++) {
			System.out.println(res[i]);
		}
		System.out.println(sb.toString());
	}
	
	static void dfs(int pos, int cnt) {
		
		if(list[pos].size() ==0) {
			for(int i=1; i<n+1; i++) {
				if(check[i]) {
					if(dCheck[i]) {
						res[i]++;
					}
					res[i] -= cnt;
					dCheck[i]  = true;
				}
			}
			return;
		}
		
		for(int next :list[pos]) {
			check[next] = true;
			dfs(next, cnt+1);
			check[next] = false;
		}
	}
}

 

그래서 문제 힌트를 보니 플로이드 와샬이 적혀있었다. 이 문제에 최단경로를 구하는 그래프를 어떻게 활용하지? 생각하면서 지문을 다시 읽어보니 감이 와버렸다.

 

우리는 [2]>[3], [3]>[4]로부터 [2]>[4]라는 것을 알 수 있다. 

 

📚 조건

  1. 물건의 개수(정점) N ( 5 <= N <= 100)
  2. 물건 쌍의 개수(간선) M ( 0 <= M <= 2,000)
  3. 그래프에 싸이클은 존재하지 않는다. (1>3, 3>1 모순. 입력값 x)
  4. a1 > a2, a2> a3 이면 a1 > a3임을 알 수 있다.

 

풀이 순서는 1) 정점과 주어지는 간선을 입력한 후 2) 해당 데이터를 통해 비교가 가능한 정점을 모두 찾아 값을 넣어주면 된다.

 

1번 로직

정점과 정점사이의 value를 1, -1로 설정하자.

  • a1 > a2 일 때는 arr[a1][a2] = 1;
  • a1 < a2 일 때는 arr[a2][a1] = -1;

 

2번 로직

이제 값이 입력된 인접배열을 가지고 플로이드-와샬 알고리즘을 통해 각 물건간의 비교가 가능한지 측정해보자.

 

a1 > a2은  arr[a1][a2] =1 이고

a2 > a3은  arr[a2][a3]=1 이므로

 →  arr[a1][a3] = 1이 된다.

 

반대로도 마찬가지이다.

a1 < a2은  arr[a2][a1] = -1 이고

a2 < a3은  arr[a3][a2]= -1 이므로

 →  arr[a3][a1] = -1이 된다.

 

 

코드로 나타내면 다음과 같다.

// 2번 로직
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(arr[i][k] == 1 && arr[k][j] ==1) {
				arr[i][j] =1;
			}
			
			if(arr[i][k] == -1 && arr[k][j] == -1) {
				arr[i][j] = -1;
			}
		}
	}
}

 

 

최종 풀이 코드 ✔︎ 

플로이드 알고리즘의 핵심 개념은 i,j의 정점 사이를 거쳐가는 지점을 모두 조회하여 최단 경로를 찾는것이다.

 

arr[i][j] > arr[i][k] + arr[k][j] 

 

→ 이 문제에서는 개념을 모든 정점에서 모든 정점의 최단경로를 구하는 것이 아닌 모든 정점에 대한 관계를 조회하도록 만들었다는게 정말 좋은 개념 활용방법을 제시한 것 같다.

 

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

public class Main {

	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = null;
		StringBuilder sb = new StringBuilder();
		
		int n = Integer.parseInt(br.readLine());
		int m = Integer.parseInt(br.readLine());
		
		int[][] arr = new int[n+1][n+1];
		for(int i=1; i<n+1; i++) {
			arr[i][i] = 1;
		}
		for(int i=0; i<m; i++) {
			 st = new StringTokenizer(br.readLine());
			 int a1 = Integer.parseInt(st.nextToken());
			 int a2 = Integer.parseInt(st.nextToken());
			 
			 arr[a1][a2]=1; // a1 > a2;
			 arr[a2][a1]= -1; // a2 > a1;
		}
		
		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(arr[i][k] == 1 && arr[k][j] ==1) {
						arr[i][j] =1;
					}
					
					if(arr[i][k] == -1 && arr[k][j] == -1) {
						arr[i][j] = -1;
					}
				}
			}
		}
		
		for(int i=1; i<n+1; i++) {
			int cnt=0;
			for(int j=1; j<n+1; j++) {
				if(arr[i][j] != 0) {
					cnt++;
				}
			}
			sb.append((n-cnt)+"\n");
			
		}
		System.out.println(sb.toString());
	}
}