题解:多重背包问题
83 小S的货船租赁冒险
问题分析
这是一道经典的多重背包问题。小S希望在预算 V 内,选择若干种货船,以使总载货量最大化。对于每种货船,其数量有限,这种限制增加了问题的复杂性。
解决此问题的关键是使用动态规划,构建一个容量为 V 的背包,逐步将每种货船的选择纳入决策。
动态规划思路
我们可以用一个数组 dp 来表示在不同预算情况下的最大载货量,其中:
dp[j]表示在预算为 jj 时,小S可以获得的最大载货量。
状态转移方程: 对于第 ii 种货船,租赁成本为 v[i],最大载货量为 w[i],数量为 m[i],我们需要考虑是否选择这一种货船。决策时需满足预算 。
- 对于货船 i 的每一艘,我们逐步分配给当前预算 j: 其中 k 表示选择的船数量,范围是 。
优化处理: 由于直接枚举 kk 会导致时间复杂度较高,可以采用二进制分组优化。将货船的数量 m[i] 转化为若干虚拟物品,避免直接枚举。
算法步骤
-
初始化:
- 创建一个数组
dp,长度为 V+1V + 1,初始值为 0,表示初始预算下的最大载货量。
- 创建一个数组
-
二进制分组:
- 对于每种货船 i,将其数量 m[i] 分解为若干件物品(数量为 1,2,4,…,直至不超过 m[i]),并将每件物品视为独立的选择。
-
动态规划转移:
- 遍历所有虚拟物品,根据租赁成本和载货量更新
dp数组。
- 遍历所有虚拟物品,根据租赁成本和载货量更新
-
结果输出:
- 输出
dp[V],即在预算 VV 下的最大载货量。
- 输出
示例代码
def max_cargo(Q, V, ships):
# 初始化 DP 数组
dp = [0] * (V + 1)
# 遍历每种货船
for m, v, w in ships:
# 二进制分组
k = 1
while m > 0:
count = min(k, m) # 当前分组中物品的数量
m -= count
cost = count * v # 当前分组的总租赁成本
weight = count * w # 当前分组的总载货量
# 更新 DP 数组,从后向前遍历避免重复计算
for j in range(V, cost - 1, -1):
dp[j] = max(dp[j], dp[j - cost] + weight)
k *= 2 # 下一个分组容量倍增
return dp[V]
# 测试样例
print(max_cargo(2, 10, [[2, 3, 2], [3, 2, 10]])) # 输出: 32
print(max_cargo(3, 50, [[5, 10, 20], [2, 20, 30], [3, 15, 25]])) # 输出: 100
print(max_cargo(1, 100, [[10, 5, 50]])) # 输出: 500
print(max_cargo(4, 100, [[1, 100, 200], [2, 50, 100], [3, 33, 66], [4, 25, 50]])) # 输出: 200
print(max_cargo(2, 300, [[100, 1, 1], [50, 5, 10]])) # 输出: 550
示例解析
示例 1
输入:Q = 2, V = 10, ships = [[2, 3, 2], [3, 2, 10]]
-
初始化:
dp = [0] * 11。 -
对第 1 种货船(二进制分组:1 和 1):
- 选择一艘:成本 3,载货量 2。
- 两次遍历后,
dp = [0, 0, 0, 2, 2, 2, 4, 4, 4, 4, 4]。
-
对第 2 种货船(二进制分组:1、2):
- 第一轮:
dp = [0, 0, 0, 2, 10, 10, 12, 12, 12, 20, 20]。 - 第二轮:
dp = [0, 0, 0, 2, 10, 10, 12, 20, 20, 20, 32]。
- 第一轮:
最终结果为 dp[10] = 32。
复杂度分析
-
时间复杂度:
- 二进制分组:每种货船 m[i] 被分解为 件虚拟物品。
- 动态规划:对每件虚拟物品遍历预算 VV,复杂度为 。
-
空间复杂度:
- 仅需维护一个大小为V + 1 的 DP 数组,复杂度为 O(V)。
总结
本题通过将多重背包问题转换为多个 0/1 背包子问题,并利用二进制分组优化枚举,实现了高效求解。动态规划中的状态转移逻辑清晰,适用于资源分配、成本优化等类似问题,是经典的算法模型之一。