CS/알고리즘

[커뮤러닝/4기] 프로그래머스 3주차 - 더 맵게

HC-Kang 2022. 5. 11. 22:45

더 맵게

레벨2, 힙

링크

작년에 프로그래밍을 처음 시작하고, 처음으로 접했던 프로그래머스 레벨 2 문제였습니다.

당시에는 heap이 뭔지도 몰랐기에.. 분명 논리적으로 맞는데, 계속 시간초과가 나서 정말 미치는줄 알았습니다. 그러다 결국 포기했었던 기억이 있는 문제입니다

 

1. 문제 설명

매운 것을 좋아하는 Leo는 모든 음식의 스코빌 지수를 K 이상으로 만들고 싶습니다.
모든 음식의 스코빌 지수를 K 이상으로 만들기 위해 Leo는 스코빌 지수가 가장 낮은 두 개의 음식을 아래와 같이 특별한 방법으로 섞어 새로운 음식을 만듭니다.

섞은 음식의 스코빌 지수 = 가장 맵지 않은 음식의 스코빌 지수 + (두 번째로 맵지 않은 음식의 스코빌 지수 * 2)

Leo는 모든 음식의 스코빌 지수가 K 이상이 될 때까지 반복하여 섞습니다.
Leo가 가진 음식의 스코빌 지수를 담은 배열 scoville과 원하는 스코빌 지수 K가 주어질 때, 모든 음식의 스코빌 지수를 K 이상으로 만들기 위해 섞어야 하는 최소 횟수를 return 하도록 solution 함수를 작성해주세요.

 

  문제의 요구사항은 매우 간단합니다. 가장 안 매운 음식을(작은 수) 두 가지를 뽑아, 이를 조합해 조금 더 매운 음식(더 큰 수)을 만드는 행동을 반복해서, 최소 K만큼은 매운 음식을 만들어야 합니다.

 

2. 제한사항

1. scoville의 길이는 2 이상 1,000,000 이하입니다.
2. K는 0 이상 1,000,000,000 이하입니다.
3. scoville의 원소는 각각 0 이상 1,000,000 이하입니다.
4. 모든 음식의 스코빌 지수를 K 이상으로 만들 수 없는 경우에는 -1을 return 합니다.

 

3. 사고과정 / 나의 풀이

  처음에는 조합 전, 조합 후 2개의 리스트를 가지고 문제를 풀 생각이었습니다. 그렇지만, 조금 생각 해 보니, [1, 1, 2, 2, 2, 9] 같은 경우 조합 후에는 리스트가 [3], [2, 2, 2, 9] 이 되고 한 번 더 조합하면 [3, 6], [2, 9]이 되는 등, 생각보다 비교 로직이 많아진다(귀찮아진다)는 생각에 그냥 하나의 리스트만 사용하기로 했습니다.

 

  그래서 가장 단순하게 생각 해 본 절차는

 

1. 내림차순으로 정렬한다.
2. 2개를 pop한다.
3. 결과를 append한다.
4. 횟수를 +1 한다
5. 이를 조건이 만족할 때 까지 반복한다.

 

이를 가장 단순하게 표현한 코드는 아래와 같습니다.

 

 

def solution(scoville, K):
    answer = 0
    while min(scoville) < K:
        scoville.sort(reverse=True)
        a = scoville.pop()
        b = scoville.pop()
        new = a + 2 * b
        scoville.append(new)
        answer += 1

    return answer

 

 

  논리적으로는 틀릴 이유가 없는 풀이라고 생각해서 제출 해 보니, 역시 정확성은 통과... 일 줄 알았는데 런타임 에러가 꽤 많이 났습니다.

물론 당연하게도 효율성은 0점이 나왔습니다.

 

  조금 생각 해 보니, pop이 연달아 있는것이 문제인 것 같습니다. 그래서 가장 단순하게 try - except문을 활용 해 봤습니다.

 

def solution(scoville, K):
    answer = 0
    while min(scoville) < K:
        try:
            scoville.sort(reverse=True)
            a = scoville.pop()
            b = scoville.pop()
            new = a + 2 * b
            scoville.append(new)
            answer += 1
        except:
            return -1

    return answer

 

  결과적으로 정확성은 모두 통과했습니다. 그러나 역시나 효율성은 0점입니다.

 

  왜일지 코드를 확인 해 보면, while문을 순회하며 매번 sort를 하게됩니다.

아무리 파이썬 기본 sort가 빠르다지만(상대적으로), 역시 O(nlogn)의 벽은 높습니다. (참고: 파이썬 시간복잡도 정리)

문제풀이 페이지에 남아있는 풀이를 보니, 이전에는 여기까지 왔다가  포기한 것 같았습니다.

 

  그렇지만 이제는 힙에 대해서 이해했고, 써먹을 줄도 알게 되었기에, 아래와 같이 코드를 수정 해 줍니다.

 

 

import heapq

def solution(scoville, K):
    answer = 0
    heapq.heapify(scoville)
    while scoville[0] < K:
        try:
            a = heapq.heappop(scoville)
            b = heapq.heappop(scoville)
            new = a + b + b
            heapq.heappush(scoville, new)
            answer += 1
        except:
            return -1

    return answer

 

  가장 큰 다른점은, 힙의 구조상 pop, push가 반복되어도 정렬을 매번 할 필요가 없다는 점이겠고, 이로인해 효율성이 매우 높아집니다.

따라서 최초에 heapify 한 번만 해주면 정렬은 신경 쓸 필요가 없게 됩니다.

 

3. 다른 사람의 풀이

 

가장 먼저 보이는 풀이

 

import heapq as hq

def solution(scoville, K):

    hq.heapify(scoville)
    answer = 0
    while True:
        first = hq.heappop(scoville)
        if first >= K:
            break
        if len(scoville) == 0:
            return -1
        second = hq.heappop(scoville)
        hq.heappush(scoville, first + second*2)
        answer += 1  

    return answer

 

  제 마지막 풀이와 거의 비슷한데, 다만 while문에 조건을 거는 대신, 탈출조건으로 이를 처리했습니다. 이후에 커뮤러닝 강사님께서도 말씀하셨다시피, 간략하게 풀이 할 때에는 탈출조건을 활용하는 것이 가장 편리 한 것 같습니다.

 

from heapq import heapify, heappush, heappop

def solution(scoville, K):
    heapify(scoville)
    for i in range(1000000):
        try:
            heappush(scoville, heappop(scoville)+(heappop(scoville)*2))
            if scoville[0] >= K: return i+1
        except:
            return -1

 

  처음에 보고는 가장 의문이 많이 들었던 코드였습니다. 아무래도 이유는 for문을 활용했다는 점과, range에 저렇게 무심한듯 숫자로 박아넣은 부분, 두 가지가 그 원인이 아닐까 싶은데, 생각해보면 가장 영리한 풀이가 아닐까 싶습니다.

  굳이 answer+=1 같은 코드도 활용하지 않고 i로 모두 해결 해 버렸으며, 문제의 조건에서 길이도 1,000,000으로 제한되어있으니 딴지를 걸 수 없겠습니다. 부족한 지식을 어찌 쥐어짜서 꼬투리를 잡아봐도 결국은 가독성 말고는 흠잡을 곳이 없어보입니다.