ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 힙_더 맵게_python
    프로그래머스 문제풀이 2021. 4. 22. 23:37
    728x90

    문제 설명

    매운 것을 좋아하는 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. 스코빌 지수가 1인 음식과 2인 음식을 섞으면 음식의 스코빌 지수가 아래와 같이 됩니다.
      새로운 음식의 스코빌 지수 = 1 + (2 * 2) = 5
      가진 음식의 스코빌 지수 = [5, 3, 9, 10, 12]
    2. 스코빌 지수가 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

     

    참고 :

    stackoverflow.com/questions/38806202/whats-the-time-complexity-of-functions-in-heapq-library/38833175

    github.com/python/cpython/blob/fe240882936d8b16b968b9a7adce6b2b3de4e7eb/Lib/heapq.py#L205

    yunaaaas.tistory.com/36

    728x90
Designed by Tistory.