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

263 阅读4分钟

一、 最优硬币组合问题

问题描述

小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]

二、解题思路

  1. 动态规划思想

    • 这类问题可以使用动态规划来解决,因为我们可以通过小金额的解来逐步求解大金额的解。我们通过逐步建立一个状态 dp[i],表示凑出金额 i 所需的最少硬币数。最后,dp[amount] 就会给出凑出目标金额所需的最少硬币数。
  2. 状态定义

    • 用 dp[i] 来表示凑出金额 i 的最少硬币数。
    • 另外,使用一个 usedCoins[i] 数组来记录凑出金额 i 时,最后使用的是哪种硬币。这样可以在最后一步回溯得到硬币的具体组合。
  3. 初始化

    • 初始化 dp[0] = 0,因为凑出金额为 0 不需要任何硬币。
    • 其他 dp[i] 初始化为 Integer.MAX_VALUE,表示初始状态下无法凑出该金额。
  4. 动态转移

    • 对于每个金额 i,我们遍历所有硬币面值 coin,如果 i >= coin 并且 dp[i - coin] != Integer.MAX_VALUE(即凑出 i - coin 需要的硬币数是有限的),我们就考虑用一个 coin 来凑出金额 i。此时,dp[i] 可以更新为 dp[i - coin] + 1,表示在凑出 i - coin 的基础上再加上一个硬币 coin
    • 同时,我们记录 usedCoins[i] = coin,即凑出金额 i 时,最后使用了硬币 coin
  5. 回溯求解硬币组合

    • 在动态规划表构建完成后,我们通过 usedCoins 数组回溯,找出所有使用过的硬币面值,直到金额为 0。
  6. 结果返回

    • 返回通过回溯得到的硬币组合。

三、solution函数

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)));
    }
 
}

四、时间和空间复杂度分析

时间复杂度分析

  1. 动态规划填表部分

    • 外层循环遍历 amount(从 1 到 amount),总共进行 amount 次。
    • 内层循环遍历硬币数组 coins,假设硬币的数量为 n,则内层循环执行 n 次。

    因此,动态规划填表的时间复杂度是 O(amount × n) ,其中 amount 是目标金额,n 是硬币种类的数量。

  2. 回溯求解硬币组合部分

    • 回溯的过程中,每次减去一个硬币,直到金额为 0。最多需要遍历 amount 个硬币(在极端情况下每次选择的都是面值为 1 的硬币),因此回溯的时间复杂度是 O(amount)

总时间复杂度O(amount × n) + O(amount),其中 amount 是目标金额,n 是硬币种类数量。对于大部分情况下,主导的时间复杂度是O(amount × n)

空间复杂度分析

  1. dp 数组:用来存储每个金额所需的最少硬币数,长度为 amount + 1,因此空间复杂度是 O(amount)

  2. usedCoins 数组:用来记录每个金额所用的最后一个硬币面值,长度也是 amount + 1,因此空间复杂度是 O(amount)

  3. 结果列表:用于存储硬币的组合,最坏情况下,结果列表的长度为 amount(例如当每次都选择面值为 1 的硬币时),因此空间复杂度是 O(amount)

总空间复杂度:O(amount) + O(amount) + O(amount) = O(amount)