代码随想录算法训练营Day46 | 139.单词拆分 | 多重背包 | 背包问题总结

139.单词拆分

题目链接 | 理论基础

乍一看是回溯问题,和分割回文子串很像,不过本题使用回溯解决会超时(有些极端 case 过不了),而且这样只需要求解 True/False 的问题一般不会考虑回溯,毕竟回溯是暴力的指数搜索。

wordDict 看作可以无限取用的物品,s 看作是背包,问能否用物品填满背包 – 完全背包,启动!
注意到本题的结果是必须依赖于排列的,只靠组合不能确定结果,因为填满背包的方式是有顺序要求的,同样是 wordDict = ["apple", "pen"]["apple", "pen", "apple"] 就可以得到 "applepenapple",而 ["apple", "apple", "pen"] 不可以。用爬楼梯的思路来解决的话会流畅一些。

  1. dp 数组的下标含义:dp[j] 代表是否能够填满背包 s[:j]

  2. dp 递推公式:dp[j] = dp[j] or dp[j - len(wordDict[i])]

    • 不需要当前的单词 wordDict[i],就已经能组合成 s[:j]
    • 需要当前的单词 wordDict[i],并且之前已经能组成 s[:j-len(wordDict[i])]
      • “需要当前单词”代表着 s[j - len(wordDict[i]): j] == wordDict[i] 成立
    • 以上情况只要有一种成立,就能够得到 dp[j]=True
  3. dp 数组的初始化:根据递推公式可以得到 dp[0]=True,否则后面的递推无法进行

    • 从逻辑上来说,dp[0] 的取值没有明确定义,但是 dp[0]=True 能够使得 wordDict = ["pen"], s = "pen" 的情况推导出正确的 dp 数组
  4. dp 的遍历顺序:如上所述,这道题的结果依赖于排列而非组合,很明显必须是先背包再物品(爬楼梯 yyds)

  5. 举例推导:wordDict = ["leet", "code"], s = "leetcode"

    012345678
    dpTFFFTFFFT
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        # dp[j] represents whether can make s[:j] 
        dp = [False] * (len(s) + 1)
        dp[0] = True

        # dp formula, bag->item
        for j in range(1, len(s) + 1):
            for i in range(len(wordDict)):
                if j >= len(wordDict[i]) and s[j - len(wordDict[i]): j] == wordDict[i]:
                    dp[j] = dp[j] or dp[j - len(wordDict[i])]
        
        return dp[-1]

多重背包

理论基础

给定一个限重为 V V V 的背包,有 N N N 种物品,其中第 i i i 件物品有 M i M_i Mi 个可用,每一件消耗空间 C i C_i Ci,价值是 W i W_i Wi。求怎样装入物品,使得物品总重量不会超过背包限重,同时获得最大价值。

最简单的等价转换就是将多重背包转化为 01 背包问题。将每种物品每一件都摊开来,就是 01 背包,毕竟 01 背包也没有规定物品的重量、价值不能相同。
如下,第一张表是完全背包,第二张表是 01 背包:

物品价值物品重量数量
物品 01151
物品 13203
物品 24302
物品价值物品重量
物品 0115
物品 1320
物品 1320
物品 1320
物品 2430
物品 2430

所以,只要将多重背包正确地转换成 01 背包的输入,就可以用 01 背包的方法来解决。转换代码如下:

weight = [1, 3, 4]
value = [15, 20, 30]
nums = [2, 3, 2]
bagWeight = 10

# 将数量大于1的物品展开
for i in range(len(nums)):
    while nums[i] > 1:
        weight.append(weight[i])
        value.append(value[i])
        nums[i] -= 1

另一种思路就是在物品+背包的遍历内部再加上一层使用数量的遍历。之前的 01 背包,无论是二维数组还是滚动数组,无论遍历顺序,都需要考虑“当前背包容量为 j,是否要取物品 i”;现在的滚动背包内,需要考虑“当前背包容量为 j,要取多少件物品 i”。
本质上还是很像爬楼梯。

