背包问题:322.零钱兑换

84 阅读2分钟

leetcode: 322. 零钱兑换

题目描述

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例

输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1

解题思路

朴素解法

定义dp[i][j]为使用前i个硬币,凑成总金额为j,需要的最少的硬币数量。

那么,显然 状态转移可以表示为:dp[i][j] = min(dp[i-1][j], dp[i-1][j-v]+1, ..., dp[i-1][j-k*v]+k)

class Solution {
    int INF = 0x3f3f3f3f;

    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[][] dp = new int[n + 1][amount + 1];
        Arrays.fill(dp[0], INF); dp[0][0] = 0;

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= amount; j++) {
                int val = coins[i - 1];
                dp[i][j] = dp[i - 1][j];
                for (int k = 1; k * val <= j; k++) {
                    dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - k * val] + k);
                }
            }
        }

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

一维空间优化

观察下面两个转移方程:

dp[i][j] = min(dp[i-1][j], dp[i-1][j-v]+1, ..., dp[i-1][j-k*v]+k)

dp[i][j-v] = min(dp[i-1][j-v], ..., dp[i-1][j-k*v]+(k-1))

显然,可以得到dp[i][j] = min(dp[i-1][j], dp[i][j-v]+1

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

i维度进行消除,因为dp[i-1][j]可以看作使用前i种硬币(0个第i种硬币)的情况,即dp[i][j]=dp[i-1][j],那么dp[i][j]就只取决于dp[i][j-v]

维度消除后的状态转移方程如下,容量维度遍历顺序从小到大: dp[j] = min(dp[j], dp[j-v]

class Solution {
    int INF = 0x3f3f3f3f;

    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, INF); dp[0] = 0;

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

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