目录
[322. 零钱兑换](#322. 零钱兑换)
322. 零钱兑换
动规五部曲
1.dp[j]的含义是,凑齐j所用最少硬币数
2.递推公式:
如果取当前硬币i ,就需要dp[j-coins[i]]个硬币数再+1;
如果不取当前硬币i (前i-1个硬币就已经凑齐j) dp[j]个硬币,二者取最小值
故最终递推公式为dp[j]=min(dp[j-coins[i]]+1, dp[j])
3.初始化方式:凑齐0元需要0个硬币,所以dp[0]=0; 其余初始化为最大值,否则会无法被递推公式覆盖。
4.遍历顺序 :可以重复取硬币-正序遍历背包 ;求的是硬币最小数 ,所以先物品(硬币)后背包或者先背包后物品都无所谓;
cpp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//dp数组:dp[j]的含义是,凑齐j所用最少硬币数
vector<int> dp(amount+1, INT_MAX);
//初始化:凑齐0元需要0个硬币,并且其余可初始化为最大值,否则会无法被递推公式覆盖。
dp[0] =0;
//递推公式:如果取当前硬币i,就需要dp[j-coins[i]]个硬币数再+1;
//如果不取当前硬币i(前i-1个硬币就已经凑齐j) dp[j]个硬币,二者取最小值
//故最终递推公式为dp[j]=min(dp[j-coins[i]]+1, dp[j])
//遍历顺序:可以重复取硬币-正序遍历背包;硬币数和顺序无关,所以是组合数,所以先硬币后背包;
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]=min(dp[j-coins[i]]+1, dp[j]);
}
}
if(dp[amount]==INT_MAX) return -1;
return dp[amount];
}
};
279.完全平方数
动规五部曲
1.dp[j]的含义是,dp[j]的含义是,凑齐n所用最少完全平方数
2.递推公式:
如果取当前i的平方, 就需要dp[j-i*i]个完全平方数再+1;
如果不取当前i的平方 (前i-1个完全平方数就已经凑齐j),则就是不更新的dp[j],二者取最小值
故最终递推公式为dp[j]=min(dp[j-coins[i]]+1, dp[j])
3.初始化方式:凑齐0需要0个完全平方数,并且其余可初始化为最大值,否则会无法被递推公式覆盖。
4.遍历顺序 :可以重复取硬币-正序遍历背包;可以重复取完全平方数-正序遍历背包;求最小数,所以先物品后背包或者先背包后物品都可以;
为什么相比上一题不需要if判断
**先背包后物品:**遍历容量1时,只能塞下1,所以dp[1]一定是1,有了这个基础,后面再遍历的时候不需要考虑前面还没有有效值覆盖的情况从而出现INT_MAX;
**先物品后背包:**遍历平方数1的时候,会把所有容量用1填满,同样每个值都已经变成了有效值,不是INT_MAX了,所以可以。
注:本题一定有结果,因为1可以填任意值。
cpp
class Solution {
public:
int numSquares(int n) {
//dp数组:dp[j]的含义是,凑齐n所用最少完全平方数
vector<int> dp(n+1, INT_MAX);
//初始化:凑齐0需要0个完全平方数,并且其余可初始化为最大值,否则会无法被递推公式覆盖。
dp[0] =0;
//递推公式:如果取当前i的平方,就需要dp[j-i*i]个完全平方数再+1;
//如果不取当前i的平方(前i-1个完全平方数就已经凑齐j),则就是不更新的dp[j],二者取最小值
//故最终递推公式为dp[j]=min(dp[j-coins[i]]+1, dp[j])
//遍历顺序:可以重复取完全平方数-正序遍历背包;求最小数,所以先物品后背包或者先背包后物品都可以;
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]);
}
}
return dp[n];
}
};
139.单词拆分
动规五部曲
1.dp[j]的含义是,dp[j]的含义是,长度为j的字符串,是否能被字典中字符串填满
2.递推公式:
如果s[j:i]这个字符子串在字典内,并且dp[j]为true,则dp[i]为true
3.初始化方式:dp[0]必须为true,否则后面全为false了;其余初始值为false,否则最后直接输出true了;
4.遍历顺序:
可以重复取字符串-正序遍历背包;
物品的顺序会影响dp值,所以是排列数,需要先背包后物品
**注:**物品是字典中的字符串,但需要先匹配上,也就是用j遍历到当前容量(字符串索引),截取i-j长度的子串,如果和字典中的某元素能对上,则为有效物品,可以进行下一步判断(判断dp[j]是否为true)
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size()+1, false);
dp[0] = true;
for(int i=1;i<=s.size();i++){
for(int j=0;j<i;j++){
string str = s.substr(j,i-j);
if(wordSet.find(str)!=wordSet.end() && dp[j]) dp[i]=true;
}
}
return dp[s.size()];
}
};
多重背包
问题解释
有N种物品和一个容量为V 的背包。第i种物品最多有M[i]件可用,每件耗费的空间是C[i] ,价值是W[i] 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
只要把每种物品的M[i]件展开,就可以转化为0-1背包问题;
代码的改动
在0-1背包的先物品后背包(倒序)的基础上,再加一层循环,意思是,对于第i种物品,求放1次、放2次、...、放M[i]次到底放多少次价值最大
cpp
for(int i = 0; i < n; i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
// 以上为01背包,然后加一个遍历个数
//遍历放入个数时,需满足背包容量大于等于k个物品i的重量
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
}
总结
三种背包问题
**0-1背包:**先物品后背包+背包倒序
**完全背包:**先物品后背包或先背包后物品或两种都可以+背包正序
**多重背包(0-1背包变种):**同0-1背包
两种递推公式
最值问题(尽量装满背包情况下):
cpp
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
//dp[j] = min(dp[j], dp[j - weight[i]] + value[i]);
最多方案数问题:
cpp
dp[j] += dp[j - weight[i]];
其实不止两种,非典型的还有单词拆分 这种根据字符子串是否匹配物品 和加上新子串前是否为true来推出当前状态的递推方式
组合数与排列数
**组合数:**先物品后背包-先物品则路径固定;
**排列数:**先背包后物品-后物品可遍历选择最后一个位置放哪个物品;