最优硬币组合问题 | 豆包MarsCode AI刷题

72 阅读3分钟

问题分析

原题链接:最优硬币组合问题

题目给定硬币面值列表,每种面值的硬币数量都是无限的,要求使用最少的硬币数量凑够给定的总金额 amount

对于这道题,可以转化为完全背包问题,即某种物品的数量无限,要求使用最少的物品恰好装满背包

算法描述

状态定义:dp[i][j] 表示考虑前 i 个硬币,恰好凑出总金额 j 的最小硬币数量

鉴于硬币的供应量是无限的,对于总金额 j,我们假设已经找到了使用前 i - 1 种硬币面值的最优解。现在,当我们引入第 i 种硬币面值时,我们需要考虑所有可能的该面值硬币的数量,因此可以推导出以下递推关系:

dp[i][j] = min(dp[i-1][j], dp[i-1][j-coins[i]] + 1, dp[i-1][j - 2 * coins[i]] + 2, ...)

对于总金额 j - coins[i] > 0,可以得到如下的推导公式:

dp[i][j - coins[i]] = max(dp[i-1][j - coins[i]], dp[i-1][j - 2 * coins[i]] + 2, dp[i-1][j - 3 * coins[i]] + 2, ...)

合并两项可得:

dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i]] + 1)

这样,我们就把状态转移方程从无限多项转化为只有 2 项,且符合最优子结构,因此我们可以使用动态规划进行求解

算法需要两层循环,整体的时间复杂度是 O(n2)O(n^2)

如果只存储最优的硬币数量,空间复杂度是 O(n×amount)O(n \times amount),其中 amount 是要凑够的总金额

状态压缩

观察状态转移方程,可以发现第 i 层的结果只需要通过第 i 层和第 i-1 层计算而来,因此我们可以进一步将空间复杂度压缩到 O(amount)O(amount)

如果将 dp 数组从二维压缩到一维,重新考虑状态转移方程:

  • 第 1 项 dp[i-1][j]:直接由上一状态转移过来,直接去掉[i-1] 即可
  • 第 2 项 dp[i][j-coins[i]] + 1:从第 i 层的角度,后面状态是由前面状态转移过来的。因此,我们需要第 i 层前面的状态是第 i 层的,后面还未更新的状态是第 i - 1 层的。因此我们去掉 [i] 的同时,确保内层循环是从前往后遍历。

最终的状态转移方程:dp[j] = min(dp[j], dp[j-coins[i]] + 1)

接下来,我们可以从直觉的角度,重新理解这个状态转移方程。

外层循环遍历硬币的面值列表,内层循环从 coins[i] 开始遍历,直到 j == amount,内层循环其实就是在模拟一直增加 coins[i] 的数量,直到超过总金额 amount 为止

方案存储

这道题的不同之处是,要求我们给出最终方案,而不是最终最少的硬币数。因此,在求解的过程红,我们还需要一个临时变量存储最优方案

完整代码


import java.util.*;

public class Main {
    public static List<Integer> solution(int[] coins, int amount) {
        // Edit your code here
        int[] dp = new int[amount + 1];
        for(int i = 1; i <= amount; i++) {
            dp[i] = Integer.MAX_VALUE / 2;
        }
        List<List<Integer>> res = new ArrayList<>();
        for(int i = 0; i <= amount; i++) {
            res.add(new ArrayList<>());
        }
        for(int coin : coins) {
            for(int j = coin; j <= amount; j++) {
                if( dp[j - coin] + 1 < dp[j] ) {
                    dp[j] = dp[j - coin] + 1;
                    List<Integer> list = new ArrayList<>(res.get(j - coin));
                    list.add(coin);
                    res.set(j, list);
                }
            }
        }
        
        List<Integer> ans = res.get(amount);
        Collections.sort(ans, Collections.reverseOrder());
        return ans;
    }

    public static void main(String[] args) {
        // Add your test cases here
        
        System.out.println(solution(new int[]{1, 2, 5}, 18).equals(List.of(5, 5, 5, 2, 1)));
    }
}