题目描述

牛客原题: ✅ 补充题 102. 0-1 背包的最大价值

给你一个容量为 capacity 的背包,以及长度相同的两个数组 volumes 和 values。第 i 件物品的体积为 volumes[i],价值为 values[i]。

每件物品至多选择一次。请返回所选物品总体积不超过 capacity 时,可以获得的最大总价值。背包不需要恰好装满。

示例 1:

输入: capacity = 5, volumes = [2,3,4], values = [3,4,5]
输出: 7
解释: 选择前两件,体积为 2+3=5,价值为 3+4=7,每件只使用一次。

提示:

  • 容量及物品数均不超过 1000。
  • 物品体积为正数、价值为非负数且不超过 1000。
  • 每件至多取一次。
  • 不要求恰好装满。

题意分析

每件物品只有取或不取两种选择,且最多使用一次。容量只是上限,不要求装满,因此什么都不选也是价值为 0 的合法方案。

枚举所有子集会产生指数级分支;处理完相同前缀物品、拥有相同容量上限的情况,只需要保留价值最大的结果。由此按物品推进,用容量作为动态规划状态。

解法:容量倒序的 01 背包

核心思路

[!blue]

dp[c] 表示只使用已经处理的物品、总体积不超过 c 时的最大价值。初始全为 0,表示尚未选物品。

对体积为 v、价值为 w 的当前物品,不取它就保留旧的 dp[c];取它则需要给此前物品留下 c - v 的容量,候选价值为 dp[c - v] + w。仅当 c >= v 时比较这两个选择。

容量必须从大到小更新:读取较小的 dp[c - v] 时,它尚未使用当前物品,才能保证每件至多取一次。若正序更新,读取的状态可能已经包含当前物品,便会重复选取。处理全部物品后,dp[capacity] 就是容量上限下的最优值。

解题步骤

  1. 建立容量上限为 c 时的最大价值数组,初始为 0,表示可不选任何物品。
  2. 逐个物品从大容量向小容量更新,比较不选与选择当前物品。
  3. 返回完整容量的最优值,不要求恰好装满。

代码实现

class Solution {
    public int knapsack(int capacity, int[] volumes, int[] values) {
        int[] dp = new int[capacity + 1];

        for (int i = 0; i < volumes.length; i++) {
            for (int c = capacity; c >= volumes[i]; c--) {
                dp[c] = Math.max(dp[c], dp[c - volumes[i]] + values[i]);
            }
        }

        return dp[capacity];
    }
}
func knapsack(capacity int, volumes, values []int) int {
    dp := make([]int, capacity+1)
    for i, v := range volumes {
        for c := capacity; c >= v; c-- {
            dp[c] = max(dp[c], dp[c-v]+values[i])
        }
    }
    return dp[capacity]
}

复杂度分析

  • 时间复杂度:$O(nC)$,n 为物品数,C 为背包容量;每件最多枚举 C 个容量。
  • 空间复杂度:$O(C)$,只保存一维容量状态。

关键点总结

[!green]

倒序让前驱仍属于上一批物品;初始化为 0 对应不要求恰好装满,恰好装满则要另设不可达状态。

易错点总结

[!yellow]

容量顺序改成递增会变成完全背包;本题不要求恰好装满。

相似题目

题目 难度 关联与区别
416. 分割等和子集 中等 同样每个物品最多一次,原题状态是半和是否可达,本题状态是容量内最大价值。
474. 一和零 中等 从一维容量扩展到两种资源限制,两个容量维度也需要倒序以避免重复使用。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/09154531
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!