Day39 | 动态规划 :完全背包应用 零钱兑换&&零钱兑换II
动态规划应该如何学习?-CSDN博客
01背包模板 | 学习总结-CSDN博客
完全背包模板总结-CSDN博客
难点:
代码都不难写,如何想到完全背包并把具体问题抽象为完全背包才是关键
@[toc]
322.零钱兑换
322. 零钱兑换 - 力扣(LeetCode)
思路分析:
完全背包模板总结-CSDN博客
322. 零钱兑换 - 力扣(LeetCode)
套用模板时候的注意点
1.这道题的金币可以重复选,所以是完全背包不是01背包
2.这道题是返回最少的金币数量,所以dfs/dp的含义就是前i种金币可以凑成金额为c的面额的最少金币数量是多少
3.由于求的是金币数量,所以面额amount就是背包的容量,物品的重量就是自己的面额,每个物品的价值都为1,因为dfs和dp的含义是最少的金币的数量是多少
4.求的是最少,所以要把max换成min,初始化的时候要初始化为INT_MAX/2而不是0(除以2是因为返回的INT_MAX+1后会溢出,导致报错)
1.回溯法
1.参数和返回值
i是物品编号,表示从前i个物品里面选
c是容量
coins是w[i]
v[i]的值全都为1这里就不写了
1
| int dfs(int i,int c,vector<int>& coins)
|
2.终止条件
i小于0说明到了树形结构的最下面了
如果同时容量c==0,那说明正好可以凑够,我们就找到了一个合法的方案,返回0(不返回1的原因是我们dfs的含义是最少金币的数量,而不是能凑够amount的方案数量)
如果容量不是0就凑不够,返回INT_MAX/2
如果容量不够当前的硬币,那就递归下一个物品去了,最后返回的就是在前i-1种金币能凑够amount的最小金额数
1 2 3 4 5 6 7
| if(i<0) if(c==0) return 0; else return INT_MAX/2; if(c<coins[i]) return dfs(i-1,c,coins);
|
3.本层逻辑
递归公式max换成min
1
| return min(dfs(i-1,c,coins),dfs(i,c-coins[i],coins)+1);
|
完整代码:
当然是超时的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Solution { public: int dfs(int i,int c,vector<int>& coins) { if(i<0) if(c==0) return 0; else return INT_MAX/2; if(c<coins[i]) return dfs(i-1,c,coins); return min(dfs(i-1,c,coins),dfs(i,c-coins[i],coins)+1); } int coinChange(vector<int>& coins, int amount) { int res=dfs(coins.size()-1,amount,coins); if(res<INT_MAX/2) return res; else return -1; } };
|
2.记忆化搜索
就是还是全都初始化为-1,每次返回前给dp赋值,碰到不是-1的那就是算过的,那就直接返回计算过的结果,不需要再次递归了
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public: int dfs(int i,int c,vector<int>& coins,vector<vector<int>>& dp) { if(i<0) if(c==0) return 0; else return INT_MAX/2; if(dp[i][c]!=-1) return dp[i][c]; if(c<coins[i]) return dp[i][c]=dfs(i-1,c,coins,dp); return dp[i][c]=min(dfs(i-1,c,coins,dp),dfs(i,c-coins[i],coins,dp)+1); } int coinChange(vector<int>& coins, int amount) { vector<vector<int>> dp(coins.size(),vector<int>(amount+1,-1)); int res=dfs(coins.size()-1,amount,coins,dp); if(res<INT_MAX/2) return res; else return -1; } };
|
3.动态规划
笔者选择的是dp数组的物品编号从1开始
递归的边界条件是
i<0的时候c==0是返回0的。而我们这里的dp数组编号从1开始,那就是i<1且c==0的时候是等于0的
所以dp[0][0]的初始值就为0,其他的都是INT_MAX/2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,INT_MAX/2)); dp[0][0]=0; for(int i=1;i<=coins.size();i++) for(int j=0;j<=amount;j++) if(j<coins[i-1]) dp[i][j]=dp[i-1][j]; else dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]]+1); if(dp[coins.size()][amount]<INT_MAX/2) return dp[coins.size()][amount]; else return -1; } };
|
4.滚动数组
把第一维度全都删掉即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int> dp(amount+1,INT_MAX/2); dp[0]=0; for(int i=1;i<=coins.size();i++) for(int j=coins[i-1];j<=amount;j++) dp[j]=min(dp[j],dp[j-coins[i-1]]+1); if(dp[amount]<INT_MAX/2) return dp[amount]; else return -1; } };
|
518.零钱对换II
518. 零钱兑换 II - 力扣(LeetCode)
思路分析:
这回求的是方案数量很明显,求方案数量那dfs的的含义就是在前i种金币里面有多少种方法可以恰好凑够当前的容量c,递推公式和01背包变形类似
就把max换成+就可以了,即两种情况的方案数加起来就是当前i,c的总方案数量
1.回溯暴力枚举
就挑重点说吧
终止条件如果c==0说明恰好可以凑够,那说明我们找到了合法的方案,由于我们dfs的含义是返回合法的方案数量,所以我们每找到一个合法方案返回的就是1,否则就是0.
完整代码
当然是超时的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public: int dfs(int i,int c,vector<int>& coins) { if(i<0) if(c==0) return 1; else return 0; if(c<coins[i]) return dfs(i-1,c,coins); return dfs(i-1,c,coins)+dfs(i,c-coins[i],coins); } int change(int amount, vector<int>& coins) { return dfs(coins.size()-1,amount,coins); } };
|
2.记忆化搜索
还是老样子
初始化dp为-1,如果不是-1说明计算过了直接返回
并且在返回之前给dp赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public: int dfs(int i,int c,vector<int>& coins,vector<vector<int>>& dp) { if(i<0) if(c==0) return 1; else return 0; if(dp[i][c]!=-1) return dp[i][c]; if(c<coins[i]) return dp[i][c]=dfs(i-1,c,coins,dp); return dp[i][c]=dfs(i-1,c,coins,dp)+dfs(i,c-coins[i],coins,dp); } int change(int amount, vector<int>& coins) { vector<vector<int>> dp(coins.size(),vector<int>(amount+1,-1)); return dfs(coins.size()-1,amount,coins,dp); } };
|
3. 1:1翻译为动态规划
递归终止条件就是动态规划的初始化
i<0且c==0的时候就是返回1,而我们这里dp数组中的物品编号是从1开始的。所以,dp[0][0]就等于1。
(dp数组物品编号从0开始的太麻烦了就不写了),注意一点事dp数组中物品编号是从1开始的,而coins数组却是从0开始的
所以比较和加减coins[i]的地方都是coins[i-1]
因为dp数组中的第i个元素的重量在coins 的i-1的位置存储着
注意定义为unsigned,不然有个测试案例过不去
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public: int change(int amount, vector<int>& coins) { vector<vector<unsigned>> dp(coins.size()+1,vector<unsigned>(amount+1,0)); dp[0][0]=1; for(int i=1;i<=coins.size();i++) for(int j=0;j<=amount;j++) if(j<coins[i-1]) dp[i][j]=dp[i-1][j]; else dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]]; return dp[coins.size()][amount]; } };
|
4.滚动数组优化
就是删掉第一维度就完事
从1开始
1 2 3 4 5 6 7 8 9 10 11
| class Solution { public: int change(int amount, vector<int>& coins) { vector<unsigned> dp(amount+1,0); dp[0]=1; for(int i=1;i<=coins.size();i++) for(int j=coins[i-1];j<=amount;j++) dp[j]=dp[j]+dp[j-coins[i-1]]; return dp[amount]; } };
|
从0开始
1 2 3 4 5 6 7 8 9 10 11
| class Solution { public: int change(int amount, vector<int>& coins) { vector<unsigned> dp(amount+1,0); dp[0]=1; for(int i=0;i<coins.size();i++) for(int j=coins[i];j<=amount;j++) dp[j]=dp[j]+dp[j-coins[i]]; return dp[amount]; } };
|