🙊🙊作者主页: 🔗天天爱编程
作者简介:阿里非典型程序员一枚 ,一起学习Java、大数据、数据结构算法(公众号同名)
💞💞觉得文章还不错的话欢迎大家点赞👍➕收藏⭐️➕评论💬支持博主🤞
————————————————-
- 标签(题目类型):动态规划
题目描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,
返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
思路及实现
方式一:递归+记忆化搜索
思路
递归地尝试使用每种硬币,选择最小的硬币数量。为了避免重复计算,使用记忆化搜索(dp数组)来存储中间结果。
代码实现
Java版本
public class Solution {
private int[] memo;
public int coinChange(int[] coins, int amount) {
memo = new int[amount + 1];
Arrays.fill(memo, -2); // 初始化为-2,表示还未计算
return dp(coins, amount);
}
private int dp(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
if (amount < 0) {
return -1;
}
if (memo[amount] != -2) {
return memo[amount];
}
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = dp(coins, amount - coin);
if (res >= 0 && res < min) {
min = res + 1;
}
}
memo[amount] = (min == Integer.MAX_VALUE) ? -1 : min;
return memo[amount];
}
}
说明:
- 使用
memo
数组来存储计算过的结果,避免重复计算。memo
数组初始化为-2,表示还未计算;-1表示无法凑成总金额;其他值表示凑成总金额所需的最少硬币数量。
C语言版本
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int* memo;
int coinChange(int* coins, int coinsSize, int amount) {
memo = (int*)malloc((amount + 1) * sizeof(int));
for (int i = 0; i <= amount; i++) {
memo[i] = -2;
}
return dp(coins, coinsSize, amount);
}
int dp(int* coins, int coinsSize, int amount) {
if (amount == 0) {
return 0;
}
if (amount < 0) {
return -1;
}
if (memo[amount] != -2) {
return memo[amount];
}
int min = INT_MAX;
for (int i = 0; i < coinsSize; i++) {
int res = dp(coins, coinsSize, amount - coins[i]);
if (res >= 0 && res < min) {
min = res + 1;
}
}
memo[amount] = (min == INT_MAX) ? -1 : min;
return memo[amount];
}
int main() {
int coins[] = {1, 2, 5};
int amount = 11;
int coinsSize = sizeof(coins) / sizeof(coins[0]);
int result = coinChange(coins, coinsSize, amount);
printf("Minimum number of coins required: %d\n", result);
free(memo);
return 0;
}
说明:
- C语言版本的实现与Java版本类似,主要差异在于内存分配和释放,以及输入输出处理。
Python3版本
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
memo = [-2] * (amount + 1)
def dp(n):
if n == 0:
return 0
if n < 0:
return -1
if memo[n] != -2:
return memo[n]
min_coins = float('inf')
for coin in coins:
res = dp(n -coin)
if res >= 0 and res < min_coins:
min_coins = res + 1
memo[n] = min_coins if min_coins != float('inf') else -1
return memo[n]
return dp(amount)
说明:
- Python版本的实现同样采用记忆化搜索,通过闭包函数
dp
来实现递归。
复杂度分析
- 时间复杂度:O(m * n),其中m是硬币种类的数量,n是总金额。
- 空间复杂度:O(n),使用了一个大小为n+1的数组来存储中间结果。
方式二:动态规划
思路
自底向上地计算每个金额所需的最少硬币数量,使用一个数组来存储中间结果。
代码实现
Java版本
public class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 初始化为一个大值
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
说明:
dp[i]
表示凑成金额i
所需的最少硬币数量。- 初始化
dp
数组时,将所有值设为一个大值(这里使用amount + 1
),表示凑不成当前金额。- 当
dp[i - coin] + 1
小于dp[i]
时,更新dp[i]
。
C语言版本
#include <stdio.h>
#include <limits.h>
int coinChange(int* coins, int coinsSize, int amount) {
int dp[amount + 1];
for (int i = 0; i <= amount; i++) {
dp[i] = INT_MAX;
}
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coinsSize; j++) {
if (coins[j] <= i) {
dp[i] = fmin(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
int main() {
int coins[] = {1, 2, 5};
int amount = 11;
int coinsSize = sizeof(coins) / sizeof(coins[0]);
int result = coinChange(coins, coinsSize, amount);
printf("Minimum number of coins required: %d\n", result);
return 0;
}
说明:
- C语言版本的实现与Java版本类似,只是语法和输入输出处理有所不同。
Python3版本
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# 示例使用
solution = Solution()
coins = [1, 2, 5]
amount = 11
result = solution.coinChange(coins, amount)
print(f"Minimum number of coins required: {result}")
说明:
- Python版本使用了列表来实现动态规划算法,并且给出复杂度分析。
复杂度分析
-
时间复杂度:O(m * n)
在Java、C和Python的实现中,我们都有两个嵌套的循环。外层循环遍历每个可能的金额(从1到amount),内层循环遍历每种硬币。因此,时间复杂度是O(m * n),其中m是硬币种类的数量,n是总金额。 -
空间复杂度:O(n)
对于动态规划的实现,我们使用了一个长度为amount+1的数组来存储每个金额所需的最少硬币数。因此,空间复杂度是O(n),其中n是总金额。
总结
动态规划是解决这类问题的有效方法,因为它避免了重复计算子问题,从而提高了效率。在记忆化搜索和动态规划之间选择时,如果问题的规模较小或者递归结构清晰,记忆化搜索可能更为直观。然而,对于大规模问题或需要优化的场景,动态规划通常更加高效。
无论是记忆化搜索还是动态规划,它们的核心思想都是将原问题分解为子问题,并存储子问题的解以避免重复计算。在实际应用中,根据问题的特点和需求,可以选择合适的方法来解决问题。
方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
记忆化搜索 | O(m * n) | O(n) | 直观易理解,递归结构清晰 | 可能存在递归调用的开销 |
动态规划 | O(m * n) | O(n) | 高效,避免重复计算 | 可能需要更多的初始设置和迭代步骤 |
需要注意的是,记忆化搜索和动态规划在本质上都是利用子问题的解来避免重复计算,因此它们的时间复杂度通常是相同的。主要的区别在于实现方式和直观性。记忆化搜索通常是通过递归实现的,而动态规划则是通过迭代实现的。在实际应用中,可以根据问题的特性和个人偏好来选择合适的方法。
相似题目
题目名 | 难度 | 力扣链接 |
---|---|---|
硬币找零 | 中等 | leetcode-322 |
硬币找零 II | 中等 | leetcode-518 |
组合总和 | 中等 | leetcode-377 |
组合总和 II | 中等 | leetcode-40 |
组合总和 III | 中等 | leetcode-216 |
组合总和 IV | 困难 | leetcode-773 |
同时,题目难度可能因个人经验和技能水平的不同而有所差异。在解决这些题目时,建议您仔细阅读题目描述和要求,并尝试使用动态规划、贪心算法等算法和技术来解决问题。通过不断练习和思考,您可以逐渐提高解决这类问题的能力。
欢迎一键三连(关注+点赞+收藏),技术的路上一起加油!!!代码改变世界
关于我:阿里非典型程序员一枚 ,记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法(公众号同名),回复暗号,更能获取学习秘籍和书籍等
—⬇️欢迎关注我的公众号:
进朱者赤
,认识不一样的技术人。⬇️—