动态规划类-背包问题
重点是0-1背包和完全背包,其他类型都是竞赛级别的
背包问题的定义
背包问题是一类经典的组合优化问题,核心是在有限容量的约束下,从一组物品中选择子集以最大化或最小化目标(如价值或成本)。
常见变种:
-
多重背包:每种物品有数量限制。
-
分组背包:物品分组,每组只能选一个。
-
多维背包:容量限制涉及多个维度(如重量和体积)。

0-1背包问题
0-1背包问题是最基础的背包问题形式。给定一组物品,每个物品有固定的重量和价值,背包有容量限制。每个物品只能选择装入(1)或不装入(0),目标是在不超过背包容量的前提下,最大化背包中物品的总价值。
特点:
-
每个物品只能选择一次。
-
通常用动态规划解决,状态转移方程为:
-
其中 (dp[i][j]) 表示前 (i) 个物品在容量 (j) 下的最大价值,
分别为第 (i) 个物品的重量和价值。
完全背包问题
完全背包问题与0-1背包类似,但允许每种物品无限次选择。即物品可以重复装入背包,只要总重量不超过容量限制。
特点:
-
物品可重复选取。
-
动态规划的状态转移方程为:
注意第二项为
,表示可重复选择当前物品。
背包问题二维
1.dp[i][j]
含义【0,i】下标的物品任取,放入容量为j的背包里
2.递推公式
放物品i,或者不放物品i
不管放不放物品i,【0,i-1】物品都是任取
假设不放物品i:dp【i-1】【j】
假设放物品i:dp【i-1】【j-weight【i】】+value【i】
dp【i】【j】=max(dp【i-1】【j】,dp【i-1】【j-weight【i】】+value【i】)
3.初始化


其他位置初始化什么都可以,因为后面会被覆盖掉
4.遍历顺序
都可以
python
# 1. 输入:n是物品数量,bagweight是背包最大承重
n, bagweight = map(int, input().split())
# 2. 输入:每个物品的重量数组
weight = list(map(int, input().split()))
# 输入:每个物品的价值数组
value = list(map(int, input().split()))
# 3. 创建二维DP数组
# dp[i][j]:表示 前i个物品里选,背包容量为j 时的最大价值
# 维度:n行(物品),bagweight+1列(容量0~bagweight),全部初始化为0
dp = [[0] * (bagweight + 1) for _ in range(n)]
# 4. 初始化第一行(只有第0个物品时)
# 背包容量j >= 物品0重量时,才能放下,价值=value[0]
for j in range(weight[0], bagweight + 1):
dp[0][j] = value[0]
# 5. 填充DP表(从第1个物品开始遍历)
for i in range(1, n):
# 遍历所有背包容量
for j in range(bagweight + 1):
# 如果当前容量j 装不下 第i个物品
if j < weight[i]:
# 只能不选这个物品 → 等于上一行的结果
dp[i][j] = dp[i - 1][j]
# 装得下:两种选择取最大
else:
# 不选i:dp[i-1][j]
# 选i:dp[i-1][j - weight[i]] + value[i]
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
# 6. 答案:n个物品,容量bagweight时的最大价值
print(dp[n - 1][bagweight])
背包问题一维
动态规划:01背包理论基础(滚动数组) | 动态规划 | 滚动数组 | 01背包 | 代码随想录
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
1.dp数组的含义
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
2.递推公式
二维dp数组的递推公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
两种状态:放或者不放
递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
3.初始化
我们要得到最大值,初始化的时候取一个小值,后面计算可以覆盖掉
dp[0]=0
4.遍历顺序
难以理解,我看不懂
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
为什么呢?倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次
python
n, bagweight = map(int, input().split())
weight = list(map(int, input().split()))
value = list(map(int, input().split()))
dp = [0] * (bagweight + 1) # 创建一个动态规划数组dp,初始值为0
dp[0] = 0 # 初始化dp[0] = 0,背包容量为0,价值最大为0
for i in range(n): # 应该先遍历物品,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品
for j in range(bagweight, weight[i]-1, -1): # 倒序遍历背包容量是为了保证物品i只被放入一次
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp[bagweight])
416. 分割等和子集
题目链接416. 分割等和子集 - 力扣(LeetCode)
思路
-
数组总和为
sum_total,若能分成两个和相等子集,总和必须是偶数,目标子集和:target=sum_total÷2 -
问题转化为:01 背包问题从数组中选若干数字,总和恰好等于 target
-
DP 定义:
dp[j]= 背包容量为j时,能否凑出和为j
关键细节
-
奇数直接返回 False:无法平分
-
倒序遍历背包容量:01 背包经典写法,保证每个数字只选 1 次
-
状态转移:dp[j]=dp[j]∥dp[j−num]
-
不选当前数字:维持原本能否凑出
-
选当前数字:看
j-num是否能凑出
提交

python
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2 != 0:
return False
n = len(nums)
target = total // 2
# dp[i][j]:前i个数,能否凑出和j
dp = [[False]*(target+1) for _ in range(n)]
# 初始化第一个数
if nums[0] <= target:
dp[0][nums[0]] = True
for i in range(1, n):
for j in range(target + 1):
# 不选第i个数
dp[i][j] = dp[i-1][j]
# 选第i个数
if j >= nums[i]:
dp[i][j] = dp[i][j] or dp[i-1][j - nums[i]]
return dp[-1][target]