背包问题(Knapsack Problem)是计算机科学和运筹学中最经典的组合优化问题之一。我第一次接触这个问题是在大学算法课上,当时就被它简洁定义背后蕴含的深刻数学原理所吸引。简单来说,背包问题描述的是:给定一组物品,每个物品都有重量和价值,在限定背包总重量的情况下,如何选择物品使得背包中物品的总价值最大。
这个看似简单的问题在实际应用中有着惊人的广泛性。从资源分配到投资组合,从货物装载到任务调度,背包问题的变体几乎渗透到了我们生活和工作的方方面面。作为一名算法工程师,我在实际工作中至少遇到过十几种不同场景下的背包问题变体。
0-1背包是最基础的背包问题形式。每个物品要么完整放入背包(1),要么完全不放入(0),不能分割。这个问题可以用动态规划高效解决,其状态转移方程为:
code复制dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i]] + val[i])
其中dp[i][w]表示考虑前i个物品,背包容量为w时的最大价值。这个方程的核心思想是:对于每个物品,我们都有放入和不放入两种选择,取其中价值更大的方案。
我在实际编码时发现,这个二维DP可以优化为一维数组:
python复制def knapsack_01(values, weights, capacity):
dp = [0] * (capacity + 1)
for i in range(len(values)):
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
注意:内层循环必须从大到小遍历,否则会重复计算同一物品多次。
完全背包与0-1背包的区别在于,每种物品可以选取无限次。这在现实中对应原材料采购等场景。状态转移方程变为:
code复制dp[w] = max(dp[w], dp[w-wt[i]] + val[i])
实现代码与0-1背包类似,但内层循环改为从小到大:
python复制def unbounded_knapsack(values, weights, capacity):
dp = [0] * (capacity + 1)
for i in range(len(values)):
for w in range(weights[i], capacity + 1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
多重背包介于前两者之间,每种物品有数量限制。常见的解法是将物品按二进制分组转换为0-1背包问题。例如,某物品有13个,可以拆分为1+2+4+6个的组。
python复制def multiple_knapsack(values, weights, counts, capacity):
# 二进制拆分
new_weights = []
new_values = []
for i in range(len(counts)):
k = 1
while k <= counts[i]:
new_weights.append(weights[i] * k)
new_values.append(values[i] * k)
counts[i] -= k
k *= 2
if counts[i] > 0:
new_weights.append(weights[i] * counts[i])
new_values.append(values[i] * counts[i])
# 0-1背包解法
dp = [0] * (capacity + 1)
for i in range(len(new_values)):
for w in range(capacity, new_weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - new_weights[i]] + new_values[i])
return dp[capacity]
如前所述,二维DP可以优化为一维数组。但要注意遍历顺序:
对于0-1背包,可以预处理物品,按价值密度(价值/重量)排序,在剩余容量无法放入更高价值密度的物品时提前终止。
python复制def knapsack_01_optimized(values, weights, capacity):
# 按价值密度排序
items = sorted(zip(values, weights), key=lambda x: x[0]/x[1], reverse=True)
dp = [0] * (capacity + 1)
for v, w in items:
for j in range(capacity, w - 1, -1):
if dp[j - w] + v > dp[j]:
dp[j] = dp[j - w] + v
return dp[capacity]
对于物品数量较少的情况(n≤30),可以用分支限界法获得精确解。这种方法通过优先队列维护当前最优解的上界,剪除不可能更优的分支。
物品被分为若干组,每组只能选一个物品。解法是对每组物品进行决策:
python复制def group_knapsack(groups, capacity):
# groups是列表的列表,每个子列表是一组物品(weight,value)
dp = [0] * (capacity + 1)
for group in groups:
for w in range(capacity, -1, -1):
for weight, value in group:
if w >= weight:
dp[w] = max(dp[w], dp[w - weight] + value)
return dp[capacity]
背包有多个限制条件(如重量和体积)。状态数组增加维度:
python复制def multi_dim_knapsack(values, weights, volumes, max_weight, max_volume):
dp = [[0]*(max_volume+1) for _ in range(max_weight+1)]
for i in range(len(values)):
for w in range(max_weight, weights[i]-1, -1):
for v in range(max_volume, volumes[i]-1, -1):
if dp[w][v] < dp[w-weights[i]][v-volumes[i]] + values[i]:
dp[w][v] = dp[w-weights[i]][v-volumes[i]] + values[i]
return dp[max_weight][max_volume]
常见原因:
当背包容量极大时(如1e9),可以:
可以增加一个选择矩阵,或反向追溯DP数组:
python复制def track_selection(values, weights, capacity):
dp = [0] * (capacity + 1)
choices = [[] for _ in range(capacity + 1)]
for i in range(len(values)):
for w in range(capacity, weights[i] - 1, -1):
if dp[w] < dp[w - weights[i]] + values[i]:
dp[w] = dp[w - weights[i]] + values[i]
choices[w] = choices[w - weights[i]] + [i]
return dp[capacity], choices[capacity]
当问题规模过大时,可以采用:
对于特别复杂的变体,可以尝试遗传算法:
在物品动态到达的场景下,可以采用在线算法,根据当前信息做出局部最优决策。
背包问题的魅力在于它的简单与深刻的统一。经过多年的实践,我发现真正掌握它需要:1) 理解动态规划的本质;2) 大量练习变体问题;3) 学会根据实际问题特点调整算法。建议从LeetCode上的相关题目开始训练,逐步挑战更复杂的现实场景问题。