본문 바로가기

CS ﹒ Algorithm/Baekjoon

Java 백준 문제풀이 (59) 1931 _ 회의실 배정

 

그리디 알고리즘 입문 문제 중 하나로 꼽히는 회의실 배정 문제다.

2개월?3개월 쯤 전 막 알고리즘 문제를 풀기 시작했던 시점에 이 문제가 너무 어려워서 보다가 때려 치웠던 기억이 난다.

근데 지금 다시 풀어보니까 너무 쉽다.

눈 감고도 풀 수 있을 듯 ㅎ (불가능)

 

나 같이 알고리즘 기초 문제도 어려워서 내가 이렇게 지능이 낮았나 자괴감 들고 자존감이 뚝뚝 떨어지는 사람이 있다면 기운내자.

계속 풀어보면 다 별 것 없다. 지금까지 살아오면서 겪은 일들이 늘 그랬듯이 말이다.

 

예전에 어렵게 느낀 문제를 너무 쉽게 풀었더니 감회가 새로워서 잡소리가 길었다.

우선 문제를 읽고 분석해보자.

 

내가 처음 작성한 주석은 다음과 같다.

(오늘은 평소와 달리 글을 작성하면서 푼 게 아니라 다 풀고 작성하는 것이지만 시행착오는 그대로 넣었다.)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        // 각 회의마다 시작시간, 끝나는 시간 주어짐
        // 회의가 겹치지 않게 하면서 사용할 수 있는 회의 최대 개수
        // 회의는 도중에 중단될 수 없음
 
        // {1, 4} {5, 7} {6, 10} {2, 13}
        // {3, 5} {3, 8} {8, 11} {12, 14}
        // {0, 6} {5, 9} {8, 12}
 
        // 최소값이 0 최대값이 14
        // 1. n2가 작을 수록 좋다.
        // 2. n2 - n1이 작을 수록 좋다.
        // 3. n2와 다음 n1의 차이가 작을 수록 좋다.
 
        // 그리티 알고리즘은 "해당 순간에 가장 최선인 경우를 모두 탐색하는 것"
        // 1. 1-4 0-6 ? 1-4
        // 2. 5-7 5-9 ? 5-7
        // 3. 8-11, 8-12 ? 8-11
        // 4. 12-14
 
        // 시작 시간과 끝나는 시간은 2^31-1 == 정수형(Integer)의 범위를 의미한다.
cs

 

이 문제는 어렵게 생각하면 끝도 없이 어려울 수 있지만, 그리디 알고리즘의 의의를 생각해보면 쉽게 풀이를 생각할 수 있다.

그리디 알고리즘은 탐욕 알고리즘이라는 이름에서 보이듯이 미래 같은 건 내 알 바 아니고, 무조건 최적의 해만 찾아가면 그만이다.

 

고난이도 문제에서는 다른 알고리즘들이 섞이면서 복잡해지지만, 적어도 이런 눈에 보이는 문제는 우리 상식 선에서 생각할 수 있는 최적의 해를 찾는 방법이 정답이라는 말이다.

 

1. 주어진 값 n1 n2를 정수형 배열 arr의 원소라고 생각했을 때, n2가 작다면 무조건 좋다.

 => 회의가 빨리 끝난다면 당연히 더 많은 팀원들이 회의실을 사용할 확률이 높아진다.

 

2. 이어지는 배열의 n1의 차이가 n2가 작을 수록 좋다.

=> 회의가 끝나자마자 해당 회의실을 다른 팀이 사용하는 것이 효율적이다.

 

이 두 가지 상식적인 원칙만 생각하면 나머지는 구현의 문제다.

마지막으로 시작 시간과 끝나는 시간은 2^31이라고 하는데, 이것은 정수형 범위를 의미한다.

 

이제 위 조건들을 활용해서 야매 수도코드를 작성해보자.

 

 

 

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
 
public class Main {
    public static void main(String[] args) throws Exception {
        // 각 회의마다 시작시간, 끝나는 시간 주어짐
        // 회의가 겹치지 않게 하면서 사용할 수 있는 회의 최대 개수
        // 회의는 도중에 중단될 수 없음
 
        // {1, 4} {5, 7} {6, 10} {2, 13}
        // {3, 5} {3, 8} {8, 11} {12, 14}
        // {0, 6} {5, 9} {8, 12}
 
        // 최소값이 0 최대값이 14
        // 1. n2가 작을 수록 좋다.
        // 2. n2 - n1이 작을 수록 좋다.
        // 3. n2와 다음 n1의 차이가 작을 수록 좋다.
 
        // 그리티 알고리즘은 "해당 순간에 가장 최선인 경우를 모두 탐색하는 것"
        // 1. 1-4 0-6 ? 1-4
        // 2. 5-7 5-9 ? 5-7
        // 3. 8-11, 8-12 ? 8-11
        // 4. 12-14
 
        // 시작 시간과 끝나는 시간은 2^31-1 == 정수형(Integer)의 범위를 의미한다.
 
        // 회의의 수 N을 입력 받는다.
 
        // 반복문 (i<N)
            // 우선 순위 큐(정렬 기준 n2).push(배열)
 
        // int answer = findBestValue(큐)
 
        // 출력
    }
 
    public static int findBestValue(Queue<int[]> queue) {
        // count = 0
        // arr = null
 
        // while (!queue.empty) {
            // temp = queue.poll
            // if ( arr에 등록된 배열의 n2보다 temp의 n1이 크다면 )
                // arr = temp;
                // count ++;
            // }
        // 반환
    }
}
cs

 

우선 우리는 회의가 빨리 끝나는 순서(n[1])대로 정렬하는 것이 중요한데, 나는 우선순위 큐를 사용했다.

