322.零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
- 输入:coins = [1, 2, 5], amount = 11
- 输出:3
- 解释:11 = 5 + 5 + 1
示例 2:
- 输入:coins = [2], amount = 3
- 输出:-1
思路
dp[j]表示凑够金额所需的最小硬币个数。
dp[0]=0表示凑0元所需硬币数是0.其他则初始化为最大值,因为接下来是要找最小值
递推公式则分选择 当前硬币和不选择硬币两种情况。
dp[j]=min(dp[j],dp[j-coins[i]).
遍历顺序先物品在背包或背包物品都不影响,和排列组合无关。内层循环为保证完全背包里的物品可以无限次使用,则要递增遍历。
代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int>dp(amount+1,INT_MAX);
dp[0]=0;//凑成金额 0 所需的硬币数是 0
for(int i=0;i<coins.size();i++)
{
for(int j=coins[i];j<=amount;j++)
{
if(dp[j-coins[i]]!=INT_MAX)//如果 dp[j - coins[i]] == INT_MAX,表示无法用给定硬币凑出金额 j - coins[i],即目前无法到达该状态,所以不应更新 dp[j]。只有当 dp[j - coins[i]] != INT_MAX 时,才有可能通过再加上一个面值为 coins[i] 的硬币凑成金额 j。
dp[j]=min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount]==INT_MAX)return -1;//如果没找到能凑成的
return dp[amount];
}
};
举例dp(部分)
使用硬币
1
当
i = 0
(硬币面值为1
)时,内层循环更新dp
数组:
- 对于
j = 1
:dp[1] = min(dp[1], dp[1-1] + 1) = min(INT_MAX, 0 + 1) = 1
- 对于
j = 2
:dp[2] = min(dp[2], dp[2-1] + 1) = min(INT_MAX, 1 + 1) = 2
- 对于
j = 3
:dp[3] = min(dp[3], dp[3-1] + 1) = min(INT_MAX, 2 + 1) = 3
- 对于
j = 4
:dp[4] = min(dp[4], dp[4-1] + 1) = min(INT_MAX, 3 + 1) = 4
- 对于
j = 5
:dp[5] = min(dp[5], dp[5-1] + 1) = min(INT_MAX, 4 + 1) = 5
- 对于
j = 6
:dp[6] = min(dp[6], dp[6-1] + 1) = min(INT_MAX, 5 + 1) = 6
- 对于
j = 7
:dp[7] = min(dp[7], dp[7-1] + 1) = min(INT_MAX, 6 + 1) = 7
- 对于
j = 8
:dp[8] = min(dp[8], dp[8-1] + 1) = min(INT_MAX, 7 + 1) = 8
- 对于
j = 9
:dp[9] = min(dp[9], dp[9-1] + 1) = min(INT_MAX, 8 + 1) = 9
- 对于
j = 10
:dp[10] = min(dp[10], dp[10-1] + 1) = min(INT_MAX, 9 + 1) = 10
- 对于
j = 11
:dp[11] = min(dp[11], dp[11-1] + 1) = min(INT_MAX, 10 + 1) = 11
更新后的
dp
数组是:
dp = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
279.完全平方数
给你一个整数
n
,返回 和为n
的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,
1
、4
、9
和16
都是完全平方数,而3
和11
不是。示例 1:
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13 输出:2 解释:13 = 4 + 9
思路,和上题差不多。dp表示达到背包容量j所需要的完全平方数个数。物品就是i*i。dp【0】初始化为0.dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。
此时我们要选择最小的dp[j],所以递推公式:**dp[j] = min(dp[j - i * i] + 1, dp[j]);**遍历顺序是背包物品或者物品背包都可以。
举例
版本一
背包物品
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,INT_MAX);
dp[0]=0;
for(int i=0;i<=n;i++)//背包
{
for(int j=1;j*j<=i;j++)//物品
{
dp[i]=min(dp[i-j*j]+1,dp[i]);//dp是由前面的数推导过来的
}
}
return dp[n];
}
};
版本二
物品背包
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,INT_MAX);
dp[0]=0;
for(int i=1;i*i<=n;i++)//物品
{
for(int j=i*i;j<=n;j++)//背包
{
dp[j]=min(dp[j-i*i]+1,dp[j]);//dp[j]表示为了达到容量为j的背包,所需要的完全平方数的最小个数
}
}
return dp[n];
}
};
是的,很简单上面这两题。模板都差不多。
139.单词拆分
思路
单词是物品,字符串是背包,字符串能不能有一个或多个给出的单词组成就是问物品能不能装满背包,而且物品可以重复使用,这就一个完全背包问题。
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
初始值,dp[0]=true,因为长度0的字符串是可以被0个单词组成的,且如果是false,往下推导就全是false了。其他值则初始化为false。
递推公式:
如果确定dp[j] 是true ,且**[j, i]** 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if**([j, i] 这个区间的子串出现** 在字典(物品)里 &&dp[j]是true) 那么 dp[i] = true。(进一步解释,这里感觉意思是,字符串前段的单词出现在字典里,后段直到当前背包容量末尾的单词也都在字典里,那么这个字符串一定都出现在字典里,就是背包能被物品装满。)
遍历顺序:如果求排列数就是外层for遍历背包,内层for循环遍历物品。
而本题字符串里单词的出现是由顺序的,所以我们求得是排列数。
举例dp
代码
本题还有个旧知识新用,就是把词典数组做个set哈希表,方便查找某单词是否出现在其中。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<int>dp(s.size()+1,0);
dp[0]=1;
unordered_set<string>wordset(wordDict.begin(),wordDict.end());//使用 unordered_set 来存储字典中的单词,方便快速查找。
for(int i=1;i<=s.size();i++)//背包
{
for(int j=0;j<i;j++)//物品,内层循环 j 从 0 到 i-1,用于检查从位置 j 到 i-1 的子字符串。
{
string word=s.substr(j,i-j);//!!一个个把(j,i-j)的字符串加入到word中,直到确认存在词典中
if(wordset.find(word)!=wordset.end()&&dp[j]==true)//如果该子字符串存在于字典中(通过 wordset.find(word) != wordset.end()),并且 dp[j] 为 true(表示前 j 个字符可以被拆分),则我们可以将 dp[i] 设置为 true。
dp[i]=true;
}
}
return dp[s.size()];
}
};
打印dp数组
假设我们有以下输入:
- 字符串 :
s = "leetcode"
- 字典 :
wordDict = ["leet", "code"]
初始化
dp
数组初始的
dp
数组为:dp = [1, 0, 0, 0, 0, 0, 0, 0, 0] // dp[0] = 1
填充
dp
数组逐步遍历并更新
dp
数组的过程如下:
当
i = 1
:
j = 0
:word = "l"
,不在字典中,继续。dp[1] = 0
。当
i = 2
:
j = 0
:word = "le"
,不在字典中。j = 1
:word = "e"
,不在字典中。dp[2] = 0
。当
i = 3
:
j = 0
:word = "lee"
,不在字典中。j = 1
:word = "e"
,不在字典中。j = 2
:word = "e"
,不在字典中。dp[3] = 0
。当
i = 4
:
j = 0
:word = "leet"
,在字典中,且dp[0] = 1
。- 更新
dp[4] = 1
。当
i = 5
:
j = 0
:word = "leetc"
,不在字典中。j = 1
:word = "eec"
,不在字典中。j = 2
:word = "ec"
,不在字典中。j = 3
:word = "c"
,不在字典中。dp[5] = 0
。当
i = 6
:
j = 0
:word = "leetco"
,不在字典中。j = 1
:word = "etco"
,不在字典中。j = 2
:word = "et"
,不在字典中。j = 3
:word = "c"
,不在字典中。j = 4
:word = "co"
,不在字典中。dp[6] = 0
。当
i = 7
:
j = 0
:word = "leetcod"
,不在字典中。j = 1
:word = "etcod"
,不在字典中。j = 2
:word = "etc"
,不在字典中。j = 3
:word = "c"
,不在字典中。j = 4
:word = "co"
,不在字典中。j = 5
:word = "o"
,不在字典中。dp[7] = 0
。当
i = 8
:
j = 0
:word = "leetcode"
,不在字典中。j = 1
:word = "eetcode"
,不在字典中。j = 2
:word = "etc"
,不在字典中。j = 3
:word = "c"
,不在字典中。j = 4
:word = "co"
,不在字典中。j = 5
:word = "o"
,不在字典中。j = 6
:word = "e"
,不在字典中。dp[8] = 0
。最终
dp
数组为:dp = [1, 0, 0, 0, 1, 0, 0, 0, 1]
小小总结,完全背包的排序问题所求涉及三方面:组合数,排列数,最小数。
背包总结篇如图
多重背包理论基础
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
多重背包和01背包是非常像的, 为什么和01背包像呢?
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
例如:
背包最大重量为10。
物品为:
重量 价值 数量 物品0 1 15 2 物品1 3 20 3 物品2 4 30 2 问背包能背的物品最大价值是多少?
和如下情况有区别么?
重量 价值 数量 物品0 1 15 1 物品0 1 15 1 物品1 3 20 1 物品1 3 20 1 物品1 3 20 1 物品2 4 30 1 物品2 4 30 1 毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。