def test_multi_pack(weight, value, nums, bagWeight):
    dp = [0] * (bagWeight + 1)

    for i in range(len(weight)):  # 遍历物品
        for j in range(bagWeight, weight[i] - 1, -1):  # 遍历背包容量
            # 以上为01背包,然后加一个遍历个数
            for k in range(1, nums[i] + 1):  # 遍历个数
                if j - k * weight[i] >= 0:
                    dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i])

        # 使用 join 函数打印 dp 数组
        print(' '.join(str(dp[j]) for j in range(bagWeight + 1)))

    print(dp[bagWeight])


if __name__ == "__main__":
    weight = [1, 3, 4]
    value = [15, 20, 30]
    nums = [2, 3, 2]
    bagWeight = 10
    test_multi_pack(weight, value, nums, bagWeight)

背包问题总结

理论基础

在这里插入图片描述
通过之前这一堆背包问题的练习,解决背包问题已经比较有套路了,对遍历顺序、初始化的理解也算不错。然而,将复杂的问题背景抽象成背包问题,仍然是需要经过思考的,其中最后一块石头的重量 II、目标和 绝对是这种复杂抽象的难题。
另外,很多背包问题看上去也都能用回溯算法解决,但毫无疑问都一定会超时。区别在于,背包问题依然能够依靠子问题的解来节省复杂度,而回溯算法不可避免地需要进行穷举,只不过是优雅的穷举,两者还是有本质上的区别。

代码随想录上总结了不同的递推公式。但这些背包问题刷下来,个人感觉找对了 dp 数组的含义后,dp 递推公式就能很自然地得到,无需特别考虑。相比之下,初始化和遍历顺序才是更大的坑。

初始化

背包问题的初始化堪称是五花八门,每一道题都需要谨慎思考。找到正确的初始化,最好的方法是对于自己的想法,找个简单的例子推一下,看看能否得到想要的答案。

  1. 有些简单的 dp 题,dp 数组的部分初始化根本不重要,例如爬楼梯中的 dp[0]。这种情况下,最重要的是确保自己的初始化能够正确地得到后续的结果即可。
  2. 二维数组解背包问题,最重要的是对于 i=0 的初始化,因为后续操作 dp[i][j] 的时候要用到 dp[i-1][...] 的值。二维数组的初始化一般比较直观,能够根据含义直接得到 i=0 和 j=0 时的值,通常来说也能帮助顺利得到正确的递推结果。
    • 目标和 堪称是二维数组初始化的难度巅峰。
  3. 滚动数组解背包问题,相对来说初始化更容易一些,因为这个一维数组中一般只需要初始化 dp[0],要思考的东西大大减少了。相对应的,滚动数组的初始化更为抽象,因为这里的初始化是没有发生更新之前,需要结合递推公式来得到合理的初始化值。
  4. 对于不特殊的 dp 数组位置,初始化同样要小心。无脑初始化为 0 或 -1 绝对不是明智的选择。

遍历顺序

  • 01 背包
    • 二维数组:先物品后背包、先背包后物品都是可以的,内部循环从小到大、从大到小进行都可以
    • 滚动数组:对于背包的遍历必须是从大到小反向遍历(否则可能重复选取同一物品),从而导致必须是先物品后背包(否则每个背包容量都只能选取一件物品)
  • 完全背包
    • 二维数组仍然不在乎遍历顺序
      • 限制于二维数组总是根据之前的状态进行递推,二维数组似乎并不能解决
    • 滚动数组:纯粹的完全背包只在乎最终容量能够得到的价值,不在乎得到的方式(顺序),所以先物品后背包、先背包后物品都是可以的,背包的遍历必须是从小到大(才能捕捉到重复使用
      • 如果完全背包问题要求组合结果,那就必须是先物品后背包(相当于固定了物品的出现顺序)
      • 如果完全背包问题要求排列结果,那就必须是先背包后物品(相当于允许了任意的物品顺序)
        • dp 解决排列问题,最好的抽象方法是爬楼梯!
      • 如果完全背包问题不在意结果的顺序,那么物品、背包的先后顺序就无所谓了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值