박종훈 알고리즘 블로그

(Leetcode) 322 - Coin Change 풀이

New Year Gift - Curated List of Top 75 LeetCode Questions to Save Your Time

위 링크에 있는 추천 문제들을 시간이 있을때마다 풀어보려고 한다.


https://leetcode.com/problems/coin-change/description/

DP 문제이다.

내가 풀었던 방식

처음 생각

처음에는 큰 동전부터 넣어서 될 때까지 하면 베스트 일 것이라고 생각했다. 하지만 그렇게 간단하지는 않았다.

예를들어 [100, 30, 1] 이라는 동전이 있다고 해보자. 이때 주어지는 amount는 120 이라고 하면

30으로는 4개면 되지만 100을 먼저 채우면 30은 들어갈 수 없고 1짜리 20개를 더 써서 21개의 동전이 사용된다.

중간 생각

그래서 모든 경우의 수를 다 찾아봐야 겠다고 생각했다. 처음에는 잘 먹히는 듯 보였으나, 역시나 복잡한 경우로 인해 타임아웃이 발생되었다.

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return 0

        coins.sort()
        coins.reverse()

        stack = []
        worst = math.floor(amount / coins[-1])
        min = worst + 1

        def calc(index, amount):
            nonlocal min
            # print("start:", stack, len(stack), amount, index)
            if len(stack) == worst:
                return -1 if min > worst else min

            while index == len(coins):
                last_removed_index = stack.pop()
                amount += coins[last_removed_index]
                index = last_removed_index + 1

            while amount > 0:
                stack.append(index)
                amount -= coins[index]

            if amount == 0:
                if min > len(stack):
                    min = len(stack)
                    # print("min:", min)

            removed_index = stack.pop()
            amount += coins[removed_index]
            return calc(removed_index + 1, amount)

        try:
            return calc(0, amount)
        except:
            return min

모범 답안

모든 시점에서의 최선을 상황을 찾는다.

from typing import List
import math


class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [0] + ([float('inf')] * amount)
        for i in range(1, amount + 1):
            for coin in coins:
                if coin <= i:
                    dp[i] = min(dp[i], dp[i - coin] + 1)

        if dp[-1] == float('inf'):
            return -1
        return dp[-1]

예시를 들어 차근차근 실행시켜보기

for 문의 끝에 아래와 같이 print를 넣어주면 다음과 같이 print 된다.

print(solution.coinChange([1, 2, 5], 10))
[0, 1, inf, inf, inf, inf, inf, inf, inf, inf, inf] 1
[0, 1, 1, inf, inf, inf, inf, inf, inf, inf, inf] 2
[0, 1, 1, 2, inf, inf, inf, inf, inf, inf, inf] 3
[0, 1, 1, 2, 2, inf, inf, inf, inf, inf, inf] 4
[0, 1, 1, 2, 2, 1, inf, inf, inf, inf, inf] 5
[0, 1, 1, 2, 2, 1, 2, inf, inf, inf, inf] 6
[0, 1, 1, 2, 2, 1, 2, 2, inf, inf, inf] 7
[0, 1, 1, 2, 2, 1, 2, 2, 3, inf, inf] 8
[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, inf] 9
[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 2] 10

풀어서 설명하면 다음과 같다.

example page1 example page2 example page3 example page4 example page5

시간복잡도

  • 이중 반복문 사용
    • 바깥쪽 반복문 : amount 만큼 반복 (O(n))
    • 안쪽 반복문 : coins 리스트의 length 만큼 반복 (O(m))

O(n * m) 의 시간 복잡도를 가짐

공간복잡도

amount 만큼 dp array를 생성함

O(n) 의 공간 복잡도를 가짐

결론

DP 문제를 더 많이 접해봐야겠다는 생각이 들었다.

java 로 다시 풀기 (24.07.09)

먼저는 stream으로 풀었다. 이미 들린 dp pointer 일 때, 기존에 등록된 값보다 클 경우 (dp[currentPointer] > dp[amount] + 1) 이후 dfs 단계를 생략하도록 한 것이 포인트이다.

처음으로 dp[0]에 도달했다고 해서 최소값이 아니기 때문에 전체 케이스를 고려해야 하는데 그렇다고 진짜로 전체 케이스를 확인해본다면 timeout이 발생된다. 따라서 이 조건문을 찾아내지 못하면 timeout이 발생된다.

class Solution {
    public int coinChange(int[] coins, int amount) {
        if(amount == 0) {
            return 0;
        }

        int[] dp = new int[amount + 1];

        List<Integer> sortedCoins = Arrays.stream(coins).boxed()
                .sorted(Collections.reverseOrder())
                .toList();

        sortedCoins.forEach(coin -> dfs(dp, sortedCoins, amount, coin));

        return dp[0] == 0 ? -1 : dp[0];
    }

    void dfs(int[] dp, List<Integer> coins, int amount, int selectedCoin) {
        int currentPointer = amount - selectedCoin;
        if (currentPointer < 0) {
            return;
        }

        if (dp[currentPointer] == 0 || dp[currentPointer] > dp[amount] + 1) {
            dp[currentPointer] = dp[amount] + 1;
            coins.forEach(coin -> dfs(dp, coins, currentPointer, coin));
        }
    }
}

stream 의 경우 for loop 보다는 느리지만 이해하기에는 훨씬 좋다. 더 직관적이다.

java-using-for-loop

다만 성능을 더 최적화 하기 위해서 for loop를 이용해서 풀도록 바꾸면 다음과 같다.

class Solution {
    public int coinChange(int[] coins, int amount) {
        if(amount == 0) {
            return 0;
        }

        int[] dp = new int[amount + 1];

        Arrays.sort(coins);
        for (int i = coins.length - 1; i > -1; i--) {
            dfs(dp, coins, amount, coins[i]);
        }

        return dp[0] == 0 ? -1 : dp[0];
    }

    void dfs(int[] dp, int[] coins, int amount, int selectedCoin) {
        int currentPointer = amount - selectedCoin;
        if (currentPointer < 0) {
            return;
        }

        if (dp[currentPointer] == 0 || dp[currentPointer] > dp[amount] + 1) {
            dp[currentPointer] = dp[amount] + 1;
            for (int i = coins.length - 1; i > -1; i--) {
                dfs(dp, coins, currentPointer, coins[i]);
            }
        }
    }
}

java-using-for-loop

stream을 사용했을 때보다 시간이 단축된 것을 볼 수 있다.

TS, SC

코인의 수를 n 이라고 했을 때, O(n * amount ^ 2) 의 시간복잡도와 O(amount) 의 공간복잡도를 가진다.

java 모범 답안 (더 효율적인 방법)

위 코드는 dp 라는 array를 사용하긴 했지만, 사실 brute force 에 가까운 방법이다.

아래와 같이 작성하면 더 효율적으로 동작한다.

public class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;

        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }

        return dp[amount] > amount ? -1 : dp[amount];
    }
}

TS, SC

코인의 수를 n 이라고 했을 때, O(n * amount) 의 시간복잡도와 O(amount) 의 공간복잡도를 가진다.

java-using-dp

categories: 스터디-알고리즘

tags: 파이썬 , 알고리즘 , python , algorithm , Leetcode , DP , coin , java , stream