02_Knapsack DP 背包DP

背包 DP

背包问题简介

背包问题通常涉及将有限容量的容器用物品的子集填满,我们希望计算或优化与物品相关的某些数量。几乎每次,你可以将每个物品视为具有正重量,而我们选择的物品的总重量不得超过容器的容量,这个容量是一个数字。

背包问题的变体

  • 0/1 背包问题: 选择物品的子集,使得它们总价值最大化,且总重量不超过容器容量
  • 完全背包问题: 找到所有可能的、由任何物品子集实现的总重量,这些总重量不超过容器容量
  • 计数问题: 计算有多少种物品序列能够完全填满容器,这意味着总重量正好等于容器的容量(顺序可能重要也可能不重要)

提示: 背包问题的动态规划解法通常包含一个状态来跟踪背包的容量,状态转移涉及尝试将一个物品添加到背包中。在竞赛编程中,你可以预期经典背包问题会被赋予变化、伪装和额外的状态信息。


示例 - 骰子组合 (Dice Combinations)

题目描述

https://cses.fi/problemset/task/1633

问题要求我们计算有多少种骰子投掷序列,使得顶面的和为 NN ≤ 106)。为了与背包问题类比,这意味着我们有无限数量的重量为 16 的物品,并且我们想要计算有多少种物品序列,使得如果我们按照序列将物品放入容器中,容器会完全填满。请注意,在这个问题中物品的顺序是重要的。

解法

时间复杂度: O(N)

dp[x] 为掷骰子得到的和为 x 的所有序列的数量。要计算和为 N 的序列数量,或者说找到 dp[N],我们可以看看最后一个掷骰子的结果,使得总和达到 N

如果最后一个掷骰子的结果是 1,那么我们知道当最后一个掷骰子的结果是 1 时,有 dp[N − 1] 种方法可以得到和为 N。如果最后一个掷骰子的结果是 2,那么我们知道当最后一个掷骰子的结果是 2 时,有 dp[N − 2] 种方法可以得到和为 N。继续用这种逻辑处理所有骰子数值直到 6。考虑所有这些情况,我们得到:

状态转移方程

dp[i] = dp[i − 1] + dp[i − 2] + dp[i − 3] + dp[i − 4] + dp[i − 5] + dp[i − 6]

从基础情况 dp[0] = 1 开始,然后 dp[1]dp[2]dp[3],依此类推,可以使用递推公式计算,直到找到 dp[N]。注意在代码中,如果 x < 0 存在,我们会忽略 dp[x]

为什么 dp[0] = 1? - dp[0] 表示和为 0 的序列数量 - 只有一种方式得到和为 0:什么都不投 - 这是一个基准点,作为递推的起点 - 如果没有 dp[0] = 1,后续所有计算都会出错

MOD = 10**9 + 7
n = int(input())
dp = [1]

for i in range(n):
    total = sum(dp[-6:])
    dp.append(total % MOD)

print(dp[-1])

0/1 背包问题

问题描述

N 件物品,每件物品有重量 wi 和价值 vi,求在不超过背包容量 W 的情况下,选择物品使得总价值最大。

解法

定义 dp[i][j] 为考虑前 i 件物品,背包容量为 j 时的最大价值。

状态转移方程: - 不选第 i 件物品:dp[i][j] = dp[i − 1][j] - 选第 i 件物品:dp[i][j] = dp[i − 1][j − wi] + vi

dp[i][j] = max (dp[i − 1][j], dp[i − 1][j − wi] + vi)

代码实现

# 0/1 背包 - 二维 DP
N, W = map(int, input().split())
dp = [[0] * (W + 1) for _ in range(N + 1)]

for i in range(1, N + 1):
    w, v = map(int, input().split())
    for j in range(W + 1):
        dp[i][j] = dp[i-1][j]
        if j >= w:
            dp[i][j] = max(dp[i][j], dp[i-1][j-w] + v)

print(dp[N][W])

详细解释

外层循环 i:遍历第 1 到 N 件物品 内层循环 j:遍历容量 0 到 W

核心逻辑(以 i=2, j=8 为例):

假设:第 2 件物品重量 w=3,价值 v=5

