Published on
👁️

회의실 배정 ⭐

Authors
  • avatar
    Name
    River
    Twitter
회의실 배정문제 보기
Gold5
백준📅 2025-07-09
그리디정렬우선순위 큐
문제 설명

한 개의 회의실이 있는데 이를 사용하고자 하는 N개의 회의에 대하여 회의실 사용표를 만들려고 한다. 각 회의 I에 대해 시작시간과 끝나는 시간이 주어져 있고, 각 회의가 겹치지 않게 하면서 회의실을 사용할 수 있는 회의의 최대 개수를 찾아보자. 단, 회의는 한번 시작하면 중간에 중단될 수 없으며 한 회의가 끝나는 것과 동시에 다음 회의가 시작될 수 있다. 회의의 시작시간과 끝나는 시간이 같을 수도 있다. 이 경우에는 시작하자마자 끝나는 것으로 생각하면 된다.

제한사항
  • N의 범위는 1 이상 100,000 이하
  • 시작 시간, 끝나는 시간의 범위는 0 이상 2^31-1 이하
입출력 예
입력
11
1 4
3 5
0 6
5 7
3 8
5 9
6 10
8 11
8 12
2 13
12 14
출력
4
설명

(1,4), (5,7), (8,11), (12,14) 를 이용할 수 있다.

풀이 접근법
시간복잡도 : O(N log N)
공간복잡도 : O(N)
사용 알고리즘 :
그리디정렬우선순위 큐
핵심 포인트 :
  • 끝나는 시간 기준 오름차순 정렬
  • 종료시간이 같을 때는 시작시간 기준 오름차순 정렬 ⭐
  • 가장 빨리 끝나는 회의부터 선택하는 그리디 전략
경계값/반례 :
  • 회의의 시작시간과 끝나는 시간이 같은 경우
  • 종료시간이 같은 회의들이 여러 개 있는 경우

핵심 아이디어

  • 가장 빨리 끝나는 회의부터 선택하는 그리디 알고리즘
  • 끝나는 시간이 빠른 순서로 정렬한 후, 각 회의를 순차적으로 검토하면서 이전 회의 종료 시간과 겹치지 않는 회의만 선택
  • 종료시간이 같을 때는 시작시간이 빠른 순서로 정렬하여 최적해 보장
  • 우리가 선택한 회의의 끝나는 시간이 곧 체크 타임이다.

해결 방법

  1. 회의를 끝나는 시간 기준 오름차순 정렬
  2. 이후 제거를 편하게 하기 위해 우선순위 큐 사용
  3. 첫 번째 회의 이후 다음으로 끝나는 시간이 빠른 회의를 꺼낸다.
  4. 꺼낸 회의의 시작 시간이 이미 지났으면 그냥 버린다.
  5. 아니라면, 해당 회의를 진행하고, 끝나는 시간을 다음 체크 타임으로 지정한다.
내부 해부 과정
tables = [[1,4], [3,5], [0,6], [5,7], [3,8], [5,9], [6,10], [8,11], [8,12], [2,13], [12,14]]
  1. 11개의 회의 데이터가 들어온다.
  2. 끝나는 시간을 기준으로 오름차순 정렬한다.
  3. tables = [[1,4], [3,5], [0,6], [5,7], [3,8], [5,9], [6,10], [8,11], [8,12], [2,13], [12,14]]
  4. checkTime = 0으로 초기화하고 회의를 하나씩 검토한다.
  5. 선택 과정
    • [1,4] : 0 ≤ 1 ⇒ ⭕ count=1, checkTime=4
    • [3,5] : 4 ≤ 3 → 선택 안함 (이미 4시까지 회의 중)
    • [0,6] : 4 ≤ 0 → 선택 안함
    • [5,7] : 4 ≤ 5 ⇒ ⭕ count=2, checkTime=7
    • [3,8] : 7 ≤ 3 → 선택 안함
    • [5,9] : 7 ≤ 5 → 선택 안함
    • [6,10] : 7 ≤ 6 → 선택 안함
    • [8,11] : 7 ≤ 8 ⇒ ⭕ count=3, checkTime=11
    • [8,12] : 11 ≤ 8 → 선택 안함
    • [2,13] : 11 ≤ 2 → 선택 안함
    • [12,14] : 11 ≤ 12 ⇒ ⭕ count=4, checkTime=14
  6. 결과 : 총 4개의 회의를 진행할 수 있다.
생각 과정

tables = [[1,4], [3,5], [0,6], [5,7], [3,8]]

❌ 처음 시도한 정렬 방식들

  1. 시작 시간 정렬 : tables = [[0,6], [1,4], [3,5], [3,8], [5,7]]
    • 문제점 : 끝나는 시간이 크다면 너무 많은 경우를 포기해야 한다.
  2. 걸리는 시간 정렬 : tables = [[3,5], [5,7], [1,4], [3,8], [0,6]]
    • 문제점 : 앞뒤 시간에 올 수 있는 여러 경우의 수를 모두 탐색해야 하므로 비효율적
  3. 끝나는 시간 정렬 : tables = [[1,4], [3,5], [0,6], [5,7], [3,8]]
    • ✅ 최적 : 빠르게 끝나는 것을 먼저 확보하여 최대한 많은 수를 선택하기에 알맞다.

