- Published on
- •👁️
순회강연 ⭐
- Authors

- Name
- River
문제 설명
한 저명한 학자에게 n(0 ≤ n ≤ 10,000)개의 대학에서 강연 요청을 해 왔다. 각 대학에서는 d(1 ≤ d ≤ 10,000)일 안에 와서 강연을 해 주면 p(1 ≤ p ≤ 10,000)만큼의 강연료를 지불하겠다고 알려왔다. 각 대학에서 제시하는 d와 p값은 서로 다를 수도 있다. 이 학자는 이를 바탕으로, 가장 많은 돈을 벌 수 있도록 순회강연을 하려 한다. 강연의 특성상, 이 학자는 하루에 최대 한 곳에서만 강연을 할 수 있다.
예를 들어 네 대학에서 제시한 p값이 각각 50, 10, 20, 30이고, d값이 차례로 2, 1, 2, 1 이라고 하자. 이럴 때에는 첫째 날에 4번 대학에서 강연을 하고, 둘째 날에 1번 대학에서 강연을 하면 80만큼의 돈을 벌 수 있다.
제한사항
- n의 범위는 0 이상 10,000 이하
- p(강연료)의 범위는 1 이상 10,000 이하
- d(마감일)의 범위는 1 이상 10,000 이하
입출력 예
7 20 1 2 1 10 3 100 2 8 2 5 20 50 10
185
각 강연을 마감일 순으로 정렬하여 우선순위 큐로 최적화하면 185의 최대 강연료를 얻을 수 있습니다.
풀이 접근법
- 마감일 순으로 정렬하여 순차적으로 처리
- 우선순위 큐로 현재까지의 최적 선택 유지
- 큐 크기가 마감일을 초과하면 가장 작은 강연료 제거
- 두 가지 접근 방법: 마감일 기준 vs 강연료 기준
- 모든 강연의 마감일이 1일인 경우: 가장 비싼 강연 하나만 선택
- 마감일이 모두 다른 경우: 모든 강연을 다 할 수 있음
- n이 0인 경우: 0 출력
핵심 아이디어
방법 1
마감일이 빠른 순서로 정렬한 후, 각 강연을 순차적으로 검토하면서 현재까지 선택할 수 있는 최적의 강연 집합을 유지한다.
방법 2
강연료 기준 내림차순 정렬한 후, 날짜 자리 배치를 통해 해당 강연의 마감일에 가까운 순서부터 채워나가는 방법
내부 해부 과정
- 7개의 데이터가 들어온다.
lectures = [ [20, 1], [2, 1], [10, 3], [100, 2], [8, 2], [5, 20], [50, 10] ]로 데이터가 들어온다. 첫번째는 강연료이고, 두번째는 마감일로 된 배열이다.- 강연료를 기준으로 내림차순 정렬을 한다.
lectures = [ [100, 2], [50, 10], [20, 1], [10, 3], [8, 2], [5, 20], [2, 1] ]- 최대한 이득이 되도록 하려면, 마감일이 작은 애들 중 높은 애를 선택해야 한다.
- 첫번째 날, 마감일이 1인 데이터인
[20, 1],[2, 1]중[20, 1]을 택하고[2, 1]은 버려진다.sum = 20
- 2번째 날, 마감일이 2인 데이터인
[100, 2],[8, 2]중[100, 2]을 택하고[8, 2]은 버려진다.sum = 120
- 3번째 날, 마감일이 3인 데이터는
[10, 3]뿐이다.sum = 130
- 4번째 날, 마감일이 4인 데이터는 없다. 다음으로 다가오는 제일 작은 마감일을 가진 것은
[50, 10]이다.sum = 180
- 5번째 날, 그 다음으로 작은 마감일은
[5, 20]이다.sum = 185
⚠️ 그리디 함정
- 위의 내부 해부 과정은 함정에 걸린 상태
lectures = [[10, 1], [100, 2], [150, 2]]- 위의 로직으로는 1번째 날에 마감일이 1인 것은 1개이므로 10을 채택하고, 2번째 날은 마감일이 2인 것 중 큰 150을 채택한다.
- 그렇지만 첫번째 날 100을 하고 두번째 날 150을 하면 더욱 돈을 더 벌 수 있다는 것을 제외한 것이다.
생각 과정
❌ 잘못된 접근 (함정)
- 강연료를 기준으로 내림차순 정렬
- 마감일이 같은 것들 중 가장 비싼 것만 선택
- 문제점:
lectures = [[10, 1], [100, 2], [150, 2]]의 경우- 잘못된 로직: 1일차에 10, 2일차에 150 선택 → 160
- 올바른 답: 1일차에 100, 2일차에 150 선택 → 250
⭕ 방법 1 (boolean 배열 방식)
마감일이 겹치면 강연료가 제일 큰 것을 택해야 하지만, 이후 더 큰 마감일을 가진 것 중에 버려지는 것이 있다면 그것을 대신 택해야 한다.
마감일이 낮은 순서로 처리하면 미래에 일어나는 일을 계산하는 것이 쉽지 않다.
결국 무엇을 포기해야 하는 것이므로 비싼 것을 위주로 선택해야 한다.
강연료를 기준으로 내림차순 정렬을 한다.
boolean[]로 해당 날짜에 주인이 있는지 여부를 선택한다. 주인 있다면 정렬이 되었으므로, 무조건 주인의 강연료 이상일 수 밖에 없다. boolean[]의 크기는 최대 마감일로 설정한다. 그 이상은 될 수 없다.
즉, 해당 날짜에 주인이 있다면 그 날짜는 넘어가야 한다.
비싼 강연이라도 최대한 마감일에 가깝게 처리해야 더 돈을 많이 받을 수 있다.
즉, 마감일부터 줄여가며 반복을 돌아야 한다.
💡 추가 설명
이 방법의 핵심은 "가능한 가장 늦은 날짜에 배치"하는 그리디 전략이다. 비싼 강연을 마감일에 가깝게 배치함으로써 다른 강연들이 들어갈 수 있는 기회를 최대한 보장한다.
// 예시: [100, 3] 강연의 경우 for (int j = 3; j > 0; j--) { // 3일 → 2일 → 1일 순으로 확인 if (!isScheduled[j - 1]) { isScheduled[j - 1] = true; // 가능한 가장 늦은 날짜에 배치 break; } }
⭕ 방법 2 (우선순위 큐 방식)
내부 해부 과정에서 문제는 마감일이 낮은 순서로 정렬 후 가장 높은 강연을 택하면 이후에 버려지는 기회 중 더 좋은 기회를 놓치게 된다.
그렇다면, 마감일이 낮은 순서로 정렬 후 높은 강연을 택하는 것은 유지하고, 이것을 확정하지 않고 추후 더 좋은 것이 나오면 기존의 것을 버리면 된다.
이전의 풀었던 명예의 전당처럼 베스트 멤버를 유지하는 것처럼, 마감일이 낮은 순서로 정렬된 배열에서 하나씩 꺼내서 강연료 오름차순 우선순위 큐에 넣는다.
베스트 멤버의 길이는 해당 날짜이다. 길이가 4일이면 4일차로, 더 좋은 경우의 수를 유지할 수 있다.
💡 추가 설명
이 방법은 "동적 최적화"이다. 마감일을 하나의 체크포인트로 생각하면, 각 체크포인트에서 "지금까지 선택할 수 있는 최고의 조합"을 계속 업데이트하는 것.
// 핵심 로직 if (lectures[i][1] < pq.size()) { pq.poll(); // 현재까지 선택된 것 중 가장 작은 것을 제거 }
💡 왜 이 조건이 중요한가?
- 마감일이 3이면 최대 3개의 강연만 할 수 있음
- 큐 크기가 3을 초과하면 불가능한 상황이므로 가장 작은 것을 버림
⭕ 실제 데이터 흐름 (방법 2)
- 7개의 데이터가 들어온다.
lectures = [[20, 1], [2, 1], [10, 3], [100, 2], [8, 2], [5, 20], [50, 10]]로 데이터가 들어온다.- 마감일을 기준으로 오름차순 정렬한다.
lectures = [[20, 1], [2, 1], [100, 2], [8, 2], [10, 3], [50, 10], [5, 20]]- 각 강연을 순차적으로 처리
[20, 1]: 큐에 20 추가, 큐 크기(1) ≤ 마감일(1) → 유지[2, 1]: 큐에 2 추가, 큐 크기(2) > 마감일(1) → 최솟값 2 제거[100, 2]: 큐에 100 추가, 큐 크기(2) ≤ 마감일(2) → 유지[8, 2]: 큐에 8 추가, 큐 크기(3) > 마감일(2) → 최솟값 8 제거[10, 3]: 큐에 10 추가, 큐 크기(3) ≤ 마감일(3) → 유지[50, 10]: 큐에 50 추가, 큐 크기(4) ≤ 마감일(10) → 유지[5, 20]: 큐에 5 추가, 큐 크기(5) ≤ 마감일(20) → 유지
- 최종 큐 : [10, 20, 50, 100, 5]
- 합계 : 10 + 20 + 50 + 100 + 5 = 185
💡 추가 인사이트
- 이 과정에서 주목할 점은
[2, 1]과[8, 2]가 제거되었다. 이들은 각각의 마감일 시점에서 "최선이 아닌 선택"이었기 때문이다. - 이처럼 우선순위 큐는 **"지금까지의 최선"**을 계속 갱신하면서 최적해를 찾아간다.
💡 두 방법의 시간복잡도 비교
- 방법 1 (boolean 배열) : O(N × D) - D는 최대 마감일
- 방법 2 (우선순위 큐) : O(N log N) - 일반적으로 더 효율적
핵심 패턴
1️⃣ 우선순위 큐의 활용
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> a - b);
- 최소힙을 사용하여 가장 작은 강연료를 쉽게 제거
- 동적으로 최적 집합을 유지하는 것이 핵심
2️⃣ 마감일 순 정렬의 중요성
Arrays.sort(lectures, (a, b) -> a[1] - b[1]);
- 시간 순서대로 처리해야 올바른 그리디 선택 가능
- 마감일이 급한 것들을 먼저 처리하여 후에 더 비싼 것들로 교체 가능
- 강연료 순 정렬은 지역 최적해에 빠질 수 있음
3️⃣ 크기 조절 로직 ⭐⭐⭐
if (lectures[i][1] < pq.size()) {
pq.poll();
}
- 현재 마감일보다 선택된 강연이 많으면 최솟값 제거
- 항상 실현 가능한 최적해를 유지
- 핵심: 마감일은 일종의 체크 타임으로, 마감일이 9라면 9일째까지 총 9개의 강의를 할 수 있음
- 마감일보다 큐 크기가 크면 제일 작은 것 하나를 버려야 함
최종 제출 코드
⭕ 방법 1 : 우선순위 큐
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] lectures = new int[n][2];
for (int i = 0; i < n; i++) {
lectures[i][0] = sc.nextInt(); // 강연료
lectures[i][1] = sc.nextInt(); // 마감일
}
Arrays.sort(lectures, (a, b) -> a[1] - b[1]); // 마감일 순 정렬
PriorityQueue<Integer> pq = new PriorityQueue<>(); // Min Heap
for (int i = 0; i < n; i++) {
pq.add(lectures[i][0]);
if (lectures[i][1] < pq.size()) {
pq.poll(); // 가장 작은 강연료 제거
}
}
int sum = 0;
while (!pq.isEmpty()) {
sum += pq.poll();
}
System.out.println(sum);
sc.close();
}
}
⭕ 방법 2 : boolean 배열
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] lectures = new int[n][2];
for (int i = 0; i < n; i++) {
lectures[i][0] = sc.nextInt();
lectures[i][1] = sc.nextInt();
}
Arrays.sort(lectures, (a, b) -> b[0] - a[0]); // 강연료 순 정렬
int lastDay = 0;
for (int i = 0; i < lectures.length; i++) {
if (lastDay <= lectures[i][1]) {
lastDay = lectures[i][1];
}
}
boolean[] isScheduled = new boolean[lastDay];
int sum = 0;
for (int i = 0; i < lectures.length; i++) {
for (int j = lectures[i][1]; j > 0; j--) {
if (!isScheduled[j - 1]) {
isScheduled[j - 1] = true;
sum += lectures[i][0];
break;
}
}
}
System.out.println(sum);
sc.close();
}
}