代码随想录算法训练营第三十八天 |动态规划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 零钱兑换
如果要是凑成总金额所需的最多的硬币个数的话,那么就很简单了:
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 完全平方数
所以大致框架就出来了。
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 单词拆分
思路:这个题的思路真难想,擦。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。
物品为:
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 2 |
| 物品1 | 3 | 20 | 3 |
| 物品2 | 4 | 30 | 2 |
问背包能背的物品最大价值是多少?
和如下情况有区别么?
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 1 |
| 物品0 | 1 | 15 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品2 | 4 | 30 | 1 |
| 物品2 | 4 | 30 | 1 |
毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。
56 携带矿石资源(第八期模拟笔试)
思路:首先拆开,然后换成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()
这次的代码把之前的技巧用上了,不过还是不行,过不去啊...
以下代码可以通过,但是现在看不懂。。。
方法思路
-
问题分析:
- 我们有一个容量为
C的背包,和N种不同类型的矿石。 - 每种矿石有重量
w[i],价值v[i],以及最多k[i]个可用。 - 目标是在不超过背包容量的情况下,最大化所选矿石的总价值。
- 我们有一个容量为
-
二进制优化:
- 将每种矿石的数量
k[i]拆分成二进制形式,例如k[i] = 5拆分成1, 4。 - 这样,我们可以将每组矿石视为独立的物品,每个组的数量是
2^m,直到剩余数量小于2^m。
- 将每种矿石的数量
-
动态规划求解:
- 使用一维 DP 数组,其中
dp[j]表示容量为j时的最大价值。 - 对于每个拆分后的矿石组,更新 DP 数组。
- 使用一维 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] 很大时。
二进制拆分优化的目的是:
-
减少物品数量:
- 将每种物品的数量
k[i]拆分成若干个二进制组合,从而将物品数量从k[i]减少到log2(k[i])。 - 例如,如果
k[i] = 7,可以拆分成1, 2, 4,而不是1, 1, 1, 1, 1, 1, 1。
- 将每种物品的数量
-
降低时间复杂度:
- 通过减少物品数量,动态规划的状态转移次数也会减少,从而降低时间复杂度。
-
保持问题等价性:
- 二进制拆分后,仍然可以通过组合这些拆分后的物品来表示原始物品的任意数量(从
0到k[i])。
- 二进制拆分后,仍然可以通过组合这些拆分后的物品来表示原始物品的任意数量(从
如何进行二进制拆分优化?
二进制拆分优化的核心思想是:将每种物品的数量 k[i] 拆分成若干个 2 的幂次方 的组合,直到无法继续拆分。
拆分步骤:
-
初始化:
- 对于每种物品,设其数量为
k[i]。 - 初始化一个基数
m = 1。
- 对于每种物品,设其数量为
-
拆分过程:
- 每次将
m作为拆分单位,拆分出min(m, k[i])个物品。 - 将拆分出的物品重量和价值分别乘以
min(m, k[i]),并加入新的物品列表。 - 更新剩余数量
k[i] -= min(m, k[i])。 - 将基数
m翻倍(m *= 2),继续拆分,直到k[i]为0。
- 每次将
-
拆分示例:
-
假设某种物品的重量为
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]]。
-
-
拆分后的物品列表:
- 将拆分后的物品重量和价值分别存储到
new_weights和new_values中。
- 将拆分后的物品重量和价值分别存储到