概念:
动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化(en:memoization)存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
套路:
1)找到什么可变参数可以代表一个递归状态, 也就是哪些参数一旦确定, 返回值就确定了【walk(int N, int cur, int rest, int P)中的cur和rest】
2)把可变参数的所有组合映射成一张表, 有 1 个可变参数就是一维表, 2 个可变参数就是二维表, ......[cur、rest两个0维 组成的二维表 如何使数组就复杂了,所以一般是解成小问题,尽可能维度要低 ]
3)最终答案要的是表中的哪个位置, 在表中标出
4)根据递归过程的 base case, 把这张表的最简单、 不需要依赖其他位置的那些位置填好值
5)根据递归过程非base case的部分, 也就是分析表中的普遍位置需要怎么计算得到, 那
么这张表的填写顺序也就确定了
6)填好表, 返回最终答案在表中位置的值
1. 518. 零钱兑换 II(方法数)
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
暴力递归
索引index和cursum作为可变参数,参与暴力递归
//暴力递归
class solution {
public:
int change(int amount, vector<int>& coins) {
if(amount<0)
return 0;
return p1(coins,0,amount);
}
int p1(vector<int>& coins,int index,int amount)// 不断减少amout
{// 当前情况下返回的个数
int res=0;
if(index == coins.size())//最后一个了
return amount==0?1:0;
// 暴力,当前的位置选了几次
for(int i=0;coins[index]*i <= amount;i++){
res+=p1(coins,index+1,amount-coins[index]*i);
return res;}
}
};
记忆化搜索(函数外加个傻缓存)
- 由可变参数构成一个结构(2个就是一个矩阵)(初始化,在递归函数之外)
- get 操作,看此次可变参数对应的位置,之前出现过没
- put 操作,将运算得到的此次可变参数,对应的res弄给矩阵
class Solution {
public:
int change(int amount, vector<int>& coins) {
if(amount<0)
return 0;
vector<vector<int> > temp(coins.size()+1,vector<int>(amount+1,0));
return p1(coins,0,amount,temp);
}
int p1(vector<int>& coins,int index,int amount,vector<vector<int> >& temp)
{
int res=0;
if(index == coins.size())
res= amount==0?1:0;
else
{
int tempval=0;
for(int i=0;coins[index]*i<=amount;i++)
{
tempval=temp[index+1][amount-coins[index]*i];
// get 操作,get出来看之前是不是已经算出来了
if(tempval!=0)
res+= tempval==-1?0:tempval;
else
res+= p1(coins,index+1,amount-coins[index]*i,temp);
}
}
// put操作,将这次计算的res放到,由可变形参组成的矩阵里面。
temp[index][amount]= res==0?-1:res;
return res;
}
};
动态规划
class Solution {
public:
int change(int amount, vector<int>& coins) {
if (amount < 0 || (coins.size() == 0 && amount != 0))
return 0;
if (coins.size() == 0 && amount == 0)
return 1;
#弄一个等大小的辅助矩阵
vector<vector<int> > dp(coins.size(), vector<int>(amount + 1, 0));
#先把能填的都填上
for (int i = 0; i < coins.size(); ++i)
dp[i][0] = 1;
for (int i = 0; i < amount + 1; i++)
{
if (i%coins[0] == 0)
dp[0][i] = 1;
else
dp[0][i] = 0;
}
#在用归纳法,推导之后的与之前的关系
for (int i = 1; i < coins.size(); ++i)
{
for (int j = 1; j < amount + 1; ++j)
{
if (coins[i] <= j) //动态规划改进
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[coins.size() - 1][amount];
}
};
322. 零钱兑换(最小零钱数)一维vec(amount,INT_MAX)
因为是最小零钱数,所以最开始的时候初始化为MAX,(这样有利于后期min的时候),用INT_MAX可能会溢出
然后在最后的时候看一下,更新了没,如果没有,还是MAX,那么就返回-1.
这道题,还有就是不要想着哪一个货币,要用多少个,dp[i]只与dp[i - coin]有关,和dp[i - 2*coin]无关了!
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int res = -1;
// 定义二维dp,初始化为amount+1
int size = coins.size();
vector<vector<int>>dp(size, vector<int>(amount + 1, amount + 1));
// 把能写的先写上,如果amount = 0的时候,返回0
for (int i = 0; i < size; i++) dp[i][0] = 0;
for (int j = 1; j < amount + 1; j++){
if (j % coins[0] == 0)
{
dp[0][j] = j / coins[0];
}
}
for (int i = 1; i < size; i++) {
for (int j = 1; j < amount + 1; j++){
if (j - coins[i] >= 0)
dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1);
else//!!!!!!!!!!!!!!!!!1
dp[i][j] = dp[i - 1][j];
}
}
return dp[size - 1][amount] != amount + 1 ? dp[size - 1][amount] : -1;
}
};
空间压缩后,不用i-1返回上一行了,直接上就是上一次的结果
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 0-amount,初始化为amount+1
vector<int> dp(amount + 1, amount + 1);
// 如果指定的钱是0,最少使用0张钱得到
dp[0] = 0;
// 开始求剩余的amount
for (int i = 1; i <= amount; i++) {
for (int coin : coins)
if (coin <= i)//如果当前的货币小于要组成的货币amount
// dp[i]所有的初始化的都是amount+1
dp[i] = min(dp[i], dp[i - coin] + 1);// 用了这样货币,所以要+1
}
// 如果大于amount说明没有切换,还是初始化的值
return dp[amount] > amount ? -1 : dp[amount];
}
};