[카카오코테] 무지의 먹방 라이브
Algorithm/greedy

[카카오코테] 무지의 먹방 라이브

문제 설명


무지의 먹방 라이브

* 효율성 테스트에 부분 점수가 있는 문제입니다.

평소 식욕이 왕성한 무지는 자신의 재능을 뽐내고 싶어 졌고 고민 끝에 카카오 TV 라이브로 방송을 하기로 마음먹었다.

그냥 먹방을 하면 다른 방송과 차별성이 없기 때문에 무지는 아래와 같이 독특한 방식을 생각해냈다.

회전판에 먹어야 할 N 개의 음식이 있다.
각 음식에는 1부터 N 까지 번호가 붙어있으며, 각 음식을 섭취하는데 일정 시간이 소요된다.
무지는 다음과 같은 방법으로 음식을 섭취한다.

  • 무지는 1번 음식부터 먹기 시작하며, 회전판은 번호가 증가하는 순서대로 음식을 무지 앞으로 가져다 놓는다.
  • 마지막 번호의 음식을 섭취한 후에는 회전판에 의해 다시 1번 음식이 무지 앞으로 온다.
  • 무지는 음식 하나를 1초 동안 섭취한 후 남은 음식은 그대로 두고, 다음 음식을 섭취한다.
    • 다음 음식이란, 아직 남은 음식 중 다음으로 섭취해야 할 가장 가까운 번호의 음식을 말한다.
  • 회전판이 다음 음식을 무지 앞으로 가져오는데 걸리는 시간은 없다고 가정한다.

무지가 먹방을 시작한 지 K 초 후에 네트워크 장애로 인해 방송이 잠시 중단되었다.
무지는 네트워크 정상화 후 다시 방송을 이어갈 때, 몇 번 음식부터 섭취해야 하는지를 알고자 한다.
각 음식을 모두 먹는데 필요한 시간이 담겨있는 배열 food_times, 네트워크 장애가 발생한 시간 K 초가 매개변수로 주어질 때 몇 번 음식부터 다시 섭취하면 되는지 return 하도록 solution 함수를 완성하라.

 

제한사항

  • food_times 는 각 음식을 모두 먹는데 필요한 시간이 음식의 번호 순서대로 들어있는 배열이다.
  • k 는 방송이 중단된 시간을 나타낸다.
  • 만약 더 섭취해야 할 음식이 없다면 -1을 반환하면 된다.

정확성 테스트 제한 사항

  • food_times 의 길이는 1 이상 2,000 이하이다.
  • food_times 의 원소는 1 이상 1,000 이하의 자연수이다.
  • k는 1 이상 2,000,000 이하의 자연수이다.

효율성 테스트 제한 사항

  • food_times 의 길이는 1 이상 200,000 이하이다.
  • food_times 의 원소는 1 이상 100,000,000 이하의 자연수이다.
  • k는 1 이상 2 x 10^13 이하의 자연수이다.

입출력 예

food_timeskresult

[3, 1, 2] 5 1

입출력 예 설명

입출력 예 #1

  • 0~1초 동안에 1번 음식을 섭취한다. 남은 시간은 [2,1,2] 이다.
  • 1~2초 동안 2번 음식을 섭취한다. 남은 시간은 [2,0,2] 이다.
  • 2~3초 동안 3번 음식을 섭취한다. 남은 시간은 [2,0,1] 이다.
  • 3~4초 동안 1번 음식을 섭취한다. 남은 시간은 [1,0,1] 이다.
  • 4~5초 동안 (2번 음식은 다 먹었으므로) 3번 음식을 섭취한다. 남은 시간은 [1,0,0] 이다.
  • 5초에서 네트워크 장애가 발생했다. 1번 음식을 섭취해야 할 때 중단되었으므로, 장애 복구 후에 1번 음식부터 다시 먹기 시작하면 된다.

 

 

첫 접근


문제에서 나온 프로세스 그대로 따라가는 코드로 짰었다.

def solution(food_times, k):
    answer = 0
    time = 0

    while True:
        checkfinish = 0
        while True:
            if checkfinish >= len(food_times):
                answer = -1
                break
            checkfinish += 1
            if answer >= len(food_times):
                answer = 0
            if food_times[answer] != 0:
                break
            else:
                answer += 1
        if time == k or answer == -1:
            break

        food_times[answer] -= 1
        answer += 1
        time += 1

    if answer != -1:
        answer += 1
    return answer

print(solution([3, 1, 2], 5))

 

 

무지에게 음식의 순서대로 음식을 주고 음식의 food_time을 하나씩 줄여주는 코드로 짰었다.

 

프로세스 그대로 짰을때 정확성테스트는 전부 통과했지만 효율성 테스트는 전부 실패했다.

 

이 코드의 시간복잡도는 food_times의 길이(n) 만큼 k번 반복해야 하기 때문에 O(n^2) 이었다.

( 정확히 하면 O(n x k) )

 

