[모의 SW 역량 테스트] 차량 정비소
1. 핵심 개념 및 근거
자료구조 및 알고리즘
- 시뮬레이션 (구현): 시간(t)의 흐름에 따라 손님의 이동을 추적합니다.
- Queue (Queue / PriorityQueue):
- 접수 대기열: 도착한 순서대로 기다립니다.
- 정비 대기열: 접수가 끝난 순서대로 기다리되, “접수 창구 번호”라는 우선순위 조건이 붙으므로
PriorityQueue가 유리합니다.
- 객체 지향 (Class):
Customer,Desk등을 클래스로 정의하여 상태(남은 시간, 창구 번호 등)를 관리합니다.’
근거 및 제약사항
- 문제 조건: “접수 창구 우선순위”, “정비 창구 우선순위” 등 조건이 매우 구체적입니다. 이는 알고리즘적 기교보다 조건 분기와 상태 변화를 얼마나 꼼꼼하게 코드로 옮기느냐를 묻는 것입니다.
- 시간 복잡도: 고객 수 ≤ 1,000, 창구 수 N, M ≤ 10, 최대 시간은 넉넉하게 잡아도 시뮬레이션으로 충분히 해결 가능한 범위입니다. O(T x (N+M)) 정도로 수렴합니다.
2. 추론 과정
- 키워드1: 도착하는 순서대로
Queue또는 도착 시간 기준 정렬.
- 키워드2: 비어있는 창구 중 번호가 작은 곳
- 창구를 순회하며 빈 곳을 찾는 반복문.
- 키워드3: 접수를 마치고 정비소로 이동
- 접수 종료 리스트 후 정비 대기열로 이동하는 파이프라인 형성
- 키워드4: 접수 창구 번호가 작은 고객
- 단순 Queue가 아닌, 정렬 기준이 포함된
PriorityQueue필요.
- 단순 Queue가 아닌, 정렬 기준이 포함된
3. 설계 및 접근
- 입력: 창구 소요 시간(a_i, b_j)과 고객 도착 시간(t_k)을 저장합니다.
- 시간 흐름 (time = 0 ~ …): 모든 고객이 정비를 마칠 때까지 반복합니다.
- Step 1. 접수 대기열 합류: 현재
time에 도착한 고객을receptionQueue에 넣습니다. - Step 2. 접수 진행:
- 비어있는 접수 창구를 찾습니다.
- 대기열에서 고객을 꺼내 창구에 배정하고, 끝나는 시간(
time + a_i)을 기록합니다.
- Step 3. 정비 대기열 합류: 접수가 끝난 고객들을
repairQueue로 보냅니다. (이때 접수 창구 번호 정보를 들고 가야 함) - Step 4. 정비 진행:
- 비어있는 정비 창구를 찾습니다.
repairQueue에서 우선순위(접수 창구 번호 등)에 따라 고객을 꺼내 배정합니다.
- 종료 및 합산: 고객이 이용한 창구 번호가 타겟($A, B$)과 일치하면 ID를 합산합니다.
4. 예시 시뮬레이션
- 상황: 접수 창구 1번(소요 2), 정비 창구 1번(소요 3). 타겟 A=1, B=1.
고객 1번 도착(t=0).
- T=0: 고객 1이 접수 창구 1에 들어감. (종료 예정 T=2)
- T=1: 변화 없음.
- T=2:
- 고객 1 접수 완료. 정비 대기열로 이동.
- 정비 창구 1이 비어있으므로 고객 1 바로 입장. (종료 예정 T=5)
- T=5: 고객 1 정비 완료. 이용 기록(접수1, 정비1) 확인 → 결과값에 ID 1 더함.
5. 주의 사항
- 동시성 처리: 같은 시간에 여러 명이 접수/정비를 마칠 때 누가 먼저 대기열에 들어가는지 주의해야 합니다.
- 우선순위 큐 조건: 정비 대기열의 우선순위
- 접수 완료 시간 순
- 이용했던 접수 창구 번호 순
- 이 조건을 놓치면 오답이 나옵니다.
- 종료 조건: 단순히
receptionQueue가 비었다고 끝내면 안 됩니다. 모든 창구와 대기열이 비어야 합니다.
6. 코드 풀이
6.1 클래스 설계
6.1.1 Customer 클래스
static class Customer implements Comparable<Customer> {
int id, arrivalTime;
int usedReceptionId, usedRepairId;
int receptionEndTime; // 정비소 우선순위 판단 기준
public Customer(int id, int arrivalTime) {
this.id = id;
this.arrivalTime = arrivalTime;
}
@Override
public int compareTo(Customer o) {
if (this.receptionEndTime != o.receptionEndTime) {
return Integer.compare(this.receptionEndTime, o.receptionEndTime);
}
return Integer.compare(this.usedReceptionId, o.usedReceptionId);
}
}
- 고객의 번호, 도착 시간 선언
- 이용한 접수, 정비 창구 번호 선언
- 접수 완료한 시간 선언
- 우선순위 큐에서 정렬할 수 있게
Comparable인터페이스 사용
6.1.2 Desk 클래스
static class Desk {
int idx, processingTime;
Customer currentCustomer;
int finishTime;
public Desk(int idx, int processingTime) {
this.idx = idx;
this.processingTime = processingTime;
}
public boolean isFree(int currentTime) {
return currentCustomer == null || finishTime <= currentTime;
}
public void assign(Customer c, int currentTime) {
this.currentCustomer = c;
this.finishTime = currentTime + this.processingTime;
}
public Customer release() {
Customer c = currentCustomer;
currentCustomer = null;
return c;
}
}
- 창구의 번호, 처리 시간, 이용 중인 고객, 종료 시간 선언
- 빈 자리 확인하는 함수
isFree선언 - 고객 등록하는 함수
assign선언 - 고객 퇴실하는 함수
release선언
6.2 입력
// 접수 창구
Desk[] receptionDesks = new Desk[N + 1];
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= N; i++) receptionDesks[i] = new Desk(i, Integer.parseInt(st.nextToken()));
// 정비 창구
Desk[] repairDesks = new Desk[M + 1];
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= M; i++) repairDesks[i] = new Desk(i, Integer.parseInt(st.nextToken()));
// 고객 정보
Queue<Customer> arrivalQueue = new LinkedList<>();
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= K; i++) arrivalQueue.add(new Customer(i, Integer.parseInt(st.nextToken())));
// 접수 대기 큐
Queue<Customer> waitReception = new LinkedList<>();
// 정비 대기 큐
PriorityQueue<Customer> waitRepair = new PriorityQueue<>();
int finishedCount = 0; // 모든 고객이 끝나는
int totalIdSum = 0; // 결과
int currentTime = 0; // 진행 시간
repairDesks,repairDesks를 Desk 클래스 배열로 선언하고 처리 시간을 입력합니다.- 고객 도착 시간을 데이터의 삽입과 삭제를 간단하게 하는 큐로 선언하고, 도착 시간을 입력합니다.
6.3 시뮬레이션
6.3.1 도착 고객 접수 대기열 합류
// Step 1: 도착 고객 접수 대기열 합류
while (!arrivalQueue.isEmpty() && arrivalQueue.peek().arrivalTime <= currentTime) {
waitReception.add(arrivalQueue.poll());
}
arrivalQueue.isEmpty()가false일 때arrivalQueue.peek().arrivalTime이currentTime보다 작을 때- 고객 접수 대기열에 해당 고객 정보를 삽입합니다.
6.3.2 접수 완료 처리 -> 정비 대기열 이동
// Step 2: 접수 완료 처리 -> 정비 대기열 이동
for (int i = 1; i <= N; i++) {
if (receptionDesks[i].currentCustomer != null && receptionDesks[i].finishTime == currentTime) {
Customer c = receptionDesks[i].release();
waitRepair.add(c);
}
}
- 해당 접수 창구에 고객이 있고, 접수 마무리 시간이
currentTime과 같을 때 - 해당 고객을 정비 대기열에 삽입합니다.
6.3.3 정비 완료 처리 -> 결과 합산
// Step 3: 정비 완료 처리 -> 결과 합산
for (int i = 1; i <= M; i++) {
if (repairDesks[i].currentCustomer != null && repairDesks[i].finishTime == currentTime) {
Customer c = repairDesks[i].release();
if (c.usedReceptionId == targetA && i == targetB) totalIdSum += c.id;
finishedCount++;
}
}
- 해당 정비 창구에 고객이 있고, 정비 마무리 시간이
currentTime과 같을 때 - 해당 고객이 방문한 접수, 정비 창구를 목표 A, B와 같은지 비교해서 같으면 결과를 합산합니다.
- 고객이 처리했음을 표시합니다.
6.3.4 접수 창구 배정
// Step 4: 접수 창구 배정
for (int i = 1; i <= N; i++) {
if (receptionDesks[i].isFree(currentTime) && !waitReception.isEmpty()) {
// 대기열의 가장 앞에 있는 고객 추출
Customer c = waitReception.poll();
// 고객 객체에 방문한 창구 기록
c.usedReceptionId = i;
// 창구 객체에 고객을 넣고 업무 시작
receptionDesks[i].assign(c, currentTime);
// 정비 대기열에서 정렬 기준이 될 접수 종료 시간을 고려해서 기록
c.receptionEndTime = receptionDesks[i].finishTime;
}
}
- 대기열에 머물고 있는 고객을 비어있는 접수 창구에 않히는 과정입니다.
6.3.5 정비 창구 배정
// Step 5: 정비 창구 배정
for (int i = 1; i <= M; i++) {
// i번 정비 창구가 비어있고(&&), 정비 대기열에 고객이 있다면
if (repairDesks[i].isFree(currentTime) && !waitRepair.isEmpty()) {
// PriorityQueue에서 가장 우선순위가 높은 고객 추출
// 접수 종료 시간 순 -> 접수 창구 번호 순
Customer c = waitRepair.poll();
// 고객 객체에 "내가 몇 번 정비 창구를 썼는지" 저장
c.usedRepairId = i;
// 정비 창구에 고객을 넣고 정비 시작 (종료 시간 계산)
repairDesks[i].assign(c, currentTime);
}
}
- 정비 대기열의 독특한 우선순위와 정비 창구의 상태를 결합하는 것이 핵심입니다.
6.3.6 시뮬레이션 전체 코드
while (finishedCount < K) {
// Step 1: 도착 고객 접수 대기열 합류
while (!arrivalQueue.isEmpty() && arrivalQueue.peek().arrivalTime <= currentTime) {
waitReception.add(arrivalQueue.poll());
}
// Step 2: 접수 완료 처리 -> 정비 대기열 이동
for (int i = 1; i <= N; i++) {
if (receptionDesks[i].currentCustomer != null && receptionDesks[i].finishTime == currentTime) {
Customer c = receptionDesks[i].release();
waitRepair.add(c);
}
}
// Step 3: 정비 완료 처리 -> 결과 합산
for (int i = 1; i <= M; i++) {
if (repairDesks[i].currentCustomer != null && repairDesks[i].finishTime == currentTime) {
Customer c = repairDesks[i].release();
if (c.usedReceptionId == targetA && i == targetB) totalIdSum += c.id;
finishedCount++;
}
}
// Step 4: 접수 창구 배정
for (int i = 1; i <= N; i++) {
if (receptionDesks[i].isFree(currentTime) && !waitReception.isEmpty()) {
Customer c = waitReception.poll();
c.usedReceptionId = i;
receptionDesks[i].assign(c, currentTime);
c.receptionEndTime = receptionDesks[i].finishTime;
}
}
// Step 5: 정비 창구 배정
for (int i = 1; i <= M; i++) {
if (repairDesks[i].isFree(currentTime) && !waitRepair.isEmpty()) {
Customer c = waitRepair.poll();
c.usedRepairId = i;
repairDesks[i].assign(c, currentTime);
}
}
currentTime++;
}
6. 전체 코드
import java.util.*;
import java.io.*;
public class Solution {
// 1. 고객 클래스: 자신의 정보와 이용 기록을 저장
static class Customer implements Comparable<Customer> {
int id, arrivalTime;
int usedReceptionId, usedRepairId;
int receptionEndTime; // 정비소 우선순위 판단 기준
public Customer(int id, int arrivalTime) {
this.id = id;
this.arrivalTime = arrivalTime;
}
@Override
public int compareTo(Customer o) {
if (this.receptionEndTime != o.receptionEndTime) {
return Integer.compare(this.receptionEndTime, o.receptionEndTime);
}
return Integer.compare(this.usedReceptionId, o.usedReceptionId);
}
}
// 2. 창구 클래스: 창구의 상태와 작업 로직 관리
static class Desk {
int idx, processingTime;
Customer currentCustomer;
int finishTime;
public Desk(int idx, int processingTime) {
this.idx = idx;
this.processingTime = processingTime;
}
public boolean isFree(int currentTime) {
return currentCustomer == null || finishTime <= currentTime;
}
public void assign(Customer c, int currentTime) {
this.currentCustomer = c;
this.finishTime = currentTime + this.processingTime;
}
public Customer release() {
Customer c = currentCustomer;
currentCustomer = null;
return c;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int T = Integer.parseInt(br.readLine());
for (int tc = 1; tc <= T; tc++) {
StringTokenizer st = new StringTokenizer(br.readLine());
int N = Integer.parseInt(st.nextToken());
int M = Integer.parseInt(st.nextToken());
int K = Integer.parseInt(st.nextToken());
int targetA = Integer.parseInt(st.nextToken());
int targetB = Integer.parseInt(st.nextToken());
Desk[] receptionDesks = new Desk[N + 1];
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= N; i++) receptionDesks[i] = new Desk(i, Integer.parseInt(st.nextToken()));
Desk[] repairDesks = new Desk[M + 1];
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= M; i++) repairDesks[i] = new Desk(i, Integer.parseInt(st.nextToken()));
Queue<Customer> arrivalQueue = new LinkedList<>();
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= K; i++) arrivalQueue.add(new Customer(i, Integer.parseInt(st.nextToken())));
Queue<Customer> waitReception = new LinkedList<>();
PriorityQueue<Customer> waitRepair = new PriorityQueue<>();
int finishedCount = 0, totalIdSum = 0, currentTime = 0;
// 시뮬레이션 시작
while (finishedCount < K) {
// Step 1: 도착 고객 접수 대기열 합류
while (!arrivalQueue.isEmpty() && arrivalQueue.peek().arrivalTime <= currentTime) {
waitReception.add(arrivalQueue.poll());
}
// Step 2: 접수 완료 처리 -> 정비 대기열 이동
for (int i = 1; i <= N; i++) {
if (receptionDesks[i].currentCustomer != null && receptionDesks[i].finishTime == currentTime) {
Customer c = receptionDesks[i].release();
waitRepair.add(c);
}
}
// Step 3: 정비 완료 처리 -> 결과 합산
for (int i = 1; i <= M; i++) {
if (repairDesks[i].currentCustomer != null && repairDesks[i].finishTime == currentTime) {
Customer c = repairDesks[i].release();
if (c.usedReceptionId == targetA && i == targetB) totalIdSum += c.id;
finishedCount++;
}
}
// Step 4: 접수 창구 배정
for (int i = 1; i <= N; i++) {
if (receptionDesks[i].isFree(currentTime) && !waitReception.isEmpty()) {
Customer c = waitReception.poll();
c.usedReceptionId = i;
receptionDesks[i].assign(c, currentTime);
c.receptionEndTime = receptionDesks[i].finishTime;
}
}
// Step 5: 정비 창구 배정
for (int i = 1; i <= M; i++) {
if (repairDesks[i].isFree(currentTime) && !waitRepair.isEmpty()) {
Customer c = waitRepair.poll();
c.usedRepairId = i;
repairDesks[i].assign(c, currentTime);
}
}
currentTime++;
}
System.out.println("#" + tc + " " + (totalIdSum == 0 ? -1 : totalIdSum));
}
}
}