青训营X豆包MarsCode AI刷题第123题(中等) | 豆包MarsCode AI刷题

45 阅读4分钟

题目序号:123

题目难度:中等

题目描述:

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

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

测试用例:

image.png

1.题目理解:

这个问题属于组合问题,问题的目标是在给定一系列硬币面值和特定目标金额的情况下,寻找一种硬币组合方案,使得这些硬币的总价值恰好等于目标金额,并且使用的硬币数量尽可能少。这种问题也是一个寻求最优解类问题,即我们追求的是最小化使用的硬币总数。

通常会考虑使用动态规划(DP)或深度优先搜索(DFS)算法处理这类问题。这道题我选择使用DP来做

定义状态:创建一个数组dp,其中dp[i]表示组成金额i所需的最少硬币数量。

初始化状态dp[0]初始化为0,因为组成金额0不需要任何硬币。对于所有其他的i1amount),初始时dp[i]可以被设置为一个很大的数(比如Integer.MAX_VALUE),表示这些金额暂时无法被组成。

状态转移方程:对于每个硬币面值coin,遍历dp数组,从coinamount,更新dp[i]的值。如果dp[i - coin]不是初始的很大值(即i - coin金额可以被组成),则更新dp[i]dp[i]dp[i - coin] + 1中的较小值。这表示组成金额i所需的最少硬币数是组成i - coin所需的最少硬币数加1(当前硬币)。

边界条件:在遍历过程中,如果dp[i]的值没有被更新过(仍然是初始的很大值),则表示金额i无法被组成。

构建解: 如果dp[amount]不是初始的很大值,那么表示目标金额可以被组成。此时,可以通过逆向遍历dp数组来构建具体的硬币组合。从amount开始,对于每个硬币面值,如果dp[amount]等于dp[amount - coin] + 1,则表示可以使用coin面值的硬币,将amount减去coin,并记录下这个硬币。

2.代码

import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;

public class Main {
    public static List<Integer> solution(int[] coins, int amount) {
        Arrays.sort(coins);
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int num : coins) {
            for (int i = num; i <= amount; i++) {
                if (dp[i - num] != Integer.MAX_VALUE) {
                    dp[i] = Math.min(dp[i], dp[i - num] + 1);
                }
            }
        }
        if (dp[amount] == Integer.MAX_VALUE) {
            return null;
        }
        List<Integer> result = new ArrayList<>();
        int remainingTotal = amount;
        for (int i = coins.length - 1; i >= 0; i--) {
            while (remainingTotal >= coins[i] && dp[remainingTotal] == dp[remainingTotal - coins[i]] + 1) {
                result.add(coins[i]);
                remainingTotal -= coins[i];
            }
        }
        // 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)));
    }
}

3.代码讲解

使用Arrays.sort(coins)对硬币面值进行排序,这样可以从最小的硬币面值开始处理。

创建一个长度为amount + 1的数组dp,用于存储动态规划的中间结果。dp[i]表示组成金额i所需的最少硬币数量。初始化所有值为Integer.MAX_VALUE,表示一个非常大的数,代表初始时无法组成该金额。

设置dp[0]为0,因为组成金额0不需要任何硬币。

外层循环遍历每个硬币面值,内层循环从当前硬币面值到amount,更新dp数组。如果dp[i - num]不是Integer.MAX_VALUE(即可以组成金额i - num),则更新dp[i]dp[i - num] + 1和当前dp[i]的最小值,这表示使用一个num面值的硬币来组成金额i

检查dp[amount]是否仍然是Integer.MAX_VALUE,如果是,则表示无法组成金额amount,返回null

如果能够组成金额amount,创建一个ArrayList来存储组成该金额的硬币。然后从amount开始,逆向遍历每个硬币面值,如果dp[remainingTotal]等于dp[remainingTotal - coins[i]] + 1,则表示可以使用coins[i]面值的硬币来减少remainingTotal,并将该硬币添加到结果列表中。

4.时间复杂度分析

dp所用时间复杂度通常为O(nm),n和m分别为dp矩阵的长和宽。所以本题中的时间复杂度为O(amount*n),前者为金额,后者为硬币的种类,可以近似为O(nn)