그리고 효율성 테스트 제한 사항을 다시 봤을때

  • food_times 의 길이는 1 이상 200,000 이하이다.
  • food_times 의 원소는 1 이상 100,000,000 이하의 자연수이다.
  • k는 1 이상 2 x 10^13 이하의 자연수이다.

k가 커질수록 시간이 기하급수적으로 늘어나고 k의 범위는 상당했다.

 

그래서 코드의 시간복잡도를 줄일 방법을 생각해야했다.

 

 

 

새로운 접근


먼저 불필요하게 반복하여 계산되는 k를 줄이기 위한 방법을 고민해 보았는데,

 

k 시간안에 다 먹을 수 있는 음식은 다 먹어버리고 나서 계산해도 괜찮을 것 같았다.

 

그래서 음식을 시간이 작은순으로 먼저 정렬하고 차례대로 다 먹을 수 있는 음식들을 지워보았다.

 

여기서 주의해야 할 점은, food_times 배열 자체를 정렬하면 나중에 다시 음식 번호대로 순서를 계산할 수 없기 때문에

 

food_times 에서의 index를 유지한 채, time순으로 정렬을 해야했다. 그래서 생각한 방법이 "최소 힙" 이었다.

 

   import heapq
   
   ...
   
   for i in range(len(food_times)):
        heapq.heappush(q, (food_times[i], i+1))
   
   print(q)
   ...

 

입출력 예

food_timeskresult

[3, 1, 2] 5 1

이 예시를 기준으로 해본다면 위의 코드를 통해 최소 힙에는 이렇게 들어갈 것이다.

 

위 코드의 출력 결과

[(음식의 남은 시간, 음식의 index), (음식의 남은 시간, 음식의 index), (음식의 남은 시간, 음식의 index)]

 

그리고 최소 힙 이므로 (1, 2)를 pop한다면 음식의 남은 시간이 적은 (2, 3)이 앞쪽으로 와 아래와 같이 바뀔 것이다.

핵심 아이디어를 정리하자면,

  • 최소 힙을 이용해 O(log N)으로 항상 남은시간이 적은 음식이 앞으로 오도록 하며 동시에 음식의 번호까지 유지하도록 하자
  • 남은 시간이 적은 음식부터 다 먹는다고 가정했을 때 k 시간안에 다 먹을 수 있는 음식은 다 먹어버리고나서 계산을 해보자
  • k 시간안에 다 먹을 수 있는 음식은 {(음식의 시간) * (한 바퀴 도는데 걸리는 시간 = 음식의 개수)} <= k 인 음식이다.
  • 한 바퀴를 돌 때마다 각 음식의 시간이 1이 줄어든다. 그러므로 한바퀴를 돌 때마다 다른 음식들의 시간도 줄여야 한다.

핵심 아이디어의 [3, 1, 2]로 예를 들어보면,

[(1, 2), (3, 1), (2, 3)] 에서 헤드에 있는 (1, 2)는 한 바퀴 동안 음식을 다 먹을 수 있다.

한 바퀴를 돌아 (1, 2)를 다 먹고 나면 최소힙은 [(2, 3), (3, 1)]가 아닌 [(1, 3), (2, 1)] 로 시간을 1씩 줄여야한다.

 

그리고 원래는 k도 줄여주어야 하지만 k를 줄여주는 대신 그동안의 계산을 계속 누적시키고 값이 유지된 k와 비교시킨다.

 

말로는 이해가 잘 안될 수 있어 전체코드를 다시 보자.

 

import heapq

def solution(food_times, k):
    answer = 0

    if sum(food_times) <= k:
        return -1

    q=[]

    for i in range(len(food_times)):
        heapq.heappush(q, (food_times[i], i+1))


    sum_value = 0
    previous = 0
    length = len(food_times)

    for i in range(length):
        if sum_value + ((q[0][0] - previous) * length) <= k:
            now = heapq.heappop(q)[0] # 다 먹은 음식 시간
            sum_value += (now - previous) * length
            length -= 1
            previous = now
        else:
            break

    result = sorted(q, key=lambda x: x[1])

    answer = result[(k-sum_value) % length][1]
    return answer

print(solution([3, 1, 2], 5))
  • k를 줄이는 대신 sum_value를 계속 누적시킨다.
  • 남은 음식이 적은 순부터 k 시간안에 먹을 수 있는지 확인하고 다 먹어버린다.
    ( if sum_value + ((q[0][0] - previous) * length) <= k: )
  • 남은 시간이 적은 순으로 다 먹기는 하지만 한 바퀴당 하나씩만 먹을 수 있기 때문에 한 바퀴를 도는 동안 다른 음식도 1씩 다 먹어진다. 그래서 q[0][0]에서 previous를 빼주는 것이다.
  • result는 다시 음식순으로 배열을 정렬한 배열
  • answer은 result에서 남은 k ( k - sum_value ) 만큼 음식먹기를 하고 다음 먹을 음식의 index를 구한다.