https://tech.kakao.com/posts/355

✍️ 문제 요약

  • 효율성 풀이가 까다로움. 이런 아이디어를 어떻게 떠올릴까?
  • K가 2 x 10^13 로 큼. 이런거를 계산과정에서 아예 예외시키는 방법으로 하는 풀이하는게 관건.
  • N은 200,000 이하.

💡 풀이 아이디어

어떻게 접근했는가?

먼저 음식별 필요 시간을 오름차순으로 정렬합니다. 시간의 오름차순으로 정렬해두면 음식을 먹는 데 소요되는 시간을 한꺼번에 지울 수 있습니다. 예를 들어 정렬한 시간이 T = [1, 3, 3, 4, 5]라면 처음에 T[0] * 5 = 5만큼의 시간을 한꺼번에 지울 수 있습니다. 다음으로 T[1]부터 남은 시간을 한꺼번에 제거합니다. 즉, (T[1] - T[0]) * 4 = 8 만큼의 시간을 한꺼번에 지웁니다. 마찬가지로 (T[2] - T[1]) * 3 = 0 만큼의 시간을 한꺼번에 지울 수 있습니다.

위와 같은 방법으로 시간을 지워가다가, 지운 시간의 합이 K 보다 커지게 되면 남은 시간의 개수로 나눈 나머지를 이용해 K 초 후 다시 먹기 시작해야 될 음식의 번호를 바로 구할 수 있습니다. 이때, 남은 시간을 다시 원래 음식의 번호 순서대로 재정렬해야 합니다.

핵심은 시간단위를 블록단위로 크게크게 해서 계산을 줄이는 것!

어떤 알고리즘/자료구조를 사용했는가?

  • 그리디 기법
  • List

시간복잡도는?

O(N log N)

✅ 코드

import java.util.*;
 
class Solution {
 
    // 음식 정보를 담는 클래스 (시간, 원래 인덱스)
    static class Food implements Comparable<Food> {
        int time; // 남은 먹는 시간
        int idx;  // 음식 번호 (1-based)
 
        Food(int time, int idx) {
            this.time = time;
            this.idx = idx;
        }
 
        // 시간을 기준으로 오름차순 정렬
        @Override
        public int compareTo(Food other) {
            return this.time - other.time;
        }
    }
 
    public int solution(int[] food_times, long k) {
 
        // 1. 전체 음식을 먹는 시간보다 k가 크거나 같으면 -1 반환 (모든 음식을 다 먹는 경우)
        long totalTimeSum = 0;
        for (int time : food_times) {
            totalTimeSum += time;
        }
        if (totalTimeSum <= k) {
            return -1;
        }
 
        // 2. (시간, 인덱스) 쌍으로 리스트에 저장하고 시간 기준 오름차순 정렬
        //nlogn
        List<Food> foodList = new ArrayList<>();
        for (int i = 0; i < food_times.length; i++) {
            foodList.add(new Food(food_times[i], i + 1));
        }
        Collections.sort(foodList); // 시간 기준 오름차순 정렬
 
        // 3. 시간 블록 단위로 처리
        long timePassed = 0;       // 현재까지 소요된 총 시간
        int prevTime = 0;          // 이전 블록에서 다 먹은 음식의 시간
        int numFoods = food_times.length; // 현재 남은 음식의 개수
 
        for (int i = 0; i < foodList.size(); i++) {
            Food currentFood = foodList.get(i);
 
            // 현재 음식 시간과 이전 음식 시간의 차이 (이번 블록에서 각 음식당 먹는 시간)
            // 반드시 long으로 계산해야 함
            long timeDifference = (long)currentFood.time - prevTime;
 
            // 이번 블록에서 모든 남은 음식을 timeDifference 만큼 먹는데 걸리는 총 시간
            // (timeDifference가 0이면 같은 시간의 음식이므로 계산 불필요)
            if (timeDifference > 0) {
                long timeToEatThisBlock = timeDifference * numFoods; // long * int -> long
 
                // 만약 k가 이번 블록을 완료하기 전에 도달한다면
                if (k < timePassed + timeToEatThisBlock) {
                    // 남은 시간 (k - timePassed) 동안 몇 번째 음식을 먹어야 하는지 계산
                    long remainingK = k - timePassed;
                    // 현재 단계에서 남은 음식들(i번째 이후)을 원래 인덱스 순으로 정렬
                    List<Food> remainingFoods = foodList.subList(i, foodList.size());
                    remainingFoods.sort(Comparator.comparingInt(f -> f.idx)); // 인덱스 기준 오름차순
 
                    // remainingK를 남은 음식 개수(numFoods)로 나눈 나머지가 최종 음식의 순서
                    return remainingFoods.get((int)(remainingK % numFoods)).idx;
                }
 
                // 이번 블록을 다 먹어도 k에 도달하지 못하면 시간 누적
                timePassed += timeToEatThisBlock;
            }
 
            // 현재 음식을 다 먹었으므로, 남은 음식 개수 감소 및 이전 시간 업데이트
            numFoods--;
            prevTime = currentFood.time;
        }
 
        // 위 로직상 여기까지 도달하면 안되지만, 컴파일러를 위해 추가 (혹은 로직 오류 시 확인용)
        return -1;
    }
}

❌ 실패 or 오답 이유

소팅하자는 생각도 못함. 문제에서 원판으로 순서가 정해져 있다고해서 거기에 사고가 갇힘. 문제에서 요구하는게 뭔지 파악했으면 좀 수월했을거 같음. 문제는 어떤 요소에 대한 인덱스를 원하지 어떤 요소의 남은 시간을 원하지 않음.

📘 배운 점

  • K가 2 x 10^13 로 큼. 이런거를 계산과정에서 아예 예외시키는 방법으로 하는 풀이의 존재를 파악함.
  • List의 subList를 처음 써봄. 유용함.
  • 소팅…

🪞 한 줄 회고

이번 문제를 통해 느낀 점, 나에게 하는 피드백 등

  • 어렵다이거… 그래도 꾸준히 할 수 밖에 없다. 걍 하자.