代码随想录算法训练营第三十八天 |动态规划part06(背包问题完)

128 阅读9分钟

代码随想录算法训练营第三十八天 |动态规划part06

前文总结

  • 价值or方法:

    • 如果要是求最大价值,那就是用dp[j] =max(dp[j], dp[j-weight[i]] + value[i])
    • 如果要是求有多少种方法,那就是用dp[j] += dp[j-i]
  • 0-1 or 完全

    • 如果是0-1背包,对于j就是倒序遍历
    • 如果是完全背包,对于j就是正序遍历
  • 组合 or 排列

    • 如果是组合,12和21是一种情况,这个时候需要先遍历i,再遍历j
    • 如果是排列,12和21是两种情况,这个时候需要先遍历j,再遍历i
    • i 物品 j 背包

322 零钱兑换

image-20250116164202337.png

如果要是凑成总金额所需的最多的硬币个数的话,那么就很简单了:

coins = [1,2,5]
amount = 11
dp = [0] * (amount + 1)
# dp[i] 凑成i金额的最多的硬币个数
dp[0] = 0
for coin in coins:
    for j in range(coin,amount+1):
        dp[j] = max(dp[j], dp[j-coin]+1)
print(dp[amount])

但是题目问的是最少的硬币个数,那就需要把max换成min了,而且最重要的是初始化的时候都需要初始化成最大值

不然的话,和0比较肯定一直0是最小的。

# coins = [1,2,5]
# amount = 11
dp = [float('inf')] * (amount + 1)
dp[0] = 0
# dp[i] 凑成i金额的最少的硬币个数
for coin in coins:
    for j in range(coin,amount+1):
        dp[j] = min(dp[j], dp[j-coin]+1)
if dp[amount] != float('inf'):
    return dp[amount]
else :
    return -1

279 完全平方数

image-20250116164211863.png

image-20250116170751931.png

所以大致框架就出来了。

    dp = [0] * (n+1)
    for i in range(物品数量):
        for j in range(weight[i],n+1):
            dp[j] = max(dp[j],dp[j-weight[i]]+value[i])

题目说要求最少数量,所以需要换成min,而且初始化需要用float('inf')

需注意,每次放进来的数应该都是i*i,因为要放的是完全平方数。

    # dp[i] 装满容量为i的背包所用的最少数量的物品
    dp = [float('inf')] * (n+1)
    dp[0] = 0
    for i in range(1,int(n**0.5)+1):
        for j in range(i*i,n+1):
            dp[j] = min(dp[j],dp[j-i*i]+1)
    return dp[-1]

139 单词拆分

image-20250116164221019.png

思路:这个题的思路真难想,擦。dp[j]应该表示什么呢?我知道s肯定是背包,wordDict肯定是物品。

目的是看 物品能否把背包装满,那dp[j] 应该就代表着装了j个物品时,对应的字符串?

结果是return dp[-1] == s? 难道是这样的吗?

如果是上述情况,代码应该是:

    dp = [''] * (len(wordDict) + 1)
    for word in wordDict:
        for j in range(len(s)):
            if s[j:]

很明显上面的思路出现问题了。dp应该是表示字符串的前 i 个字符是否可以被拆分成单词,比如说

下方代码中,leetcode前4个字符可以被拆分,所以dp[4] = True

5-8可以被拆分,所以dp[8] = True,你以为这就完了吗(二刷依旧踩坑)

需要中间不断,所以需要第二个单词的起点是第一个单词终点的下一个。

也就是,找到第一个单词结束的地方,我们需要将这个地方的下一个设为True,然后第二个单词从这个True的地方开始进行遍历。

    s = "leetcode"
    wordDict = ["leet", "code"]
    n = len(s)
    dp = [False] * (n + 1)  # dp[i] 表示字符串的前 i 个字符是否可以被拆分成单词
    dp[0] = True  # 初始状态,空字符串可以被拆分成单词for i in range(1, n + 1): # 遍历背包
        for j in range(i): # 遍历单词
            if dp[j] and s[j:i] in wordDict:
                dp[i] = True  # 如果 s[0:j] 可以被拆分成单词,并且 s[j:i] 在单词集合中存在,则 s[0:i] 可以被拆分成单词
                break
    print(dp[-1])

上面的这个代码可能和以前的不一样,那就看下面的代码:

    dp = [False] * (len(s) + 1)
    dp[0] = True
    for i in range(len(s)+1):
        for j in range(i,len(s)+1):
            if dp[i] and s[i:j] in wordDict:
                dp[j] = True
    return dp[-1]

多重背包理论基础

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

多重背包和01背包是非常像的, 为什么和01背包像呢?

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

例如:

背包最大重量为10。

物品为:

重量价值数量
物品01152
物品13203
物品24302

问背包能背的物品最大价值是多少?

和如下情况有区别么?

重量价值数量
物品01151
物品01151
物品13201
物品13201
物品13201
物品24301
物品24301

毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。

56 携带矿石资源(第八期模拟笔试)

image-20250117212946309.png

思路:首先拆开,然后换成0-1背包问题。0-1背包问题中的j就需要倒序了。

擦,又被超时折磨了..

    def main():
        import sys
        input = sys.stdin.read
        data = input().split()
        index = 0
        c = int(data[index])
        index += 1
        n = int(data[index])
        index += 1
        weights = []
        for i in range(n):
            weights.append(int(data[index]))
            index += 1
        values = []
        for i in range(n):
            values.append(int(data[index]))
            index += 1
        nums = []
        for i in range(n):
            nums.append(int(data[index]))
            index += 1
        for i in range(len(nums)):
            for j in range(nums[i]-1):
                weights.append(weights[i])
                values.append(values[i])
        dp = [0] * (c+1)
        for i in range(len(weights)):
            for j in range(c,weights[i]-1,-1):
                if dp[j] < dp[j-weights[i]] + values[i]:
                    dp[j] = dp[j-weights[i]] + values[i]
        print(dp[-1])
    if __name__ == '__main__':
        main()

