8 분 소요

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 필요.


3. 설계 및 접근

  1. 입력: 창구 소요 시간(a_i, b_j)과 고객 도착 시간(t_k)을 저장합니다.
  2. 시간 흐름 (time = 0 ~ …): 모든 고객이 정비를 마칠 때까지 반복합니다.
  3. Step 1. 접수 대기열 합류: 현재 time에 도착한 고객을 receptionQueue에 넣습니다.
  4. Step 2. 접수 진행:
    • 비어있는 접수 창구를 찾습니다.
    • 대기열에서 고객을 꺼내 창구에 배정하고, 끝나는 시간(time + a_i)을 기록합니다.
  5. Step 3. 정비 대기열 합류: 접수가 끝난 고객들을 repairQueue로 보냅니다. (이때 접수 창구 번호 정보를 들고 가야 함)
  6. Step 4. 정비 진행:
    • 비어있는 정비 창구를 찾습니다.
    • repairQueue에서 우선순위(접수 창구 번호 등)에 따라 고객을 꺼내 배정합니다.
  7. 종료 및 합산: 고객이 이용한 창구 번호가 타겟($A, B$)과 일치하면 ID를 합산합니다.


4. 예시 시뮬레이션

  • 상황: 접수 창구 1번(소요 2), 정비 창구 1번(소요 3). 타겟 A=1, B=1. 고객 1번 도착(t=0).
    1. T=0: 고객 1이 접수 창구 1에 들어감. (종료 예정 T=2)
    2. T=1: 변화 없음.
    3. T=2:
    • 고객 1 접수 완료. 정비 대기열로 이동.
    • 정비 창구 1이 비어있으므로 고객 1 바로 입장. (종료 예정 T=5)
      1. 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().arrivalTimecurrentTime 보다 작을 때
  • 고객 접수 대기열에 해당 고객 정보를 삽입합니다.

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));
        }
    }
}

카테고리:

업데이트: