INFO
1. 문제 이해
핵심 요구사항
-
목표: 손님이 계산을 완료하고 쇼핑몰을 나오는 순서 구하기 → ()
-
입력
- N: 사람 수
- : 사람 별 id
- : 사람 별 걸리는 시간(산 물건 수)
- K: 계산대 수
- N: 사람 수
-
계산 규칙
- 계산에 걸리는 시간 = 물건 수(w)
- 계산대 입장과 퇴장에 걸리는 시간은 없음
- 순서 기준
- 입장 시 → 1. 빨리 끝남 / 2. 계산대 번호가 작음
- 퇴장 시 → 1. 빨리 끝남 / 2. 계산대 번호가 큼
제한 조건
- 변수 범위 → long
- 입력
- 1 ≤ N ≤ 100,000
- 1 ≤ id ≤ 1,000,000
- 1 ≤ w ≤ 20
- 1 ≤ k ≤ 100,000
- 출력: long 사용해야함
- 입력
- 시간: 1초 →
- 최대 안으로.
- 메모리: 512MB → 넉넉
2. 사고의 흐름 (Thinking)
-
**✅ 1차 시도: 우선순위 큐 → 가용 계산대 큐(계산대 기준), 계산 중 큐(customer 기준) && 바로바로 결과값 계산
- 들어갔다가 시간 순서대로 나오는 걸 구하는 문제 → 우선순위큐가 적합
- heap을 사용한다면 으로 해결 가능 ( = heap 삽입/삭제 → n번 함)
- ⇒ 시뮬레이션 관점 / 동시 처리 어려움
-
✅✏️ 2차 시도: 우선순위 큐 → 계산대 큐 하나만 사용 && 사용자별 계산 종료시간을 기록해뒀다가 한번에 정렬
- 기준이 ‘끝나는 시간’임을 염두에 둔다면 훨씬 간단하게 생각할 수 있음. → like 16235. 나무 재테크
- 정렬 기준이 여럿인 경우.실시간으로 처리하기 보다는 값을 기록해두고 전체를 확인하여 한번에 처리하는게 안전할 수도 있음
- ⇒ 스케줄링 관점 / 실시간성이 필요 없어서 미리 계산 후 정렬
3. 풀이 및 코드 (Solution)
방법 1: 우선순위 큐 2개 / 실시간 결과 처리
import java.io.*;
import java.util.*;
// Customer 클래스 선언
class Customer implements Comparable<Customer>{
int id, w;
Counter c;
public Customer(int id, int w) {
this.id = id;
this.w = w;
}
@Override
public int compareTo(Customer o) {
if (this.c.end_time == o.c.end_time) {
return o.c.id - this.c.id; // 퇴장시에는 큰 수부터
}
return this.c.end_time - o.c.end_time;
}
}
class Counter implements Comparable<Counter> {
int id, end_time = 0;
public Counter(int id) {
this.id = id;
}
@Override
public int compareTo(Counter o) {
if (this.c.end_time == o.c.end_time) {
return this.c.id - o.c.id; // 입장시에는 큰 수부터
}
return this.c.end_time - o.c.end_time;
}
}
public class Main {
public static void main(String[] args) throws IOException {
// 기본 입력 받기 / 기본 선언(cnt/result)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N, k, cnt=1;
long result=0;
Customer[] customers;
String[] temp = br.readLine().split(" ");
N = Integer.parseInt(temp[0]);
k = Integer.parseInt(temp[1]);
// N번 돌면서 customer 생성 / customers 배열에 넣어두기
customers = new Customer[N+1];
for (int n=0; n<N; n++) {
temp = br.readLine().split(" ");
int id = Integer.parseInt(temp[0]);
int w = Integer.parseInt(temp[1]);
customers[n] = new Customer(id, w);
}
// 우선순위큐 선언
// 계산대 (1-k)
PriorityQueue<Counter> counters = new PriorityQueue<>();
for (int id=1; id<=k; id++) {
counters.offer(new Counter(id));
}
// 계산중 (빈 배열) -> 1번 손님 삽입
PriorityQueue<Customer> calculating = new PriorityQueue<>();
// customer 배열 돌면서
for (int n=0; n<N; n++) {
Customer now = customers[n];
int prev_time = -1;
while (counters.isEmpty()|| (!calculating.isEmpty() && prev_time == calculating.peek().c.end_time)){
Customer customer = calculating.poll();
result += (long) customer.id * cnt++;
counters.offer(customer.c);
prev_time = customer.c.end_time;
}
Counter counter = counters.poll();
now.c = counter;
counter.end_time += now.w;
calculating.offer(now);
}
while (!calculating.isEmpty()) {
Customer out = calculating.poll();
result += (long) out.id * cnt;
cnt++;
}
System.out.println(result);
}
}-
단순하게
- 입장시에 손님에게 할당할 가용계산대를 고르는 큐 하나
- 계산이 빨리 끝나는(+계산대 번호가 큰) 순서대로 추출할 큐 하나
- 이렇게 2개의 우선순위 큐 생성
-
사용자 배열을 0~N-1까지 돌면서,
- 가용 계산대 큐에 계산대가 있으면
- id 작은 순서로 계산대 할당 후
- 해당 사용자를 계산 중 큐에 넣음
- 없으면
- 가장 빨리 계산이 끝나는 모든 계산대를(여러개일 수 있음) 빼서 가용 계산대 큐에 넣음
- 이때 계산을 마친 사용자에 대해서 결과값 계산
- 이후 위의 연산 진행
- 가용 계산대 큐에 계산대가 있으면
-
⇒ 시간 기반 연산
-
시간복잡도:
- 요소 수가 k인 힙 시간 복잡도: log k
- 힙에 N번 삽입/삭제함
방법2: 우선순위 큐 1개/모아서 결과값 처리
import java.io.*;
import java.util.*;
class Customer implements Comparable<Customer>{
int id, end_time, counter;
public Customer(int id, int end_time, int counter) {
this.id = id;
this.end_time = end_time;
this.counter = counter;
}
@Override
public int compareTo(Customer o) {
if (this.end_time == o.end_time) {
return o.counter - this.counter; // 퇴장시에는 큰 수부터
}
return this.end_time - o.end_time;
}
}
class Counter implements Comparable<Counter> {
int id, end_time = 0, user;
// Customer user;
public Counter(int id) {
this.id = id;
}
@Override
public int compareTo(Counter o) {
if (this.end_time == o.end_time) {
return this.id - o.id; // 입장시에는 작은 수부터
}
return this.end_time - o.end_time;
}
}
public class BOJ17612_쇼핑몰 {
public static void main(String[] args) throws IOException {
// 기본 입력 받기 / 기본 선언(cnt/result)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N, k, cnt=1;
long result=0;
PriorityQueue<Counter> calculating =new PriorityQueue<>();
ArrayList<Customer> customers = new ArrayList<>();
String[] temp = br.readLine().split(" ");
N = Integer.parseInt(temp[0]);
k = Integer.parseInt(temp[1]);
// 계산대 세팅
for (int id=1; id<=k; id++) {
calculating.offer(new Counter(id));
}
// N번 돌면서 계산 돌리기
for (int n=0; n<N; n++) {
temp = br.readLine().split(" ");
int id = Integer.parseInt(temp[0]);
int w = Integer.parseInt(temp[1]);
Counter counter = calculating.poll();
counter.user = id;
counter.end_time += w;
calculating.offer(counter);
customers.add(new Customer(counter.user, counter.end_time, counter.id));
}
Collections.sort(customers);
for (Customer c : customers) {
result += (long) c.id*cnt;
cnt++;
}
System.out.println(result);
}
}-
현재 계산이 끝나는 시간 정보를 가진 계산대 우선순위 큐를 생성
- 가장 빨리 끝나면서, 가장 번호가 작은 계산대를 기준으로 정렬
-
차례대로 사용자가 쓸 계산대를 추출
-
사용자의 계산시간을 계산대의 끝나는 시간에 더해서 다시 우선순위 큐에 넣음
-
동시에 사용자에게 해당 사용자의 계산이 끝나는 시간과 사용 계산대 정보를 담아 사용자 배열에 넣음
-
이 과정을 반복
-
반복 후, 사용자 배열을 시간/큰 계산대id에 따라 정렬하여 결과값 출력
-
시간 복잡도:
- O(N log N): N개를 정렬
-
- O(N log k)
- 요소 수가 k인 힙 시간 복잡도: log k
- 힙에 N번 삽입/삭제함
4. 회고 (Retrospective)
- 순서가 중요한 경우 실시간으로 결과값을 처리하려면 좀더 엄밀하게 코드를 짜야하는 어려움이 있다는 것을 깨달음. → like 동시성 처리
- 저번의 16235. 나무 재테크처럼 기준을 ‘필수적’인 것으로 한정하고, 그것에만 집중해서 자료구조를 만드는게 중요한데 어렵다. DP 기준잡는게 처음에 어려웠던 것과 비슷한 느낌임.
- 처음에 방법1로 문제를 풀고, 오답이 떠서 포기하고 AI의 도움을 받아 방법2로 출었는데 문제 정리하면서 방법1도 잘하면 가능할 것같아서 되더라. 내 풀이를 내가 스스로 객관적으로 따져볼 수 있는 힘을 길러야할 것같다.