ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 36. 배낭문제(Knapsack problem) - 탐욕법, 동적 계획법 - 파이썬
    Python/파이썬 자료구조 알고리듬 2019. 6. 30. 18:57
    반응형

    배낭 문제는 짐을 쪼갤 수 있느냐 없느냐로 나뉩니다. 

     

    쪼갤 수 있는 경우

     

    분할 가능 배낭 문제라고 합니다. 
    배낭에 담을 수 있는 물건이 분할이 되면 쉽습니다. (금가루, 은가루, 철가루)
    부피 (또는 무게) 대비 가치가 높은 물건들부터 담으면 되기 때문입니다. 

     

    이것을 탐욕(greedy) 알고리듬이라고 합니다. 

     

    부피 대비 가치가 높은 물건 순서대로 담으면 되고, 
    담을 때 '뒤에 어떤 물건을 넣을지'
    고려하지 않아도 되기 때문에 붙여진 이름입니다. 

     

    (탐욕스럽게) 비싼 것부터 막 넣어~!

     

    어떤 행위가 나중에 어떤 영향을 미칠 지 생각하지 않고

    현재 가장 이익이 되는 행위만 취해도
    최고의 결과가 나올 때
    사용하는 쉬운 알고리듬입니다. 

     

    동전 거스름돈의 경우도 대부분의 국가에서
    탐욕 알고리듬으로 정답이 나오도록 되어 있습니다. 
    우리나라도 500원, 100원, 50원, 10원, 5원, 1원으로 5 단위(?)로 되어 있습니다.. 

    def count_changes(coins, money):
        changes = []
        for coin in coins:
            changes.append(money // coin)
            money %= coin
        return changes
    
    
    print(count_changes([500, 100, 50, 10, 5, 1], 2025))  # [4, 0, 0, 2, 1, 0]

     

    만약 이 단위를 벗어난 동전(321원)이 있다면
    높은 금액의 동전부터 순서대로, 최대한(=탐욕적으로)
    많은 동전을 주는 알고리듬으로는 답이 나올 수 없겠죠? 

     

    이런 경우도 동적계획법으로 답을 찾을 수 있습니다만.. 
    내용이 많아 별도의 글로 올립니다. 

     

    쪼갤 수 있는 경우는 너무 쉬워서 굳이 코딩을 안 해도 될 것 같습니다.
    개념만... 

     

      금가루 은가루 철가루
    부피 1 2 3
    단가 3 2 1

    배낭의 여유 공간: 4

    금가루 1 + 은가루 2 + 철가루 1 을 담는 것이 가장 많은 가치를 담을 수 있습니다. 

     

    쪼갤 수 없는 경우

    쪼갤 수 없는 배낭 문제를 0-1 배낭 문제라고도 합니다. 

    부피(무게) 대비 가치가 높은 순서로 담는 게 답이 되지 않을 수 있습니다. 

     

    재귀

    def knapsack(capacity, n):
        if capacity == 0 or n == 0:
            return 0
        if size[n-1] > capacity:
            return knapsack(capacity, n-1)
        else:
            return max(value[n-1] + knapsack(capacity-size[n-1], n-1), knapsack(capacity, n-1))
    
    
    size = [3, 4, 7, 8, 9]
    value = [4, 5, 10, 11, 13]
    print(knapsack(10, 5))  # 14

    마지막 리턴문이 모든 것을 설명합니다.

    1. 하나를 배낭에 넣었을 때 가치의 증가분 + 다음 항목을 검색하는 함수(물론 공간이 줄어듬도 입력)

    2. 배낭을 넣지 않고 다음을 검색하는 함수

    그리고 이 둘 중 큰 값을 선택합니다. 

     

    불필요하게 전역변수를 쓰는 것은 좋지 않은 습관입니다.
    프로젝트가 커지면 전역변수는 여러 가지로 불편합니다. 
    이렇게 몇 줄 안되는 코드에서야... 별 상관은 없습니다만..

     

    전역변수를 쓰지 않기 위한 테크닉들은
    다음과 같습니다.

     

    1. 매개변수에 모두 집어넣는다. 
    2. 클래스를 사용한다. 
    3. 함수 내 함수를 사용한다. 

     

    전역변수를 쓰지 않고, 매개변수를 사용했습니다.
    가독성은 좋지 않지만, 이해하기는 쉽습니다. 

    def knapsack(capacity, n, size, value):
        if capacity == 0 or n == 0:
            return 0
        if size[n - 1] > capacity:
            return knapsack(capacity, n - 1, size, value)
        else:
            return max(value[n - 1] + knapsack(capacity - size[n - 1], n - 1, size, value),
                       knapsack(capacity, n - 1, size, value))
    
    
    print(knapsack(10, 5, [3, 4, 7, 8, 9], [4, 5, 10, 11, 13]))  # 14

     

    클래스를 사용했습니다. 

    class Knapsack:
        def __init__(self, size, value):
            self.size = size
            self.value = value
    
        def tuck_into(self, capacity, n):
            if capacity == 0 or n == 0:
                return 0
            if self.size[n - 1] > capacity:
                return self.tuck_into(capacity, n - 1)
            else:
                return max(self.value[n - 1] + self.tuck_into(capacity - self.size[n - 1], n - 1),
                           self.tuck_into(capacity, n - 1))
    
    
    knapsack = Knapsack([3, 4, 7, 8, 9], [4, 5, 10, 11, 13])
    print(knapsack.tuck_into(10, 5))  # 14

     

    함수내 함수를 사용했습니다. 
    1번과 함수의 시그니쳐가 같기 때문에
    바로 리팩토링할 수 있습니다. 

     

    파이써닉하죠. 

     

    외부 함수 knapsack의 parameter(매개변수)들인 size와 value는 
    내부 함수 tuck_into의 전역변수처럼 사용되겠죠?

     

    이너 함수와 아우터 함수의 변수명이 같으면 좋지 않기 때문에
    명확하게 capacity_, n_ 으로 언더스코어로 구분을 해 주었습니다. 
    이들은 리턴 행에 있는 truk_into의 argument(인자)로만 사용됩니다. 

    def knapsack(size, value, capacity_, n_):
        def tuck_into(capacity, n):
            if capacity == 0 or n == 0:
                return 0
            if size[n - 1] > capacity:
                return tuck_into(capacity, n - 1)
            else:
                return max(value[n - 1] + tuck_into(capacity - size[n - 1], n - 1),
                           tuck_into(capacity, n - 1))
    
        return tuck_into(capacity_, n_)
    
    
    print(knapsack([3, 4, 7, 8, 9], [4, 5, 10, 11, 13], 10, 5))  # 14

     

    재귀를 사용하지 않고...

    2차원 배열을 만듭니다. 

    array[i][s]는 배낭 크기가 s일 때, i개의 물건을 넣었을 때, 가능한 최대 가치를 의미합니다.

    역시 이번에도 이 2차원 배열이 문제의 해결의 핵심이 됩니다. 

    def knapsack1(capacity, n):
        array = [[0 for _ in range(capacity+1)] for _ in range(n+1)]
        for i in range(1, n+1):
            for s in range(1, capacity+1):
                if size[i-1] > s:  # 물건의 부피가 s보다 크면
                    array[i][s] = array[i - 1][s]
                else:  # 물건의 부피가 s보다 작거나 같으면
                    array[i][s] = max(value[i-1] + array[i-1][s-size[i-1]], array[i-1][s])
                print('%2d' % array[i][s], end=' ')
            print()
        return array[n][capacity]
    
    
    size = [9, 3, 4, 7, 8]
    value = [13, 4, 5, 10, 11]
    print(knapsack1(10, 5))
     0  0  0  0  0  0  0  0  0  0  0 
     0  0  0  4  4  4  4  4  4  4  4 
     0  0  0  4  5  5  5  9  9  9  9 
     0  0  0  4  5  5  5 10 10 10 14 
     0  0  0  4  5  5  5 10 11 11 14 
     0  0  0  4  5  5  5 10 11 13 14 
    14

    혹시 이해가 가지 않으신 다면 막대기 자르기부터 순서대로 보시는 게 좋습니다. 

    반응형
Designed by Tistory.