在上一篇博客算法整理——【动态规划练习(4)完全背包】-CSDN博客的基础上,继续补充完全背包相关问题。
一、完全背包中的排列和组合
在纯完全背包问题中,我们说先遍历物品后遍历背包和先遍历背包后遍历物品都是可以的,因为返回的是值的最大和。但如果要返回的是背包里的物品或其种类数,则涉及到排列还是组合的问题,如{1,2}和{2,1}算一个还是两个。
现在我们来看一个组合题518. 零钱兑换 II - 力扣(LeetCode)和一个排列题377. 组合总和 Ⅳ - 力扣(LeetCode)。
排列还是组合,代码中主要由循环的顺序决定。如果是外层遍历物品,内层遍历背包,则物品1永远会在2前面,不会存在{2,1},是组合。如果外层遍历背包,内层遍历物品,则背包的每一个值都会考虑1或2,会存在{2,1},是排列。
总结:如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for遍历背包,内层for循环遍历物品。
组合题代码如下:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> 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-coins[i]];
}
}
return dp[amount];
}
};
排列题代码如下:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
dp[0] = 1;
for(int i = 1; i<=target; i++)
{
for(int j = 0; j<nums.size(); j++)
{
if(i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) dp[i] += dp[i-nums[j]];
}
}
return dp[target];
}
};
二、单词拆分
题目为139. 单词拆分 - 力扣(LeetCode),给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
本题和回溯算法中分割回文串的题目相似,本题也可以用回溯方法枚举分割情况。普通的回溯可能会超时,因为每个位置都有两个状态,我们可以通过增加memory数组保存之前递归后的结果,这样可以优化。
现在我们用动态规划的多重背包来解决这个问题,把字符串看成背包,单词看成物品,单词可以被重复使用,所以是一个完全背包。使用动规五部曲分析:
①确定dp数组和下标的含义。dp[i]的意思是,字符串取前面长度为i的部分,是否可以完全拆分为单词。
②递推公式。如果dp[j]为1且[j,i]这个区间的子串在字典里,那么dp[i]就是1。
③初始化。dp[0]=1,其他初始化为0。
④遍历顺序。本题其实为排列问题,因为字符串和单词都是有序的。所以我们要先遍历背包再遍历物品。
⑤尝试推导dp数组并检查。
完整代码如下:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> Dic(wordDict.begin(),wordDict.end());
vector<bool> dp(s.size()+1,0);
dp[0] = 1;
for(int i = 1; i<=s.size(); i++)
{
for(int j = 0; j<i; j++)
{
string word = s.substr(j,i-j);//substr(起始位置,截取的个数)
if(Dic.find(word) != Dic.end() && dp[j])
{
dp[i] = 1;
}
}
}
return dp[s.size()];
}
};
说明:本文为作者整理知识点用于复习巩固,参考了代码随想录的讲解,有问题可以联系作者欢迎讨论~