💡 1차 제출 후 발견한 문제점

  • 예시

    7
    1 4
    3 5
    6 6
    5 6
    0 6
    5 7
    3 8
    
  • 문제에서 나와있듯 "회의의 시작시간과 끝나는 시간이 같을 수도 있다" 즉, 만약에 입력이 위와 같다면,

    끝나는 시간 오름차순 정렬 : tables = [[1,4], [3,5], [6,6], [5,6], [0,6], [5,7], [3,8]]

    • 1 : [1,4] ⇒ 통과
    • 2 : [3,5] ⇒ X (checktime : 4 > starttime : 3)
    • 3 : [6,6] ⇒ 통과
    • 4 : [5,6] ⇒ X (checktime : 6 > starttime : 5)
    • 5 : [0,6] ⇒ X (checktime : 6 > starttime : 0)
    • 6 : [5,7] ⇒ X (checktime : 6 > starttime : 5)
    • 7 : [3,8] ⇒ X (checktime : 6 > starttime : 3)
    • 총 2개의 회의를 진행할 수 있다.

  • 먼저 [5,6] 진행한 이후에도 [6,6]은 진행할 수 있다.

  • 다만 직접 만든 예시처럼 데이터가 들어왔을 때 끝나는 시간으로만 정렬한다면, 저런 요소를 놓치게 된다.

  • 시작 시간에 대해서 추가 정렬이 필요하다.

  • 끝나는 시간에 대해 오름차순, 이후 끝나는 시간이 같은 경우 시작 시간에 대해 오름차순 정렬을 한다면,

    tables = [[1,4], [3,5], [0,6], [5,6], [6,6], [5,7], [3,8]] 이렇게 나온다.

    • 1 : [1,4] ⇒ 통과
    • 2 : [3,5] ⇒ X (checktime : 4 > starttime : 3)
    • 3 : [0,6] ⇒ X (checktime : 4 > starttime : 0)
    • 4 : [5,6] ⇒ 통과
    • 5 : [6,6] ⇒ 통과
    • 6 : [5,7] ⇒ X (checktime : 6 > starttime : 5)
    • 7 : [3,8] ⇒ X (checktime : 6 > starttime : 3)
    • ✅ 총 3개의 회의를 진행할 수 있다.
핵심 패턴

1️⃣ 그리디 알고리즘의 핵심

// 가장 빨리 끝나는 회의부터 선택
if (checkTime <= table.start) {
    count++;
    checkTime = table.end;
}
  • 매 순간 최선의 선택(가장 빨리 끝나는 회의)을 하면 전체 최적해 달성
  • 이전 회의 종료 시간과 현재 회의 시작 시간만 비교하면 됨

2️⃣ 안전한 comparator 작성

PriorityQueue<Table> pq = new PriorityQueue<>((a, b) -> {
    if (a.end != b.end) {
        return Integer.compare(a.end, b.end);  // 끝나는 시간 오름차순
    }
    return Integer.compare(a.start, b.start);  // 시작 시간 오름차순
});
  • Integer.compare() 사용으로 오버플로우 방지
  • 이차 정렬 조건으로 최적해 보장

3️⃣ 우선순위 큐 활용

// 모든 회의를 큐에 넣고 하나씩 꺼내며 처리
for (int i = 0; i < n; i++) {
    pq.add(tables.get(i));
}

while (!pq.isEmpty()) {
    Table table = pq.poll();
    // 선택 로직
}
  • 자동 정렬로 코드 간결성 증대
  • 큐에서 꺼내는 순서가 곧 검토 순서
1차 제출 코드 ❌
import java.util.*;

public class Main {

  static class Table {
    int start;
    int end;

    public Table(int start, int end) {
      this.start = start;
      this.end = end;
    }
  }

  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    int n = sc.nextInt();

    List<Table> tables = new ArrayList<>();
    for (int i = 0; i < n; i++) {
      int start = sc.nextInt();
      int end = sc.nextInt();
      tables.add(new Table(start, end));
    }

    PriorityQueue<Table> pq = new PriorityQueue<>(
        (a, b) -> a.end - b.end
    );

    for (int i = 0; i < n; i++) {
      pq.add(tables.get(i));
    }

    int count = 0;
    int checkTime = 0;
    while (!pq.isEmpty()) {
      Table table = pq.poll();
      if (checkTime <= table.start) {
        count++;
        checkTime = table.end;
      }
    }

