1.必须先dfs,这次的dfs+剪枝比dp还快。
先sort,把大的排前面,大的能先用就多用一点。
剪枝操作很重要,如果index超过硬币数组大小了||当前use的硬币个数+剩下需要的钱/当前硬币大小如果大于等于use,直接return,因为当前的硬币是接下来最大的,如果当前全取都优化不了的话,后面再搜索下去也是白搜索。
如果取n个当前的可以把剩下的全消耗掉,也就是left%当前==0,那么就进行更新,看res和use+当前要取几个哪个更小。取更小的。如果不能刚好取完,那就要去小的继续搜索,因为是递归,我们只需要考虑当前的硬币我们取几个,当然是取的越多越好!!从大到小遍历也算剪枝,i的初始值设为当前硬币能取的最多枚数,遍历到0个,相当于把当前从取n个到取0个全部检测一遍,这样就不会出现多取或者少取的问题了。dfs的参数也要注意,use加上的是当前取的个数,是+i;剩余减去的是i*当前硬币大小。
2.动态规划,其实也就是个背包问题,只不过更复杂,是每个物品可以取多次的问题,说到底是差不多的。dp[i][j]记录的是金额为j的情况下,你在前i枚硬币中取的硬币最少的个数可以将j取完。因为这里面存的是int而不是bool,所以我们给数组赋初始值为INF,如果dp[i][j]=inf,证明这种情况下取不到刚好的。初始化dp[i][0]=0,因为金额为0时取到的方法只有0种。
为什么是i而不是i-1,截图里的这句话很重要!!千万别搞错了。
状态压缩的dp
class Solution {
public:
const int INF = 1e9;
//use是使用个数,left是剩下多少钱没被凑
// void dfs(int use, vector<int>& coins, int left, int index){
// if(index >= coins.size() || use + left/coins[index] >= res) return;
// if(left % coins[index] == 0){
// res = min(res,use + left/coins[index]);
// return;
// }
// //不是大的取的越多越好,大的取的很多,最后不能刚好取到,比如22,数组是10,6,你直接取两个10肯定不行第一个数有取1个,取0个或者取多个好几种取法。比如18,你有10和6,那么10一个都不能取
// //所以要考虑的只是当前这一位可以取几个
// for(int i = left/coins[index]; i >= 0; --i){
// dfs(use+i,coins,left-i*coins[index],index+1);
// }
// }
int coinChange(vector<int>& coins, int amount) {
//直接dfs,先sort,大的放前面,先用大的
if(amount == 0) return 0;
// sort(coins.begin(),coins.end(),greater<int>());
// dfs(0,coins,amount,0);
// return (res == INT_MAX?-1:res);
//2ddp
//取或者不取就是背包问题,先用二维dp,背包问题一定是二维dp,dp[i][j],i就是取到第i项,j就是取到的总金额,初始化的话dp[i][0]=0,取金额0永远是0种方法
//因为这里可以一个数取多次,所以有点麻烦。如果当前nums[i]已经放不下了,就继承上一个状态。如果还放得下,就有两种情况,取或者不取,选择取和不取中较小的那个
// vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,INF));
// //basecase条件
// for(int i = 0; i <= coins.size(); ++i){
// dp[i][0] = 0;
// }
// for(int i = 1; i <= coins.size(); ++i){
// for(int j = 1; j <= amount; ++j){
// //因为可以无限取,所以从小遍历到大
// dp[i][j] = dp[i - 1][j];
// if (j >= coins[i - 1]) {
// dp[i][j] = min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
// }
// }
// }
// return (dp[coins.size()][amount] == INF ? -1:dp[coins.size()][amount]);
//状压dp
vector<int> dp(amount+1,INF);
dp[0] = 0;
for(int i = 0; i < coins.size(); ++i){
for(int j = 1; j <= amount; ++j){
if(j >= coins[i]){
dp[j] = min(dp[j],dp[j-coins[i]]+1);
}
}
}
return dp[amount] == INF?-1:dp[amount];
}
// private:
// int res = INT_MAX;
};
完全背包,可以无限取的就是完全背包,内部从1开始遍历,因为这一层需要这一层前面的情况,所以先更新前面的。如果是01背包,就要先更新后面的,因为01背包只用到上一层的情况
class Solution {
public:
const int INF = 1e9;
int coinChange(vector<int>& coins, int amount) {
//完全背包,只和上一层有关系
//无限取就从1到amount
vector<int> dp(amount+1,INF);
//dp[j]表示的是用前i种硬币可以凑出j的最小个数,如果凑不出来就是INF
dp[0] = 0;//凑0的话不管用几枚硬币都是只需要0个硬币
for(int i = 1; i <= coins.size(); ++i){
for(int j = 1; j <= amount; ++j){
//看看是否可以取coins[i-1],会不会超过J
//相当于dp[i][j]=dp[i-1][j]和dp[i][j-coins[i-1]]+1
//为什么要从1到amount,因为dp[j-coins[i-1]]我们需要的是这一层的情况
if(j >= coins[i-1]) dp[j] = min(dp[j],dp[j-coins[i-1]]+1);
}
}
return dp[amount] == INF ?-1:dp[amount];
}
};
左边的结果要更新过的,因为我们是无限取的,所以要考虑取过第i枚硬币的情况。
dp[i][j]代表前i个元素可以组成总金额j的最少硬币数,重点是可以重复取
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//完全背包问题,可以连续取当前元素
//dp[i][j]代表前i个元素可以组成总金额j的最少硬币数,重点是可以重复取
//状态转移方程:当取了当前元素不会<0,就有两种情况,取或者不取它dp[i][j] = min(dp[i-1][j-coins[i-1]]+1,dp[i-1][j])
//如果会,就不取它,dp[i][j] = dp[i-1][j]
//因为要取较小值,所以
// vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,1e9));
// //basecase:取总额为0的时候就是0枚硬币
// for(int i = 0; i <= coins.size(); ++i){
// dp[i][0] = 0;
// }
// for(int i = 1; i <= coins.size(); ++i){
// for(int j = 1; j <= amount; ++j){
// if(j < coins[i-1]){
// dp[i][j] = dp[i-1][j];
// }
// else{
//左边的结果要更新过的,因为我们是无限取的,所以要考虑取过第i枚硬币的情况
// dp[i][j] = min(dp[i-1][j],dp[i][j-coins[i-1]]+1);
// }
// }
// }
// return dp[coins.size()][amount] == 1e9 ? -1 : dp[coins.size()][amount];
//状压dp,只和左和上有关,但是左是要更新过的左,所以内部循环还是从小到大
vector<int> dp(amount+1,1e9);
dp[0] = 0;
for(int i =1; i <= coins.size(); ++i){
for(int j = 1; j <= amount; ++j){
if(j >= coins[i-1]) dp[j] = min(dp[j],dp[j-coins[i-1]]+1);
}
}
return dp[amount] == 1e9 ? -1 : dp[amount];
}
};