刷题系列博客链接:机试题目

目录

题目及示例

我的题解

递归

备忘录递归

动态规划(迭代)

分析题目数学模型


题目及示例

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:输入: coins = [1, 2, 5], amount = 11     输出: 3       解释: 11 = 5 + 5 + 1
示例 2:输入: coins = [2], amount = 3        输出: -1
说明:你可以认为每种硬币的数量是无限的。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的题解

//动态规划求解,自底向上
class Solution {public int coinChange(int[] coins, int amount) {int[] dp = new int[amount+1]; //初始化dp表,最大只能为amount+1//dp[i] = x 表示,当目标金额为 i 时,至少需要 x 枚硬币for(int i=0; i<amount+1; i++) { //从dp[0]迭代,一直算到dp[amount],即是所求答案dp[i] = i == 0 ? 0 : amount+1; //dp[0]为0,其余为amount+1,即所有的dp[i]一开始都是无穷大for(int coin : coins) //在dp表的每个位置,对每种硬币面值组成数目都进行比较if(i >= coin) //题目给的币值数组是从小到大的,比如1,3,5dp[i] = Math.min(dp[i-coin] + 1, dp[i]); //当前能凑成i元的coin元硬币的数目}return dp[amount] == amount+1 ? -1 : dp[amount]; //返回无解或解}
}

递归

动态规划问题的一般形式是求最值,核心问题是穷举(把所有可行的答案穷举出来,然后在其中找最值)。

动态规划的穷举存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。

虽然动态规划的核心思想就是穷举求最值,但是问题千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」才能正确地穷举。

//斐波那契数列的数学形式就是递归的,这样写代码虽然简洁易懂,但是十分低效,时间复杂度为 O(2^n)
int fib(int N) {if (N == 1 || N == 2) return 1;return fib(N - 1) + fib(N - 2);
}

PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。

观察递归树,很明显发现了算法低效的原因:存在大量重复计算。这就是动态规划问题的第一个性质:重叠子问题。

备忘录递归

即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。

//斐波那契,带备忘录的递归解法,时间复杂度是 O(n)
int fib(int N) {if (N < 1) return 0;vector<int> memo(N + 1, 0); // 备忘录全初始化为 0return helper(memo, N); // 初始化最简情况
}int helper(vector<int>& memo, int n) {if (n == 1 || n == 2) // base case return 1;if (memo[n] != 0) // 查询得知已经计算过return memo[n]; //直接返回memo[n] = helper(memo, n - 1) + helper(memo, n - 2);return memo[n];
}

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。

//独立出来DP table
int fib(int N) {vector<int> dp(N + 1, 0);// base casedp[1] = dp[2] = 1;for (int i = 3; i <= N; i++)dp[i] = dp[i - 1] + dp[i - 2];return dp[N];
}

动态规划(迭代)

动态规划问题最困难的就是写出状态转移方程,即这个暴力解,优化方法无非是用备忘录或者 DP table。

//根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,
//其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。
//所以,可以进一步优化,把空间复杂度降为 O(1)。
int fib(int n) {if (n == 2 || n == 1) return 1;int prev = 1, curr = 1;for (int i = 3; i <= n; i++) {int sum = prev + curr;prev = curr;curr = sum;}return curr;
}

分析题目数学模型

1、先确定「状态」,也就是原问题和子问题中变化的变量。由于硬币数量无限,所以唯一的状态就是目标金额 amount。

2、然后确定 dp 函数的定义:当前的目标金额是 n,至少需要 dp(n) 个硬币凑出该金额。

3、然后确定「选择」并择优,也就是对于每个状态,可以做出什么选择改变当前状态(进入下一个状态即下一个选择)。具体到这个问题,无论当前的目标金额是多少,选择就是从面额列表 coins 中选择一个硬币,然后目标金额就会减少:

# 伪码框架
def coinChange(coins: List[int], amount: int):# 定义:要凑出金额 n,至少要 dp(n) 个硬币def dp(n):# 做选择,选择需要硬币最少的那个结果for coin in coins:res = min(res, 1 + dp(n - coin))return res# 我们要求的问题是 dp(amount)return dp(amount)

4、最后明确 base case,显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1:

def coinChange(coins: List[int], amount: int):def dp(n):# base caseif n == 0: return 0if n < 0: return -1# 求最小值,所以初始化为正无穷res = float('INF')for coin in coins:subproblem = dp(n - coin)# 子问题无解,跳过if subproblem == -1: continueres = min(res, 1 + subproblem)return res if res != float('INF') else -1return dp(amount)

接下来需要消除一下重叠子问题,只需要稍加修改,就可以通过备忘录消除子问题:

def coinChange(coins: List[int], amount: int):# 备忘录memo = dict()def dp(n):# 查备忘录,避免重复计算if n in memo: return memo[n]if n == 0: return 0if n < 0: return -1res = float('INF')for coin in coins:subproblem = dp(n - coin)if subproblem == -1: continueres = min(res, 1 + subproblem)# 记入备忘录memo[n] = res if res != float('INF') else -1return memo[n]return dp(amount)

很显然「备忘录」大大减小了子问题数目,完全消除了子问题的冗余,所以子问题总数不会超过金额数 n,即子问题数目为 O(n)。处理一个子问题的时间不变,仍是 O(k),所以总的时间复杂度是 O(kn)。

当然,我们也可以自底向上使用 dp table 来消除重叠子问题,dp 数组的定义和刚才 dp 函数类似,定义也是一样的:dp[i] = x 表示,当目标金额为 i 时,至少需要 x 枚硬币。

int coinChange(vector<int>& coins, int amount) {// 数组大小为 amount + 1,初始值也为 amount + 1vector<int> dp(amount + 1, amount + 1);// base casedp[0] = 0;for (int i = 0; i < dp.size(); i++) {// 内层 for 在求所有子问题 + 1 的最小值for (int coin : coins) {// 子问题无解,跳过if (i - coin < 0) continue;dp[i] = min(dp[i], 1 + dp[i - coin]);}}return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

PS:因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值。

计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举,穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。

列出动态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。

备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门。

作者:labuladong
链接:https://leetcode-cn.com/problems/coin-change/solution/dong-tai-gui-hua-tao-lu-xiang-jie-by-wei-lai-bu-ke/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

力扣题目系列:322. 零钱兑换相关推荐

  1. LeetCode刷题复盘笔记—一文搞懂完全背包之322. 零钱兑换问题(动态规划系列第十四篇)

    今日主要总结一下动态规划完全背包的一道题目,322. 零钱兑换 题目:322. 零钱兑换 Leetcode题目地址 题目描述: 给你一个整数数组 coins ,表示不同面额的硬币:以及一个整数 amo ...

  2. golang力扣leetcode 322.零钱兑换

    322.零钱兑换 322.零钱兑换 题解 代码 322.零钱兑换 322.零钱兑换 题解 //state: dp[i]金额为i时所需最少硬币个数 //function: dp[i]=dp[i-n]+1 ...

  3. 动态规划:322. 零钱兑换(完整思路过程)

    题目 322. 零钱兑换 给你一个整数数组 coins,表示不同面额的硬币:以及一个整数 amount ,表示总金额. 计算并返回可以凑成总金额所需的 最少的硬币个数 .如果没有任何一种硬币组合能组成 ...

  4. leetcode: 322.零钱兑换

    322.零钱兑换 来源:力扣(LeetCode) 链接: https://leetcode.cn/problems/coin-change/ 给你一个整数数组 coins ,表示不同面额的硬币:以及一 ...

  5. 力扣题目——429. N 叉树的层序遍历

    注:本文的代码实现使用的是 JS(JavaScript),为前端中想使用JS练习算法和数据结构的小伙伴提供解题思路. 描述 给定一个 N 叉树,返回其节点值的层序遍历.(即从左到右,逐层遍历). 树的 ...

  6. 力扣题目——637. 二叉树的层平均值

    注:本文的代码实现使用的是 JS(JavaScript),为前端中想使用JS练习算法和数据结构的小伙伴提供解题思路. 描述 给定一个非空二叉树, 返回一个由每层节点平均值组成的数组. 示例: 输入: ...

  7. 力扣题目——103. 二叉树的锯齿形层序遍历

    注:本文的代码实现使用的是 JS(JavaScript),为前端中想使用JS练习算法和数据结构的小伙伴提供解题思路. 描述 给定一个二叉树,返回其节点值的锯齿形层序遍历.(即先从左往右,再从右往左进行 ...

  8. 力扣题目——107. 二叉树的层序遍历 II

    注:本文的代码实现使用的是 JS(JavaScript),为前端中想使用JS练习算法和数据结构的小伙伴提供解题思路. 描述 给定一个二叉树,返回其节点值自底向上的层序遍历. (即按从叶子节点所在层到根 ...

  9. LeetCode:322. 零钱兑换(python)

    LeetCode:322. 零钱兑换(python) 给定不同面额的硬币 coins 和一个总金额 amount.编写一个函数来计算可以凑成总金额所需的最少的硬币个数.如果没有任何一种硬币组合能组成总 ...

  10. 【必备算法】动态规划:LeetCode题(六)322. 零钱兑换,518. 零钱兑换 II

    322. 零钱兑换² 给定不同面额的硬币 coins 和一个总金额 amount.编写一个函数来计算可以凑成总金额所需的最少的硬币个数.如果没有任何一种硬币组合能组成总金额,返回 -1. 示例 1: ...

最新文章

  1. Linux模块机制浅析
  2. 面试:一个 Java 字符串到底有多少个字符?
  3. Actor并发模型入门
  4. python遍历文件内容_Python四种逐行读取文件内容的方法
  5. finallshell堡垒机_用lshell+脚本实现堡垒机(跳转机)功能
  6. Thrift java.net.SocketException: Broken pipe问题分析定位
  7. matlab线性同余发生器,线性同余法生成伪随机数
  8. 西威变频器使用说明书_西威变频器说明书西威变频器说明书图文.doc
  9. linux系统homeinode,ubuntu14.04 64位安装H3C iNode客户端
  10. Juniper SRX240 U盘升级junos
  11. 安邦拆分的大家保险已具正常经营能力;新东方面向全球招聘3000余人 | 美通企业日报...
  12. 创意图形的绘制 会转的风车
  13. C# 枚举高级用法之Description
  14. Java中创建目录 或 创建文件
  15. c语言菜单 键盘,三、Windows键盘快捷键—处理菜单项
  16. Unity3D 设置摄像头的分辨率,比如罗技
  17. 15_常用API_第15天(Object、String、StringBuffer、用户登陆注册)
  18. 组件分享之前端组件——甘特图时间表时间线日历组件
  19. Android自定义View实现图片放大,平移和显示大图片
  20. 超调 matlab m文件,基于Matlab的汽车运动控制系统设计.doc

热门文章

  1. Jquery:ajax跨域请求处理
  2. Goldengate进程的拆分与合并
  3. js 获得网页背景色和字体色
  4. awk和perl对多文本进行统计(求他们的并集,并且如果前三列相同第四列的数字相加,第五列信息合并)...
  5. Linux系统调用之open(), close() (转载)
  6. [iphone-Game]物理引擎-资源整理贴 (3.14 更新)
  7. .NET简谈互操作(三:基础知识之DllImport特性)
  8. 【OpenCV学习笔记】【函数学习】三(cvGetCaptureProperty函数)
  9. 利用ArcGIS Pro对疫情数据进行分析并可视化
  10. 百战java课程_java百战程序员SpringBoot视频教程