    System.out.println(count);
  }
}
  • 정답 코드와 다른 점은 딱 하나이다.

    PriorityQueue<Table> pq = new PriorityQueue<>(
        (a, b) -> a.end - b.end
    );
    
    • 끝나는 시간 기준 오름차순만 설정

  • 정답 코드에서는 끝나는 시간이 같은 경우, 시작 시간 오름차순으로 추가 정렬한다.

    PriorityQueue<Table> pq = new PriorityQueue<>(
        (a, b) -> {
          if (a.end != b.end) {
            return Integer.compare(a.end, b.end);
          }
          return Integer.compare(a.start, b.start);
        }
    );
    
    • a - b와 같이 간단하게 표현할 수 있지만, 혹시 모를 것을 대비하여 Integer.compare(a, b) 사용
    • 커스텀 Comparator를 사용했다.
최종 제출 코드
import java.util.*;

public class Main {

  static class Table {
    int start;
    int end;

    public Table(int start, int end) {
      this.start = start;
      this.end = end;
    }
  }

  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    int n = sc.nextInt();

    List<Table> tables = new ArrayList<>();
    for (int i = 0; i < n; i++) {
      int start = sc.nextInt();
      int end = sc.nextInt();
      tables.add(new Table(start, end));
    }

    PriorityQueue<Table> pq = new PriorityQueue<>(
        (a, b) -> {
          if (a.end != b.end) {
            return Integer.compare(a.end, b.end);
          }
          return Integer.compare(a.start, b.start);
        }
    );

    for (int i = 0; i < n; i++) {
      pq.add(tables.get(i));
    }

    int count = 0;
    int checkTime = 0;
    while (!pq.isEmpty()) {
      Table table = pq.poll();
      if (checkTime <= table.start) {
        count++;
        checkTime = table.end;
      }
    }

    System.out.println(count);
  }
}
관련 문제
그리디정렬우선순위 큐
46문제
백준
백준
Gold3
순회강연
그리디우선순위 큐+1
백준
Gold5
회의실 배정
그리디정렬우선순위 큐
백준
Silver2미해결!
소가 길을 건너간 이유 3
그리디정렬
백준
Silver2미해결!
잃어버린 괄호
그리디문자열
백준
Silver1미해결!
물병
비트마스크그리디
백준
Silver1미해결!
신입 사원
그리디정렬
백준
Silver1미해결!
카드 합체 놀이
그리디우선순위 큐
백준
Gold5미해결!
강의실 배정
그리디정렬우선순위 큐
백준
Gold5미해결!
구간 자르기
투포인터정렬
백준
Gold5미해결!
두 용액
투포인터정렬
백준
Gold5미해결!
수 고르기
투포인터정렬
백준
Gold4미해결!
수 묶기
그리디정렬
백준
Gold4미해결!
좋다
투포인터정렬
백준
Gold4미해결!
휴게소 세우기
이분탐색그리디
백준
Gold3미해결!
같이 눈사람 만들래?
투포인터정렬
백준
Gold3미해결!
공주님의 정원
그리디정렬
백준
Gold3미해결!
과제
그리디우선순위 큐
백준
Gold3미해결!
세 용액
투포인터정렬
백준
Gold2미해결!
가운데를 말해요
우선순위 큐자료구조
백준
Gold2미해결!
공항
그리디유니온파인드
백준
Gold2미해결!
보석 도둑
그리디우선순위 큐정렬
백준
Gold2미해결!
싸지방에 간 준하
그리디우선순위 큐정렬
백준
Gold2미해결!
집배원 한상덕
투포인터BFS그리디
백준
Gold2미해결!
칵테일
DFS수학그리디
백준
Gold2미해결!
컵라면
그리디우선순위 큐
백준
Gold2미해결!
합이 0인 네 정수
투포인터해시정렬
백준
Gold1미해결!
멀티탭 스케줄링
그리디정렬
백준
Gold1미해결!
팰린드롬 분할
DP그리디문자열
프로그래머스
프로그래머스
Level4
무지의 먹방 라이브
그리디우선순위 큐효율성+1
프로그래머스
Level2
더 맵게
우선순위 큐그리디
프로그래머스
Level3
야근 지수
그리디우선순위 큐이분 탐색
프로그래머스
Level2
디펜스 게임
그리디우선순위 큐
프로그래머스
Level3
이중우선순위큐
그리디트리맵
프로그래머스
Level3
디스크 컨트롤러
그리디스케줄링우선순위 큐
프로그래머스
Level2미해결!
과제 진행하기
스택정렬시뮬레이션
프로그래머스
Level2미해결!
구명보트
그리디투포인터정렬
프로그래머스
Level2미해결!
조이스틱
그리디문자열
프로그래머스
Level2미해결!
큰 수 만들기
그리디스택
프로그래머스
Level3미해결!
기지국 설치
그리디구간 커버
프로그래머스
Level3미해결!
단속카메라
그리디정렬
프로그래머스
Level3미해결!
단어 변환
BFS그리디
프로그래머스
Level3미해결!
베스트앨범
해시정렬
프로그래머스
Level3미해결!
섬 연결하기
그리디MST유니온파인드
프로그래머스
Level3미해결!
정수 삼각형
DP그리디
프로그래머스
Level3미해결!
최고의 집합
그리디수학
프로그래머스
Level4미해결!
징검다리
이분탐색그리디