题目
题目链接
题目主要信息:
- 给定数组arr,arr中所有的值都为正整数且不重复
- arr中每个值代表一种面值的货币,每种面值的货币可以使用任意
- 组成aim的最少货币数
- 如果无解,请返回-1
举一反三:
本题属于背包问题的一种简化版本,学习完本题的思路帮助你解决相似的背包问题。
方法一:动态规划(推荐使用)
知识点:动态规划
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
思路:
这类涉及状态转移的题目,可以考虑动态规划。
具体做法:
- step 1:可以用\(dp[i]\)表示要凑出i元钱需要最小的货币数。
- step 2:一开始都设置为最大值\(aim+1\),因此货币最小1元,即货币数不会超过\(aim\).
- step 3:初始化\(dp[0]=0\)。
- step 4:后续遍历1元到aim元,枚举每种面值的货币都可能组成的情况,取每次的最小值即可,转移方程为$ dp[i] = min(dp[i], dp[i - arr[j]] + 1)$.
- step 5:最后比较\(dp[aim]\)的值是否超过aim,如果超过说明无解,否则返回即可。
Java实现代码:
import java.util.*;
public class Solution {public int minMoney (int[] arr, int aim) {//小于1的都返回0if(aim < 1) return 0;int[] dp = new int[aim + 1];//dp[i]表示凑齐i元最少需要多少货币数Arrays.fill(dp, aim + 1); dp[0] = 0; //遍历1-aim元for(int i = 1; i <= aim; i++){ //每种面值的货币都要枚举for(int j = 0; j < arr.length; j++){ //如果面值不超过要凑的钱才能用if(arr[j] <= i) //维护最小值dp[i] = Math.min(dp[i], dp[i - arr[j]] + 1); }}//如果最终答案大于aim代表无解return dp[aim] > aim ? -1 : dp[aim]; }
}
C++实现代码:
class Solution {
public:int minMoney(vector<int>& arr, int aim) {//小于1的都返回0if(aim < 1) return 0;//dp[i]表示凑齐i元最少需要多少货币数vector<int> dp(aim + 1, aim + 1); dp[0] = 0; //遍历1-aim元for(int i = 1; i <= aim; i++){ //每种面值的货币都要枚举for(int j = 0; j < arr.size(); j++){ //如果面值不超过要凑的钱才能用if(arr[j] <= i) //维护最小值dp[i] = min(dp[i], dp[i - arr[j]] + 1); }}//如果最终答案大于aim代表无解return dp[aim] > aim ? -1 : dp[aim]; }
};
Python代码实现:
class Solution:def minMoney(self , arr: List[int], aim: int) -> int:#小于1的都返回0if aim < 1: return 0#dp[i]表示凑齐i元最少需要多少货币数dp = [(aim + 1) for i in range(aim + 1)] dp[0] = 0#遍历1-aim元for i in range(1, aim + 1): #每种面值的货币都要枚举for j in range(len(arr)): #如果面值不超过要凑的钱才能用if arr[j] <= i: #维护最小值dp[i] = min(dp[i], dp[i - arr[j]] + 1) #如果最终答案大于aim代表无解if dp[aim] > aim: return -1else:return dp[aim]
复杂度分析:
- 时间复杂度:\(O(n\cdot aim)\),第一层遍历枚举1元到aim元,第二层遍历枚举n种货币面值
- 空间复杂度:\(O(aim)\),辅助数组dp的大小
方法二:空间记忆递归(扩展思路)
知识点:递归
递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。
思路:
对于需要凑成\(aim\)的钱,第一次我们可以选择使用\(arr[0]\),则后续需要凑出\(aim-arr[0]\)的钱,那后续就是上一个的子问题,可以用递归进行。因为每种面值使用不受限,因此第一次我们可以使用arr数组中每一个,同理后续每次也可以使用arr数组中每一次,因此每次递归都要遍历arr数组,相当于分枝为\(arr.size()\)的树型递归。
具体做法:
- step 1:递归的时候,一旦剩余需要凑出的钱为0,则找到一种情况,记录下整个的使用货币的数量,维护最小值即可。
- step 2:一旦剩余需要凑出的钱为负,则意味着这一分枝无解,返回-1.
- step 3:后续每次也可以使用arr数组中一次,进入子问题。
- step 4:但是树型递归的复杂度需要\(O(aim^{n})\),重复计算过于多了,如图所示,因此我们可以用一个dp数组记录每次递归上来的结果,避免小分支重复计算,如果dp数组有值直接获取即可,不用再重复计算了。
图示:
Java实现代码:
import java.util.*;
public class Solution {public int recursion(int[] arr, int aim, int[] dp){//组合超过了,返回-1if(aim < 0) return -1;//组合刚好等于需要的零钱if(aim == 0) return 0;//剩余零钱是否已经被运算过了if(dp[aim - 1] != 0) return dp[aim - 1];int Min = Integer.MAX_VALUE;//遍历所有面值for(int i = 0; i < arr.length; i++){ //递归运算选择当前的面值int res = recursion(arr, aim - arr[i], dp); //获取最小值if(res >= 0 && res < Min) Min = res + 1;}//更新最小值dp[aim - 1] = Min == Integer.MAX_VALUE ? -1 : Min; return dp[aim - 1];}public int minMoney (int[] arr, int aim) {//小于1的都返回0if(aim < 1) return 0;//dp[i]表示凑齐i元最少需要多少货币数int[] dp = new int[aim + 1]; return recursion(arr, aim, dp);}
}
C++实现代码:
class Solution {
public:int recursion(vector<int>& arr, int aim, vector<int>& dp){//组合超过了,返回-1if(aim < 0) return -1;//组合刚好等于需要的零钱if(aim == 0) return 0;//剩余零钱是否已经被运算过了if(dp[aim - 1] != 0) return dp[aim - 1];int Min = INT_MAX;//遍历所有面值for(int i = 0; i < arr.size(); i++){ //递归运算选择当前的面值int res = recursion(arr, aim - arr[i], dp); //获取最小值if(res >= 0 && res < Min) Min = res + 1;}//更新最小值dp[aim - 1] = Min == INT_MAX ? -1 : Min; return dp[aim - 1];}int minMoney(vector<int>& arr, int aim) {//小于1的都返回0if(aim < 1) return 0;//记录递归中间的值vector<int> dp(aim, 0); return recursion(arr, aim, dp);}
};
Python代码实现:
class Solution:def recursion(self, arr: List[int], aim: int, dp: List[int]) -> int:#组合超过了,返回-1if aim < 0: return -1#组合刚好等于需要的零钱if aim == 0: return 0#剩余零钱是否已经被运算过了if dp[aim - 1] != 0: return dp[aim - 1]Min = 10001#遍历所有面值for i in range(len(arr)): #递归运算选择当前的面值res = self.recursion(arr, aim - arr[i], dp) #获取最小值if res >= 0 and res < Min: Min = res + 1if Min == 10001:dp[aim - 1] = -1else:#更新最小值dp[aim - 1] = Min return dp[aim - 1]def minMoney(self , arr: List[int], aim: int) -> int:#小于1的都返回0if aim < 1: return 0#记录递归中间的值dp = [0 for i in range(aim)] return self.recursion(arr, aim, dp)
复杂度分析:
- 时间复杂度:\(O(n \cdot aim)\),一共需要计算aim个状态的答案,每个状态需要枚举n个面值
- 空间复杂度:\(O(aim)\),递归栈深度及辅助数组的空间