처음 풀기 시작해서 다 풀 때까지 체감상 6시간 정도 걸린 거 같다… 딴짓하고 그런 시간도 좀 포함되긴 했지만 온전히 내가 풀었다는 것에 정말정말 만족하는 문제이다. 풀이법의 생각, 풀이 방법의 변화 등등 많은 생각을 할 수 있는 문제였다.

문제설명

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

평소 식욕이 왕성한 무지는 자신의 재능을 뽐내고 싶어 졌고 고민 끝에 카카오 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_times k result
[3,1,2] 5 1

접근

  • 정확도 테스트만 따지면 단순 for문을 돌려도 풀릴 문제지만 효율성 테스트 때문에 그렇게 할 수 없었다. 내가 생각한 방법은 매번 가장 적은 food_time를 가진 원소를 찾아내어 그 원소 * food_times의 사이즈만큼 시간이 지났다고 생각하는 것이다.
  • 처음에는 이 방법을 통해 한 번 걸러준 뒤 브루트 포스를 사용하여 찾으려 했다. 시간 초과가 났다.
  • 이분탐색을 통해 한 번 더 빼야 하는 값을 찾아주었다. 역시 시간초과가 났다.
  • 생각해보니 첫번째 생각을 통해 연산을 했을 경우 food_time이 0이 되는 상황은 더 이상 발생하지 않는다. 따라서 나머지 연산을 통해 바로 답을 찾을 수 있다! 솔직히 여기서 정답 나올 줄 알았는데 시간초과가 났다. 슬슬 짜증나기 시작했다.
  • 방법은 맞았다고 생각을 했고 남은 것은 자료 입력 방법의 차이였다. 자료를 다 옮겨 담고 정렬을 하는 것과(vector를 사용) 정렬을 하면서 자료구조에 옮겨 담는 것(Heap을 사용)이 큰 차이가 날까라는 생각을 했다. vector의 경우 push하는 시간 + 정렬하는데 걸리는 시간 즉 n + nlogn의 시간이 걸릴 것이고 Heap의 경우 push하는데 걸리는 시간(logn) * 자료의 수(n) 즉 nlogn의 시간이 걸린다. 아주 미세한 차이일거라고 생각했지만 그래도 차이가 있으니 Heap으로 도전해보기로 했다.
  • 자료의 수가 많아질수록 수행시간에서 큰 차이가 난다는 것을 알 수 있었다… 그래도 생각해낸 내 자신 아주 칭찬해…

내가 생각한 풀이

  • 위에서 생각의 과정을 다 적어놨지만 그래도 좀 더 자세하게 적어보기로 한다. [4,2,3] 이라는 food_times가 주어졌을 때 k가 8이라고 한다면 6번째 차례가 되었을 때 food_times는 [2,0,1] 이 되어 있을 것이다. 0이 되면 그 음식은 배열에서 빠지게 되고 food_times의 사이즈가 바뀌게 되므로 다시 가장 작은 숫자를 찾아서 똑같은 작업을 수행해주는 것이다. 남은 음식중 시간이 가장 적게 남은 음식의 시간(priority_queue의 top) * 배열의 크기가 k보다 작거나 같은 경우까지 모두 확인해주면 위에서 말한 것과 같이 나머지 연산을 통해 답을 찾으면 된다.
  • 처음에 vector를 사용해서 문제를 풀었다. 하지만 효율성 테스트에서 몇 개는 통과하고 몇 개는 시간초과가 났다. 똑같은 로직을 Heap을 사용하여 구현했더니 시간이 눈에 띄게 줄어들었다. 이 문제가 어려운 이유는 이런 사소한 차이를 발견해야 한다는 점인 것 같다.

코드

#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
#include <queue>
struct data{
  int time;
  int index;
};

bool cmp(const data &p1, const data &p2){
  if(p1.index < p2.index){
      return true;
  }
  else{
      return false;
  }
}
using namespace std;

int solution(vector<int> food_times, long long k) {
  int answer = 0, delete_time;
  long long time_count = 0, delete_count = 0;
  vector<struct data> v;
  priority_queue<pair<int, int>> pq;
  //priority_queue, 즉 힙을 사용하여 남은 시간이 가장 적은게 맨 앞으로 오게끔 한다.
  //C++ STL의 힙은 기본적으로 Max Heap이기 때문에 Min Heap을 만들기 위해 음수로 만들어 push한다.
  for(int i = 0 ; i<food_times.size() ; i++)
  {
      time_count += food_times[i];
      pq.push({-food_times[i], i+1});
  }
  //미리 시간을 계산해 본 후 k가 전체 음식의 시간을 합한 것보다 크다면 -1을 리턴한다.
  if(time_count <= k) return -1;
  time_count = 0;
  //while문의 조건을 잘 보자. 이 조건만 만족시킨다면 나머지 연산을 통해 바로 답을 구할 수 있다.
  while(time_count + (pq.size() * (-pq.top().first - delete_count)) <= k)
  {
      delete_time = -pq.top().first; //삭제될 음식의 시간
      int real_delete_time = delete_time - delete_count; //삭제될 음식의 실제 남은 시간
      time_count += (pq.size() * (real_delete_time)); //남은 음식 종류 * 남은 시간이 가장 적은 음식의 시간만큼 시간 더해주기
      pq.pop();
      delete_count += real_delete_time;//시간 업데이트
  }
  //index를 찾아야 하는데, 뒤죽박죽 섞여있을 뿐더러 priority_queue 특성상 index로 바로 접근하지 못하는 불편함이 있어 vector로 옮겨 담았다.
  for(int i = 0 ; !pq.empty() ; i++)
  {
      v.push_back({-pq.top().first, pq.top().second});
      pq.pop();
  }
  //index 순으로 다시 정렬을 해준다.
  stable_sort(v.begin(), v.end(), cmp);
  //남은 음식들은 어짜피 food_time이 절대 0이 되지 않기 때문에 나머지 연산을 통해 답을 구해준다.
  long long temp_index = (k - time_count)%v.size();
  answer = (v.begin() + temp_index)->index;
  return answer;
}