• 代码随想录 | Day 46 - LeetCode 139. 单词拆分、多重背包


            今天是完全背包的又一个应用和多重背包的基础,到这里背包问题就结束了。第1题复习了回溯法,新接触到记忆化递归,更重要地,还学习了bool形式dp数组的多重背包问题,其中最重要的是遍历顺序,还需要多熟悉。第2题学习了多重背包的模板方法,也就是将多重背包转换为01背包,比较容易。


    第1题(LeetCode 139. 单词拆分

            比较难,自己没想出来,直接看了题解。有回溯和DP两种解法,前者时间复杂度O(2^n),后者O(n^3)。明显DP解法更优。

            用回溯方法的话,需要设置indBegin作为递归参数,在递归函数中将其作为当前待尝试单词的开始。在循环中从indBegin开始遍历当前待尝试单词结尾下标,根据两下标得到当前待尝试单词。然后从wordDict中查找该单词(为了提高查找效率,需要将wordDict转换为集合来查找),如果找到则进行下一层递归,直至indBegin达到目标字符串s的末尾就返回true;如果没找到,则进行下一个尝试。按这样的方法,直接写出下面的代码虽然正确,但会超时:

    1. class Solution {
    2. public:
    3. bool back(string& s, unordered_set& wordSet, int indBegin) {
    4. if (indBegin == s.size()) {
    5. return true;
    6. }
    7. for (int i = indBegin; i < s.size(); i++) {
    8. string word = s.substr(indBegin, i - indBegin + 1);
    9. if (wordSet.find(word) != wordSet.end() && back(s, wordSet, i + 1)) {
    10. return true;
    11. }
    12. }
    13. return false;
    14. }
    15. bool wordBreak(string s, vector& wordDict) {
    16. unordered_set wordSet(wordDict.begin(), wordDict.end());
    17. return back(s, wordSet, 0);
    18. }
    19. };

            优化的方法就是进行剪枝,具体的方法叫做记忆化递归。在原本的回溯过程中,已经得到了“从某个下标开始尝试”不可行的结果,但仍会进行重复尝试。所以记忆化递归的做法是设置一个长度与目标字符串s相等的bool数组memory,用于记录从某一下标出发开始尝试是否可行。在一开始初始化memory所有值为true。如果在某一下标处进行了所有循环但仍未返回true,就说明从该下标出发不可行,就将该下标的memory设置为false。在之后的递归中循环开始前,如果当前起始下标对应的memory值为false,那么当前递归就也直接返回false,从而避免了无效的尝试。

    1. class Solution {
    2. public:
    3. vector<bool> memory;
    4. bool back(string& s, unordered_set& wordSet, int indBegin) {
    5. if (indBegin == s.size()) {
    6. return true;
    7. }
    8. if (memory[indBegin] == false) {
    9. return false;
    10. }
    11. for (int i = indBegin; i < s.size(); i++) {
    12. string word = s.substr(indBegin, i - indBegin + 1);
    13. if (wordSet.find(word) != wordSet.end() && back(s, wordSet, i + 1)) {
    14. return true;
    15. }
    16. }
    17. memory[indBegin] = false;
    18. return false;
    19. }
    20. bool wordBreak(string s, vector& wordDict) {
    21. unordered_set wordSet(wordDict.begin(), wordDict.end());
    22. memory.resize(s.size(), true);
    23. return back(s, wordSet, 0);
    24. }
    25. };

    代码中需要注意,递归的循环中,i一定是从indBegin开始的。自己起初是设置为从indBegin + 1开始的,那么下一层递归的indBegin参数就不再应该是(i + 1)了,而应该是i,否则会错误地略过当前层的第(indBegin + 1)个元素。并且递归函数开头if (indBegin == s.size())中的"=="也要改完">="。所以还是将i设置为从indBegin开始好很多,会省很多麻烦。

            背包解法方面,需要将dp[i]定义为bool类型,定义为目标字符串s的前i部分能否由单词表中单词组成。如果dp[j](j < i)为true,且目标字符串s的[j, i]部分能在单词表wordDict中找到的话,dp[i]就也设置为true。所以该问题的背包容量递增的过程目标字符串s从0向后扩展的过程,当前的目标字符串,也就是当前背包容量,对应的物品是“当前目标字符串的所有后缀”,而所有物品就是目标字符串s的所有子串。

            初始化方面,dp[0]应设置为true,代表空字符串是可以构成的,否则dp的其他值都不会是true。而遍历顺序上,因为是完全背包问题,所以内外层循环方向都是正向。而内外层顺序虽然理论上可以交换,但如果按照习惯使用外层循环遍历物品,内层循环遍历背包容量的方式,那么外层循环的物品就是前面提到的目标字符串s的所有子串。这就需要提前用一个容器保存目标字符串s的所有子串,在实现上会比较麻烦。而如果使用外层循环遍历背包容量,内层循环遍历物品的方式,则会在内层循环中遍历“当前目标字符串的所有后缀”,实现上容易很多。

    1. class Solution {
    2. public:
    3. bool wordBreak(string s, vector& wordDict) {
    4. unordered_set wordSet(wordDict.begin(), wordDict.end());
    5. int n = s.size();
    6. vector<bool> dp(n + 1, false);
    7. dp[0] = true;
    8. for (int end = 1; end <= n; ++end) {
    9. for (int begin = 0; begin < end; ++begin) {
    10. string sub = s.substr(begin, end - begin);
    11. int idx = begin - 1 + 1;
    12. if (dp[idx] && wordSet.find(sub) != wordSet.end()) {
    13. dp[end] = true;
    14. break;
    15. }
    16. }
    17. }
    18. return dp[n];
    19. }
    20. };

            这里时间复杂度O(n^3)是因为substr()的时间复杂度是O(n)。

            二刷:忘记方法。另外,内部循环中,if()中dp的下标idx,因为dp[i]对应的是s[i - 1],所以s[last]对应dp[last + 1],last即begin - 1,所以idx = last + 1 = begin - 1 + 1 = begin。


    第2题(多重背包问题)

            在LeetCode上也没有标准模板题,所以自行写数据测试。多重背包问题是指物品中的每个物品是有限个的。该问题有多种解法,其中比较容易理解和实现的是转化为01背包问题,也就是把每个数量为n的物品拆分为n个数量为1的物品,然后用01背包的解法解决。最直接的实现方法是将原来的物品数组拆分为更长的新数组,包括weight和value。

    1. #include
    2. #include
    3. using namespace std;
    4. int main() {
    5. vector<int> nums = {2, 3, 2};
    6. vector<int> weight = {1, 3, 4};
    7. vector<int> value = {15, 20, 30};
    8. int space = 10;
    9. for (int i = 0; i < nums.size(); ++i) {
    10. while (nums[i] > 1) {
    11. weight.push_back(weight[i]);
    12. value.push_back(value[i]);
    13. nums[i]--;
    14. }
    15. }
    16. vector<int> dp(space + 1, 0);
    17. for (int i = 0; i < weight.size(); ++i) {
    18. for (int j = space; j >= weight[i]; --j) {
    19. dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    20. }
    21. for (int j = 0; j <= space; ++j) {
    22. cout << dp[j] << " ";
    23. }
    24. cout << endl;
    25. }
    26. cout << dp[space] << endl;
    27. return 0;
    28. }

    需要注意第12行的循环条件处,当物品数量为1时就应该停止循环,而不是0时,否则会错误地多加1件物品。
            实际上,不需要用额外空间来拆分原来的物品数组也可以实现。做法是在更新dp矩阵时再多嵌套一层循环,循环次数就是当前物品数。

    1. #include
    2. #include
    3. using namespace std;
    4. int main() {
    5. vector<int> nums = {2, 3, 2};
    6. vector<int> weight = {1, 3, 4};
    7. vector<int> value = {15, 20, 30};
    8. int space = 10;
    9. vector<int> dp(space + 1, 0);
    10. // 物品和背包容量之间增加一层循环
    11. for (int i = 0; i < weight.size(); ++i) {
    12. for (int k = 0; k < nums[i]; ++k) {
    13. for (int j = space; j >= weight[i]; --j) {
    14. dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    15. }
    16. for (int j = 0; j <= space; ++j) {
    17. cout << dp[j] << " ";
    18. }
    19. cout << endl;
    20. }
    21. }
    22. cout << dp[space] << endl;
    23. return 0;
    24. }

            这层循环也可嵌套在最内层,此时最内层循环就需要将该物品数量从1遍历到最大数量。循环变量为k的话,放入当前物品的选项需要从dp[j - weight[i]] + value[i]改为dp[j - k * weight[i]] + k * value[i]),代表分别放入k件当前物品。所以可以将第12~23行代码替换为:

    1. // 背包容量内增加一层循环
    2. for (int i = 0; i < weight.size(); ++i) {
    3. for (int j = space; j >= weight[i]; --j) {
    4. for (int k = 1; k <= nums[i] && j >= k * weight[i]; ++k) {
    5. dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
    6. }
    7. }
    8. for (int j = 0; j <= space; ++j) {
    9. cout << dp[j] << " ";
    10. }
    11. cout << endl;
    12. }
  • 相关阅读:
    VSCode远程开发 Windows11 Linux
    MATLAB与Excel的数据交互
    Armbian OS(基于ubuntu24) 源码编译mysql 5.7
    Allegro导入导出设计数据操作指导
    P39 事件处理
    【html5期末大作业】基于HTML+CSS+JavaScript管理系统页面模板
    微信小程序实验案例:简易成语小词典
    SpringBoot整合JWT、实现登录和拦截
    理解 Vue3 里的 defineProps 和 defineEmits
    Hive行转列[一行拆分成多行/一列拆分成多列]
  • 原文地址:https://blog.csdn.net/weixin_43469527/article/details/127709758