这次的代码把之前的技巧用上了,不过还是不行,过不去啊...

以下代码可以通过,但是现在看不懂。。。

方法思路

  1. 问题分析

    • 我们有一个容量为 C 的背包,和 N 种不同类型的矿石。
    • 每种矿石有重量 w[i],价值 v[i],以及最多 k[i] 个可用。
    • 目标是在不超过背包容量的情况下,最大化所选矿石的总价值。
  2. 二进制优化

    • 将每种矿石的数量 k[i] 拆分成二进制形式,例如 k[i] = 5 拆分成 1, 4
    • 这样,我们可以将每组矿石视为独立的物品,每个组的数量是 2^m,直到剩余数量小于 2^m
  3. 动态规划求解

    • 使用一维 DP 数组,其中 dp[j] 表示容量为 j 时的最大价值。
    • 对于每个拆分后的矿石组,更新 DP 数组。
    def main():
        import sys
        input = sys.stdin.read  # 从标准输入读取所有数据
        data = input().split()  # 将输入数据按空格分割成列表
        index = 0
    ​
        # 读取背包容量 C 和矿石种类数 N
        C = int(data[index])
        index += 1
        N = int(data[index])
        index += 1
    ​
        # 读取每种矿石的重量
        weights = []
        for _ in range(N):
            weights.append(int(data[index]))
            index += 1
    ​
        # 读取每种矿石的价值
        values = []
        for _ in range(N):
            values.append(int(data[index]))
            index += 1
    ​
        # 读取每种矿石的数量上限
        nums = []
        for _ in range(N):
            nums.append(int(data[index]))
            index += 1
    ​
        # 二进制拆分优化
        new_weights = []  # 存储拆分后的矿石重量
        new_values = []   # 存储拆分后的矿石价值
        for i in range(N):
            w = weights[i]  # 当前矿石的重量
            v = values[i]   # 当前矿石的价值
            k = nums[i]     # 当前矿石的数量上限
            m = 1           # 二进制拆分的基数
            while k > 0:    # 当矿石数量还未拆分完时
                cnt = min(m, k)  # 当前拆分的数量
                new_weights.append(w * cnt)  # 拆分后的矿石重量
                new_values.append(v * cnt)   # 拆分后的矿石价值
                k -= cnt    # 减去已拆分的数量
                m *= 2      # 基数翻倍,继续拆分
    ​
        # 0-1 背包问题的动态规划求解
        dp = [0] * (C + 1)  # 初始化 DP 数组,dp[j] 表示容量为 j 时的最大价值
        for idx in range(len(new_weights)):
            w = new_weights[idx]  # 当前矿石组的重量
            val = new_values[idx]  # 当前矿石组的价值
            # 从后往前更新 DP 数组,确保每个矿石组只使用一次
            for j in range(C, w - 1, -1):
                if dp[j - w] + val > dp[j]:  # 如果选择当前矿石组更优
                    dp[j] = dp[j - w] + val  # 更新 DP 数组
    ​
        # 输出最大价值
        print(dp[C])
    ​
    if __name__ == '__main__':
        main()

为什么要进行二进制拆分优化

在多重背包问题中,每种物品的数量是有限的(例如,某种矿石最多有 k[i] 个)。如果直接使用多重背包的朴素解法(将每种物品拆分成 k[i] 个独立的物品),时间复杂度会非常高,尤其是当 k[i] 很大时。

二进制拆分优化的目的是:

  1. 减少物品数量

    • 将每种物品的数量 k[i] 拆分成若干个二进制组合,从而将物品数量从 k[i] 减少到 log2(k[i])
    • 例如,如果 k[i] = 7,可以拆分成 1, 2, 4,而不是 1, 1, 1, 1, 1, 1, 1
  2. 降低时间复杂度

    • 通过减少物品数量,动态规划的状态转移次数也会减少,从而降低时间复杂度。
  3. 保持问题等价性

    • 二进制拆分后,仍然可以通过组合这些拆分后的物品来表示原始物品的任意数量(从 0k[i])。

如何进行二进制拆分优化

二进制拆分优化的核心思想是:将每种物品的数量 k[i] 拆分成若干个 2 的幂次方 的组合,直到无法继续拆分。

拆分步骤

  1. 初始化

    • 对于每种物品,设其数量为 k[i]
    • 初始化一个基数 m = 1
  2. 拆分过程

    • 每次将 m 作为拆分单位,拆分出 min(m, k[i]) 个物品。
    • 将拆分出的物品重量和价值分别乘以 min(m, k[i]),并加入新的物品列表。
    • 更新剩余数量 k[i] -= min(m, k[i])
    • 将基数 m 翻倍(m *= 2),继续拆分,直到 k[i]0
  3. 拆分示例

    • 假设某种物品的重量为 w[i],价值为 v[i],数量为 k[i] = 7

    • 拆分过程:

      • 第一次拆分:m = 1,拆分出 1 个物品,重量为 w[i] * 1,价值为 v[i] * 1
      • 第二次拆分:m = 2,拆分出 2 个物品,重量为 w[i] * 2,价值为 v[i] * 2
      • 第三次拆分:m = 4,拆分出 4 个物品,重量为 w[i] * 4,价值为 v[i] * 4
    • 最终拆分结果:[w[i], 2*w[i], 4*w[i]][v[i], 2*v[i], 4*v[i]]

  4. 拆分后的物品列表

    • 将拆分后的物品重量和价值分别存储到 new_weightsnew_values 中。

背包问题总结

416.分割等和子集1

img