프로그래머스 42587 - 프로세스 (Lv.4)
핵심 접근 — FIFO 큐로 순서를 지키고 우선순위 큐로 남은 최댓값만 조회
프로그래머스 42587 - 프로세스 (Lv.4)
출처: https://school.programmers.co.kr/learn/courses/30/lessons/42587
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
75
76
77
78
79
80
81
82
83
// 프로그래머스 42587 - 프로세스 (Lv.4)
// https://school.programmers.co.kr/learn/courses/30/lessons/42587
// (같은 FIFO 골격: 42586 기능개발 — 다만 이쪽은 조건 미달이면 큐 뒤로 되돌린다)
// 문제 설명
// 운영체제는 다음 규칙으로 프로세스를 관리한다.
// 1) 실행 대기 큐에서 프로세스 하나를 꺼낸다.
// 2) 큐에 더 높은 우선순위의 프로세스가 남아 있으면 꺼낸 프로세스를 큐 뒤로 되돌린다.
// 3) 그렇지 않으면 실행한다.
// priorities 와 대상 프로세스의 인덱스 location 이 주어질 때, 그 프로세스가 몇 번째로
// 실행되는지 반환하라.
// 제약 조건
// 1 <= priorities 길이 <= 100
// 우선순위는 1 ~ 9 (숫자가 클수록 우선순위가 높다)
// 0 <= location < priorities 길이
// Example
// Input : priorities = [2, 1, 3, 2], location = 2
// Output: 1 (우선순위 3이 가장 높아 첫 번째로 실행)
//
// Input : priorities = [1, 1, 9, 1, 1, 1], location = 0
// Output: 5 (9가 먼저 나가고, 남은 1들은 큐 순서대로 → 대상은 5번째)
// 접근 — FIFO 큐 + 남은 최대 우선순위 조회
//
// [1단계 — 정렬하면 답이 사라진다]
// 우선순위만 보면 내림차순 정렬로 끝날 것 같지만, 동점 프로세스끼리의 순서가
// "큐에 남은 순서"로 결정된다. 예제 2에서 우선순위 1이 다섯 개인데 답이 5인 이유가 그것이다.
// 정렬은 이 순서 정보를 지워버리므로 대기열은 큐로 그대로 굴려야 한다.
//
// [2단계 — 인덱스를 함께 들고 다닌다 (자료구조 선택 근거)]
// 답으로 물어보는 것은 "값"이 아니라 "location 번째 원소가 몇 번째로 나가는가"다.
// 큐를 돌리면 원소의 현재 위치는 계속 바뀌므로 원래 인덱스를 잃으면 대상을 식별할 수 없다.
// → queue<pair<int, int>> 로 {우선순위, 원래 인덱스}를 묶어 넣는다.
//
// [3단계 — "더 급한 게 남아 있나"를 O(log n)으로 (자료구조 선택 근거)]
// 규칙 2)를 판정하려면 큐에 남은 최대 우선순위가 필요한데, 큐는 내부를 훑을 수 없다.
// 남은 프로세스의 우선순위만 따로 priority_queue<int> 에 담아 최댓값을 조회한다.
// 실행할 때만 pq.pop() 을 하므로 pq 의 내용은 항상 "아직 실행되지 않은 프로세스"와 일치한다.
// 되돌릴 때는 pq 를 건드리지 않는다 — 되돌린 프로세스도 여전히 남아 있는 프로세스다.
//
// [4단계 — 복잡도]
// 큐를 한 바퀴 돌면 최소 한 개는 실행되어 사라지므로 큐 연산은 최악 O(n^2),
// 우선순위 큐 연산은 O(n log n). n <= 100 이라 최악 1만 회 수준으로 여유가 크다.
#include <vector>
#include <queue>
using namespace std;
int solution(vector<int> priorities, int location)
{
queue<pair<int, int>> q; // {우선순위, 원래 인덱스} — 돌려도 대상을 식별할 수 있게
priority_queue<int> pq; // 아직 실행되지 않은 프로세스의 우선순위 (top = 남은 최댓값)
for (int i = 0; i < priorities.size(); i++)
{
q.push({ priorities[i], i });
pq.push(priorities[i]);
}
int order = 0;
while (!q.empty())
{
auto [pri, idx] = q.front(); // 구조적 바인딩(C++17)
q.pop();
if (pri < pq.top()) // 더 급한 게 남아 있으면 실행하지 않고
{
q.push({ pri, idx }); // 큐 뒤로 되돌린다. pq 는 그대로 (여전히 남아 있으므로)
continue;
}
pq.pop(); // 실행 확정 — pri == pq.top() 이라 짝이 맞는다
order++;
if (idx == location) return order; // 대상이 실행된 순간이 곧 답
}
return order;
}
정리
- 정렬로 끝날 것 같지만 동점 처리가 정렬을 막는다. 우선순위 내림차순으로 세우면 되는 문제처럼 보이는데, 같은 우선순위끼리의 순서는 값이 아니라 큐에 남아 있는 순서로 결정된다. 예제 2가 정확히 그 지점을 찌른다 — 우선순위 1이 다섯 개인데 답이 5인 것은 대상이 되돌려진 뒤 맨 뒤로 갔기 때문이다. 정렬은 이 정보를 지운다.
- 묻는 것이 “값”이 아니라 “위치”라서 인덱스를 값과 함께 들고 다녀야 한다. 큐를 돌리면 원소의 현재 위치가 계속 바뀌므로
location을 나중에 되찾을 방법이 없다.queue<pair<int, int>>로{우선순위, 원래 인덱스}를 묶는 것이 이 문제의 실질적인 자료구조 결정이고, 위장(42578)의 해시맵처럼 자료구조를 탐색이 아니라 식별자 보존 용도로 쓰는 경우다. - 큐는 내부를 볼 수 없으니 “남은 최댓값”은 별도 인덱스로 유지한다.
priority_queue<int>에 남은 우선순위만 따로 담아top()으로 조회한다. 여기서 실수하기 쉬운 지점은 되돌릴 때pq를 건드리는 것 — 되돌린 프로세스도 여전히 남아 있는 프로세스이므로pq는 그대로 둬야 하고,pop()은 실행이 확정되는 자리에서만 한다. 그 자리에서는pri == pq.top()이 성립하므로 어떤 원소가 빠지는지 고민할 필요가 없다. - 우선순위가 1~9로 좁다는 제약을 쓰면
int cnt[10]카운팅으로 최댓값을 O(1)에 찾을 수도 있다. 값 범위에 기대는 만큼 빠르지만 범위가 바뀌면 무너지고,priority_queue는 범위와 무관하게 성립한다. 다이얼(5622)의 룩업 테이블처럼 값 범위가 좁을 때만 서는 최적화라는 점을 알고 고르는 것이 중요하다. - 복잡도: 큐를 한 바퀴 돌 때마다 최소 하나는 실행되어 사라지므로 큐 연산은 최악 O(n²), 우선순위 큐 연산은 O(n log n).
n <= 100이라 최악도 1만 회 수준. 커리큘럼 92번. - 검증: 예제 2개(1, 5)와 경계 케이스 — 프로세스 1개(1), 동점 중 뒤쪽이 대상(2), 오름차순 입력에서 맨 앞이 대상(5) 통과 (MSVC
/std:c++17컴파일·실행).
핵심 요약 — 우선순위 내림차순 정렬로는 풀 수 없다. 동점끼리의 순서가 “큐에 남은 순서”로 결정되고 정렬이 그 정보를 지우기 때문이다(예제 2의 답 5가 그 증거). 순서는 FIFO 큐로 지키고, 큐가 내부를 볼 수 없는 문제는
priority_queue에 남은 우선순위만 따로 담아top()으로 조회해 해결한다 — 되돌릴 때는pq를 건드리지 않고 실행이 확정될 때만pop()해야 “남은 집합”과 일치가 유지된다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.