|
| 1 | +# https://leetcode.com/problems/coin-change/ |
| 2 | + |
| 3 | +from functools import cache |
| 4 | +from typing import List |
| 5 | +import math |
| 6 | + |
| 7 | +class Solution: |
| 8 | + def coinChange_n(self, coins: List[int], amount: int) -> int: |
| 9 | + """ |
| 10 | + [Complexity] |
| 11 | + - TC: O(n * amount) |
| 12 | + - SC: O(n * amount) |
| 13 | +
|
| 14 | + [Approach] |
| 15 | + 각 coin을 무한히 많이 사용할 수 있으므로 unbounded knapsack problem 이다. |
| 16 | + 이때, 가치를 최대화하는 것 == 동전의 개수를 최소화 하는 것이다. |
| 17 | + 따라서 2D DP로 풀 수 있다. |
| 18 | + """ |
| 19 | + |
| 20 | + INF = amount + 1 |
| 21 | + n = len(coins) |
| 22 | + |
| 23 | + # dp[i][j] = i번째 coin까지 사용했을 때, j 만큼의 amount를 만들 수 있는 coin의 최소 개수 |
| 24 | + dp = [[INF] * (amount + 1) for _ in range(n + 1)] |
| 25 | + dp[0][0] = 0 |
| 26 | + |
| 27 | + for i in range(1, n + 1): # -- coin |
| 28 | + dp[i][0] = 0 |
| 29 | + for j in range(1, amount + 1): # -- amount |
| 30 | + if j < coins[i - 1]: |
| 31 | + dp[i][j] = dp[i - 1][j] # 현재 coin을 넣을 수 없음 |
| 32 | + else: |
| 33 | + dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1) # min(현재 coin을 넣지 X 경우, 현재 coin을 넣는 경우) |
| 34 | + |
| 35 | + return dp[n][amount] if dp[n][amount] != INF else -1 |
| 36 | + |
| 37 | + def coinChange_1(self, coins: List[int], amount: int) -> int: |
| 38 | + """ |
| 39 | + [Complexity] |
| 40 | + - TC: O(n * amount) |
| 41 | + - SC: O(amount) |
| 42 | +
|
| 43 | + [Approach] |
| 44 | + 매 단계에서 다음의 두 값만 확인하므로, 2D DP를 rolling array 방식으로 1D DP로 space optimize 할 수 있다. |
| 45 | + - dp[i - 1][j] |
| 46 | + - dp[i][j - coins[i - 1]] |
| 47 | + """ |
| 48 | + |
| 49 | + INF = amount + 1 |
| 50 | + |
| 51 | + dp = [INF] * (amount + 1) |
| 52 | + dp[0] = 0 |
| 53 | + |
| 54 | + for coin in coins: |
| 55 | + for amnt in range(coin, amount + 1): |
| 56 | + dp[amnt] = min(dp[amnt], dp[amnt - coin] + 1) # min(현재 coin을 넣지 X 경우, 현재 coin을 넣는 경우) |
| 57 | + |
| 58 | + return dp[amount] if dp[amount] != INF else -1 |
| 59 | + |
| 60 | + def coinChange_b(self, coins: List[int], amount: int) -> int: |
| 61 | + """ |
| 62 | + [Complexity] |
| 63 | + - TC: O(n * amount) (금액 1 ~ amount 각각에 대해 len(coins) 만큼 확인) |
| 64 | + - SC: O(amount) (seen & q) |
| 65 | +
|
| 66 | + [Approach] |
| 67 | + BFS로 최단거리를 찾듯이 접근해도 된다. 이때의 최단거리란 최소 개수를 의미한다. |
| 68 | + """ |
| 69 | + from collections import deque |
| 70 | + |
| 71 | + q = deque([(0, 0)]) # (총 금액, coin 개수) |
| 72 | + seen = {0} # 이미 확인한 총 금액 |
| 73 | + |
| 74 | + while q: |
| 75 | + amnt, n = q.popleft() |
| 76 | + |
| 77 | + # base condition |
| 78 | + if amnt == amount: |
| 79 | + return n |
| 80 | + |
| 81 | + # iter |
| 82 | + for coin in coins: |
| 83 | + if (new_amnt := amnt + coin) <= amount and new_amnt not in seen: |
| 84 | + q.append((new_amnt, n + 1)) |
| 85 | + seen.add(new_amnt) |
| 86 | + |
| 87 | + return -1 |
| 88 | + |
| 89 | + def coinChange(self, coins: List[int], amount: int) -> int: |
| 90 | + """ |
| 91 | + [Complexity] |
| 92 | + - TC: O(n * amount) (금액 0 ~ amount, 각각 len(coins) 만큼 확인) |
| 93 | + - SC: O(amount) (@cache 저장 공간, call stack) |
| 94 | +
|
| 95 | + [Approach] |
| 96 | + bottom-up이었던 DP 뿐만 아니라, 더 직관적인 top-down 접근도 가능하다. |
| 97 | + 이때 @cache를 사용하면 memoization을 통해 더 최적화할 수 있다. |
| 98 | + """ |
| 99 | + |
| 100 | + @cache |
| 101 | + def dp(amnt): |
| 102 | + # base condition |
| 103 | + if amnt == 0: |
| 104 | + return 0 |
| 105 | + if amnt < 0: |
| 106 | + return math.inf |
| 107 | + |
| 108 | + # recur |
| 109 | + return min(dp(amnt - coin) + 1 for coin in coins) |
| 110 | + |
| 111 | + res = dp(amount) |
| 112 | + |
| 113 | + return res if res != math.inf else -1 |
0 commit comments