(Java解题)动态规划:最优硬币组合问题 | 豆包MarsCode AI刷题

45 阅读3分钟

问题描述

小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。

例如:小C有三种硬币,面值分别为 125。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。

测试样例

样例1:

输入:coins = [1, 2, 5], amount = 18
输出:[5, 5, 5, 2, 1]

样例2:

输入:coins = [1, 3, 4], amount = 6
输出:[3, 3]

样例3:

输入:coins = [5], amount = 10
输出:[5, 5]


问题理解

我们需要找到一种方法,使用最少数量的硬币来凑出给定的总金额 N。硬币的面值和数量是无限的。目标是找到一种组合方式,使得使用的硬币数量最少。

数据结构选择

  1. 动态规划数组 dp

    • dp[i] 表示凑出金额 i 所需的最少硬币数。
    • 初始化 dp[0] = 0,表示凑出金额 0 不需要任何硬币。
    • 其他 dp[i] 初始化为一个较大的值(如 Integer.MAX_VALUE),表示初始状态下无法凑出该金额。
  2. 辅助数组 usedCoins

    • usedCoins[i] 记录凑出金额 i 时使用的最后一个硬币的面值。
    • 用于在找到最优解后回溯路径,记录使用的硬币组合。

算法步骤

  1. 初始化

    • 初始化 dp 数组,dp[0] = 0,其他 dp[i] = Integer.MAX_VALUE
    • 初始化 usedCoins 数组,用于记录使用的硬币。
  2. 动态规划求解

    • 对于每个金额 i,遍历所有硬币面值 coin
    • 如果 i >= coin 且 dp[i - coin] 不是 Integer.MAX_VALUE,则更新 dp[i] 和 usedCoins[i]
  3. 回溯路径

    • 如果 dp[amount] 仍然是 Integer.MAX_VALUE,说明无法凑出该金额,返回空列表。
    • 否则,从 amount 开始回溯,记录使用的硬币组合。
  4. 结果处理

    • 将回溯得到的硬币组合翻转,以正确的顺序返回。

代码实现

import java.util.ArrayList;
import java.util.List;
 
public class Main {
    public static List<Integer> solution(int[] coins, int amount) {
        // 初始化 dp 数组,dp[i] 表示凑出金额 i 所需的最少硬币数
        int[] dp = new int[amount + 1];
        // 初始化 usedCoins 数组,usedCoins[i] 表示凑出金额 i 时使用的最后一个硬币的面值
        int[] usedCoins = new int[amount + 1];
 
        // 将 dp 数组初始化为一个较大的值,表示初始状态下无法凑出该金额
        for (int i = 1; i <= amount; i++) {
            dp[i] = Integer.MAX_VALUE;
        }
 
        // 动态规划求解
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (i >= coin && dp[i - coin] != Integer.MAX_VALUE) {
                    if (dp[i - coin] + 1 < dp[i]) {
                        dp[i] = dp[i - coin] + 1;
                        usedCoins[i] = coin; // 记录使用的硬币
                    }
                }
            }
        }
 
        // 如果 dp[amount] 仍然是 Integer.MAX_VALUE,说明无法凑出该金额
        if (dp[amount] == Integer.MAX_VALUE) {
            return new ArrayList<>();
        }
 
        // 回溯路径,记录使用的硬币组合
        List<Integer> result = new ArrayList<>();
        int currentAmount = amount;
        while (currentAmount > 0) {
            int coin = usedCoins[currentAmount];
            result.add(coin);
            currentAmount -= coin;
        }
        System.out.println(result);
        //翻转数组,返回结果
        for (int i = 0; i < result.size() / 2; i++) {
            Integer temp = result.get(i);
            result.set(i, result.get(result.size() - 1 - i));
            result.set(result.size() - 1 - i, temp);
        }
        System.out.println(result);
        return result;
    }
 
    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)));
    }
 
}

复杂度分析

  • 时间复杂度O(N * M),其中 N 是总金额,M 是硬币面值的数量。
  • 空间复杂度O(N),用于存储 dp 数组和 usedCoins 数组。

做题心得

  • 这道题和力扣上322. 零钱兑换相似,力扣上的题是求硬币个数,这道题是求使用的硬币,因此需要在动态规划基础上用一个数组来记录使用到的硬币。