Arrays.sort()가 아닌 우선순위 큐를 사용한 이유는 그냥 생각이 안 났기 때문이다. ㅎ

어느 방법을 사용하던지 별 차이는 없다.

 

둘 다 Comparator도 재정의해야 하고, 코드 길이도 비슷하니 익숙한 방법을 택하자.

단, PriorityQueue가 생각외로 여기저기 쓰이는 낭낭한 친구라서 익숙하지 않다면 배워놓는 게 좋다.

 

아무튼 정렬이 끝났다면 findBesetValue라는 함수로 들어간다.

무조건 끝나는 시간(arr[1])이 빠른 배열부터 반환되기 때문에 그 다음에는 시간 겹치지 않으면서 먼저 나오는 순서대로 죄다 체크하면 된다.

 

근데, 이대로만 풀면 틀린다.

위 주석에 고려하지 못한 부분이 2가지 있다.

 

1. 앞 팀이 나가자마자 다음 팀이 들어올 수 있다.

2. 어떤 팀은 들어가자마자 나갈 수 있다.(대체 왜?)

 

따라서 해당 부분을 수정만 하면.. 되는 게 아니라 아직 하나 더 있다.

Comparator가 익숙하지 않은 사람을 위해 이 부분은 바로 알려줄 예정인데 모두 스스로 풀고 싶으면 이제 나가삼

나도 Comparator는 여러번 쓰면서도 계속 헷갈린다.

 

 

 

 

 

1
2
3
4
5
6
        Queue<int[]> queue = new PriorityQueue<>((n1, n2) -> {
            if (n1[1] == n2[1]) {
                return n1[0] - n2[0];
            }
            else  return n1[1] - n2[1];
        });
cs

( 만약 회의 끝나는 시간이 같다면, 시작 시간이 빠른 배열을 앞으로 정렬하는 방법 )

( 재정의하지 않을 시 가장 마지막 수를 기준으로만 정렬함 )

 

람다를 모르면 자바의 정석 사서 자바를 다시 공부하자..

Arrays.sort()를 활용하든 PriorityQueue를 활용하든 Comparator를 재정의하는 방법은 별 차이가 없다.

 

아마 Arrays.sort(array, () -> { 어쩌고 저쩌고 })이러면 될 것 같은데 안해봐서 모름

아무튼 Comparator는 이해하려고 하면 헷갈린다. 그냥 외워라.

 

람다 파라미터 ( 첫번째놈, 두번째놈 )를 정하고

return 첫번째놈 - 두번째놈은 오름차순이다.

return 두번째놈 - 첫번째놈은 내림차순이다.

 

이유? 없다 그냥 약속이다.

있을지도 모르지만 우리 알 바 아니다. 일방적으로 정해진 약속이니까.

근데 조금만 더 깊게 들어가서 얘기하자면 사실 이게 좋은 방법은 아니다.

왜? 정수형 overflow나 underflow가 발생할 수 있기 때문이다. 뭔지 모르면 검색해보셈.

정석적으로 하자면 return if (n[1] == n2[1]) return Integer.compare(n1, n2)이렇게 해주면 좋다.

 

근데 지금까지 저렇게 했다고 오버플로우나 언더플로우 발생해서 틀려본 적은 없음. 그냥 TMI로 알려드림 ㅎ

이제 풀어보삼

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
 
public class Main {
    public static void main(String[] args) throws Exception {
        // 각 회의마다 시작시간, 끝나는 시간 주어짐
        // 회의가 겹치지 않게 하면서 사용할 수 있는 회의 최대 개수
        // 회의는 도중에 중단될 수 없음
 
        // {1, 4} {5, 7} {6, 10} {2, 13}
        // {3, 5} {3, 8} {8, 11} {12, 14}
        // {0, 6} {5, 9} {8, 12}
 
        // 최소값이 0 최대값이 14
        // 1. n2가 작을 수록 좋다.
        // 2. n2 - n1이 작을 수록 좋다.
        // 3. n2와 다음 n1의 차이가 작을 수록 좋다.
 
        // 그리티 알고리즘은 "해당 순간에 가장 최선인 경우를 모두 탐색하는 것"
        // 1. 1-4 0-6 ? 1-4
        // 2. 5-7 5-9 ? 5-7
        // 3. 8-11, 8-12 ? 8-11
        // 4. 12-14
 
        // 시작 시간과 끝나는 시간은 2^31-1 == 정수형(Integer)의 범위를 의미한다.
 
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 회의의 수 N을 입력 받는다.
        int N = Integer.parseInt(br.readLine());
 
        Queue<int[]> queue = new PriorityQueue<>((n1, n2) -> {
            if (n1[1== n2[1]) {
                return n1[0- n2[0];
            }
            else  return n1[1- n2[1];
        });
        // 반복문 (i<N)
        for (int i=0; i<N; i++) {
            // 우선 순위 큐(정렬 기준 n2).push(배열)
            int[] arr = Arrays.stream(br.readLine().split(" ")).mapToInt(e -> Integer.parseInt(e)).toArray();
            queue.add(arr);
        }
 
        // int answer = findBestValue(큐)
        int answer = findBestValue(queue);
        // 출력
        System.out.println(answer);
    }
 
    public static int findBestValue(Queue<int[]> queue) {
        // count = 0
        int count = 0;
        // arr = null
        int[] arr = null;
        // while (!queue.empty) {
        while (!queue.isEmpty()) {
            // temp = queue.poll
            int[] temp = queue.poll();
            // if ( arr에 등록된 배열의 n2보다 temp의 n1이 크다면 )
            if (arr == null || arr[1<= temp[0]) {
                // arr = temp;
                arr = temp;
                // count ++;
                count++;
            }
            // }
        }
        // 반환
        return count;
    }
}
cs

늘 그렇듯 주석은 무시하자.