dp[2][8] = max(
    dp[1][8],           # 不选第2件:用前1件填满8容量
    dp[1][8-3] + 5     # 选第2件:前1件填剩余5容量 + 第2件价值5
)

空间优化

由于每行只依赖上一行,可以使用一维数组:

# 0/01 背包 - 一维 DP(空间优化)
N, W = map(int, input().split())
dp = [0] * (W + 1)

for _ in range(N):
    w, v = map(int, input().split())
    for j in range(W, w - 1, -1):
        dp[j] = max(dp[j], dp[j - w] + v)

print(dp[W])

注意: 一维 DP 中,内层循环需要倒序遍历,以确保每个物品只被使用一次。

为什么要倒序?

原因:避免同一物品被重复使用

以物品 (w=3, v=5) 为例:

正序遍历(错误)

for j in range(w, W + 1):  # 正序
    dp[j] = max(dp[j], dp[j-w] + v)

假设 W=6,初始 dp=[0,0,0,0,0,0,0]

j=3: dp[3] = max(0, dp[0]+5) = 5    # 放了这个物品
j=4: dp[4] = max(0, dp[1]+5) = 5    # dp[1] 是 0
j=5: dp[5] = max(0, dp[2]+5) = 5    # dp[2] 是 0
j=6: dp[6] = max(0, dp[3]+5) = max(0, 5+5) = 10   # 出问题了!

dp[3] 刚刚被更新成 5,现在 dp[6] 又去查 dp[3],相当于同一个物品用了两次

倒序遍历(正确)

for j in range(W, w - 1, -1):  # 倒序
    dp[j] = max(dp[j], dp[j-w] + v)

j=6: dp[6] = max(0, dp[3]+5) = 5    # dp[3] 还是 0(未被当前物品更新)
j=5: dp[5] = max(0, dp[2]+5) = 5    # dp[2] 还是 0
j=4: dp[4] = max(0, dp[1]+5) = 5    # dp[1] 还是 0
j=3: dp[3] = max(0, dp[0]+5) = 5    # dp[0] 是 0

倒序时,大 j 先更新,小 j 后更新。大 j 用到的 dp[j-w] 还是旧值,没被当前物品污染,确保每个物品只选一次。


完全背包问题

问题描述

N 种物品,每种物品有重量 wi 和价值 vi,每种物品可以使用无限次,求在不超过背包容量 W 的情况下,选择物品使得总价值最大。

代码实现

# 完全背包 - 一维 DP
N, W = map(int, input().split())
dp = [0] * (W + 1)

for _ in range(N):
    w, v = map(int, input().split())
    for j in range(w, W + 1):
        dp[j] = max(dp[j], dp[j - w] + v)

print(dp[W])

注意: 完全背包中内层循环需要正序遍历,因为每种物品可以使用多次。


多重背包问题

问题描述

N 种物品,每种物品有重量 wi、价值 vi 和数量 ci,求在不超过背包容量 W 的情况下,选择物品使得总价值最大。

二进制优化

将数量 ci 拆分成 1, 2, 4, 8, … 的物品件数,转化为 0/1 背包问题:

# 多重背包 - 二进制优化
N, W = map(int, input().split())
items = []

for _ in range(N):
    w, v, c = map(int, input().split())
    k = 1
    while c > 0:
        take = min(k, c)
        items.append((w * take, v * take))
        c -= take
        k *= 2

dp = [0] * (W + 1)
for w, v in items:
    for j in range(W, w - 1, -1):
        dp[j] = max(dp[j], dp[j - w] + v)

print(dp[W])

总结

问题类型 状态转移 内层循环方向
0/01 背包 dp[j] = max (dp[j], dp[j − w] + v) 倒序
完全背包 dp[j] = max (dp[j], dp[j − w] + v) 正序
多重背包 二进制优化转为 0/01 背包 倒序

02_Knapsack DP 背包DP
https://mingsm17518.github.io/2026/04/07/algorithm/03_Gold/02_动态规划/02_Knapsack DP 背包DP/
作者
Ming
发布于
2026年4月7日
更新于
2026年4月7日
许可协议