-
힙_더 맵게_python프로그래머스 문제풀이 2021. 4. 22. 23:37728x90
문제 설명
매운 것을 좋아하는 Leo는 모든 음식의 스코빌 지수를 K 이상으로 만들고 싶습니다. 모든 음식의 스코빌 지수를 K 이상으로 만들기 위해 Leo는 스코빌 지수가 가장 낮은 두 개의 음식을 아래와 같이 특별한 방법으로 섞어 새로운 음식을 만듭니다.
섞은 음식의 스코빌 지수 = 가장 맵지 않은 음식의 스코빌 지수 + (두 번째로 맵지 않은 음식의 스코빌 지수 * 2)
Leo는 모든 음식의 스코빌 지수가 K 이상이 될 때까지 반복하여 섞습니다.
Leo가 가진 음식의 스코빌 지수를 담은 배열 scoville과 원하는 스코빌 지수 K가 주어질 때, 모든 음식의 스코빌 지수를 K 이상으로 만들기 위해 섞어야 하는 최소 횟수를 return 하도록 solution 함수를 작성해주세요.제한 사항
- scoville의 길이는 2 이상 1,000,000 이하입니다.
- K는 0 이상 1,000,000,000 이하입니다.
- scoville의 원소는 각각 0 이상 1,000,000 이하입니다.
- 모든 음식의 스코빌 지수를 K 이상으로 만들 수 없는 경우에는 -1을 return 합니다.
입출력 예
입출력 예 설명
- 스코빌 지수가 1인 음식과 2인 음식을 섞으면 음식의 스코빌 지수가 아래와 같이 됩니다.
새로운 음식의 스코빌 지수 = 1 + (2 * 2) = 5
가진 음식의 스코빌 지수 = [5, 3, 9, 10, 12] - 스코빌 지수가 3인 음식과 5인 음식을 섞으면 음식의 스코빌 지수가 아래와 같이 됩니다.
새로운 음식의 스코빌 지수 = 3 + (5 * 2) = 13
가진 음식의 스코빌 지수 = [13, 9, 10, 12]
모든 음식의 스코빌 지수가 7 이상이 되었고 이때 섞은 횟수는 2회입니다.
문제 접근 방법
1. heapify와 힙 정렬을 통해 주어진 배열을 힙으로 만들고 내림차순 정렬을 통해 해결하려고 하였지만 입출력만 통과할뿐 대부분 테스트에서 통과하지 않았다. 가능한 배운내용을 활용하고 풀고 싶었지만, 통과를 할 수 없었다.
2. 파이썬 heapq 라이브러리
소스를 얻기 위해 구글링을 해보니 파이썬의 heapq 라는 라이브러리에 대해 알게 되었다.
파이썬의 heapq 라는 내장모듈을 통해 힙 우선순위 큐 알고리즘을 제공하는 미친 효율의 라이브러리가 있었다.
우리는 구현에 신경쓰지 않고 기능만을 사용하여 이 문제를 풀 수 있었다.
실제로 내부구조를 들여다보니 push, pop, replace등 다양한 함수들을 제공했다.
참고 : github.com/python/cpython/blob/master/Lib/heapq.py
해당 문제는 heapq를 사용하지 않고는 모두 런타임 에러를 발생시킨다고 한다.
먼저 주어진 배열을 힙으로 변환한다. 이때 heap은 min heap이 디폴트이다. 따라서 0번째 인덱스는 항상! 가장 작은 값이 되며 이 특성을 이용해 스코빌 지수 이상이 될 경우 반복문을 종료한다.
- 모든 음식의 스코빌 지수를 K 이상으로 만들 수 없는 경우에는 -1을 return 한다라는 제약이 있다.
스코빌 넘버를 반복할 수록 pop을 2번 push를 1번 사용하였기에 언젠가는 힙의 길이가 0이 된다. 길이가 0이 될때까지 앞의 조건문을 만족하지 못할경우 예외처리 -1을 리턴한다.
import heapq def solution(scoville, K): answer = 0 # 배열을 힙으로 변환 heapq.heapify(scoville) # 0번째 인덱스가 가장 작은 값(min heap) # 기본적으로 heapq는 min heap을 default로 함 while True: number_1 = heapq.heappop(scoville) if number_1 >= K: break if len(scoville) == 0: return -1 number_2 = heapq.heappop(scoville) scoville_number = number_1 + (number_2 * 2) heapq.heappush(scoville, scoville_number) answer += 1 return answer
시간복잡도
1. heapq=> heapify
heap을 공부할때 heapify 함수를 만들며 heapify의 시간복자도는 O(logN)라고 알고 있었다.
heapq의 heapify 함수는 단순히 heapify 알고리즘 한번을 실행하는 것이 아닌 힙 정렬로 배열을 힙으로 변환한다.
heapq의 heapify 함수는 O(N)이라는 의견도 있지만, O(NlogN)이 더 정확한 것 같다.
(참고 : yunaaaas.tistory.com/36)
아래는 heapq 라이브러리의 내부구조이다.
def heapify(x): """Transform list into a heap, in-place, in O(len(x)) time.""" n = len(x) # Transform bottom-up. The largest index there's any point to looking at # is the largest with a child index in-range, so must have 2*i + 1 < n, # or i < (n-1)/2. If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so # j-1 is the largest, which is n//2 - 1. If n is odd = 2*j+1, this is # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1. for i in reversed(range(n//2)): _siftup(x, i) def _siftup(heap, pos): endpos = len(heap) startpos = pos newitem = heap[pos] # Bubble up the smaller child until hitting a leaf. childpos = 2*pos + 1 # leftmost child position while childpos < endpos: # Set childpos to index of smaller child. rightpos = childpos + 1 if rightpos < endpos and not heap[childpos] < heap[rightpos]: childpos = rightpos # Move the smaller child up. heap[pos] = heap[childpos] pos = childpos childpos = 2*pos + 1 # The leaf at pos is empty now. Put newitem there, and bubble it up # to its final resting place (by sifting its parents down). heap[pos] = newitem _siftdown(heap, startpos, pos) def _siftdown(heap, startpos, pos): newitem = heap[pos] # Follow the path to the root, moving parents down until finding a place # newitem fits. while pos > startpos: parentpos = (pos - 1) >> 1 parent = heap[parentpos] if newitem < parent: heap[pos] = parent pos = parentpos continue break heap[pos] = newitem
heapify의 경우 배열 내부의 원소들이 _siftup, _siftdown 메서드를 이용해 힙 구조에 맞게 노드의 위치를 재배치한다.
_siftup의 경우 왼쪽 자식 노드를 찾고 이 왼쪽 자식노드가 힙의 마지막 노드보다 커질때까지 재배치 되며
_siftdown의 경우 부모노드가 힙 속성을 위배할 경우 부모노드를 트리 아래쪽으로 재배치 시키며 힙 속성을 유지한다.
이때 최소값이 가장 첫번째 인덱스로 위치하게 된다.
힙의 경우 완전 이진트리로 노드의 개수가 N일때 반복문처럼 N번 반복하지 않는다. 이는 힙_1, 힙_2 포스팅했듯이 최악의경우 트리의 높이(logN)만큼 비례하기 때문이다.
아래 설명할 heappop(), heappush()의 삽입과 삭제 연산의 경우 _siftup 함수를 호출하며 이는 O(logN)이다.
즉, heapq 라이브러리에서의 heapify 시간 복잡도의경우 O(NlogN) 을 갖는다 볼 수 있다.
2. heapq => heappop()
def heappop(heap): """Pop the smallest item off the heap, maintaining the heap invariant.""" lastelt = heap.pop() # raises appropriate IndexError if heap is empty if heap: returnitem = heap[0] heap[0] = lastelt _siftup(heap, 0) return returnitem return lastelt
O(1), O(1)도 있지만 _siftup 재배치 과정은 트리의 높이인 logN과 비례하며, 시간복잡도는 O(logN)이다.
3. heapq => heappush()
def heappushpop(heap, item): """Fast version of a heappush followed by a heappop.""" if heap and heap[0] < item: item, heap[0] = heap[0], item _siftup(heap, 0) return item
동일.
해답의 총 시간복잡도는 O(NlogN)으로 생각된다!
import heapq def solution(scoville, K): answer = 0 # 배열을 힙으로 변환 heapq.heapify(scoville) # 0번째 인덱스가 가장 작은 값(min heap) # 기본적으로 heapq는 min heap을 default로 함 while True: number_1 = heapq.heappop(scoville) if number_1 >= K: break if len(scoville) == 0: return -1 number_2 = heapq.heappop(scoville) scoville_number = number_1 + (number_2 * 2) heapq.heappush(scoville, scoville_number) answer += 1 return answer
참고 :
github.com/python/cpython/blob/fe240882936d8b16b968b9a7adce6b2b3de4e7eb/Lib/heapq.py#L205
728x90'프로그래머스 문제풀이' 카테고리의 다른 글
정렬_K번째수_python_Js (0) 2021.05.02 스택/큐_주식가격_python (0) 2021.04.27 해시_전화번호 목록_python (0) 2021.04.19 스택/큐_다리를 지나는 트럭_python (0) 2021.04.13 프로그래머스_LV1_크레인 인형뽑기 게임 (0) 2020.10.28