代码改变世界

深入解析:LeetCode算法日记 - Day 108: 01背包

2026-01-27 12:23  tlnshuju  阅读(0)  评论(0)    收藏  举报

目录

1. 01背包

1.1 题目解析

1.2 解法

1.3 代码实现


1. 01背包

https://www.nowcoder.com/practice/fd55637d3f24484e96dad9e992d3f62e?tpId=230&tqId=2032484&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D196

描述

你有一个背包,最大容量为 V。现有 n 件物品,第 i 件物品的体积为 vi​,价值为 wi​。研究人员提出以下两种装填方案:
1. 1.​ 不要求装满背包,求能获得的最大总价值;
2. 2.​ 要求最终恰好装满背包,求能获得的最大总价值。若不存在使背包恰好装满的装法,则答案记为 00。

输入描述:

第一行输入两个整数 n 和 (1≦,≦103)V(1≦n,V≦103),分别表示物品数量与背包容量。
此后 n 行,第 i 行输入两个整数 ,(1≦,≦103)vi​,wi​(1≦vi​,wi​≦103),分别表示第 i 件物品的体积与价值。

输出描述:

输出两行:
1. 1.​ 第一行输出方案 11 的答案;
2. 2.​ 第二行输出方案 22 的答案(若无解输出 00)。

示例1

输入:

3 5
2 10
4 5
1 4

复制输出:

14
9

复制说明:

在该组样例中: 
∙ ∙ 选择第 11、第 33 件物品即可获得最大价值 10+4=1410+4=14(未装满); 
∙ ∙ 选择第 22、第 33 件物品可使背包体积 4+1=54+1=5 恰好装满且价值最大,为 5+4=95+4=9。

示例2

输入:

3 8
12 6
11 8
6 8

复制输出:

8
0

复制说明:

装第三个物品时总价值最大但是不满,装满背包无解。

1.1 题目解析

题目本质
经典的 01 背包问题,但有两个变种——一个允许背包有剩余空间,另一个要求恰好装满。核心是"在容量限制下,如何选择物品使价值最大",本质上是线性DP。

常规解法
暴力枚举所有物品的选/不选组合(2^n 种),计算每种组合的总体积和总价值,筛选出符合条件的最大值。

// 常规解法:暴力枚举(会超时)
public class Solution {
    static int maxValue1 = 0;  // 方案1:不要求装满
    static int maxValue2 = 0;  // 方案2:恰好装满
    public static void dfs(int[] v, int[] w, int index, int curV, int curW, int V) {
        if (index == v.length) {
            // 方案1:只要不超容量
            maxValue1 = Math.max(maxValue1, curW);
            // 方案2:必须恰好装满
            if (curV == V) {
                maxValue2 = Math.max(maxValue2, curW);
            }
            return;
        }
        // 不选当前物品
        dfs(v, w, index + 1, curV, curW, V);
        // 选当前物品
        if (curV + v[index] <= V) {
            dfs(v, w, index + 1, curV + v[index], curW + w[index], V);
        }
    }
}

问题分析
暴力枚举的时间复杂度是 O(2^n),当 n=1000 时完全不可行。问题在于存在大量重复计算——比如"前 5 个物品选了 1、3、5"和"前 5 个物品选了 3、1、5"本质是同一个状态,但会被重复计算。

思路转折
要想高效 → 必须消除重复计算 → 动态规划。关键观察:当前状态只依赖"处理了多少个物品"和"当前背包容量",与选择的顺序无关。定义 dp[i][j] 表示"前 i 个物品,容量为 j 时的最优解",通过递推逐步构建答案,时间复杂度降至 O(nV)。

1.2 解法

算法思想

两个方案的核心区别在于 DP 初始化和状态有效性

  • 外层循环 i:遍历第 i 个物品(处理第 i 个物品)。

  • 内层循环 j:遍历当前背包的容量 j(从 1 到最大容量 v)。

  • 方案1(不装满):dp[i][j] = 前 i 个物品,容量 ≤ j 时的最大价值

  • 递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])

    • 初始状态:dp[0][j] = 0(所有容量都合法,最差不选任何物品)

  • 方案2(恰好装满):dp[i][j] = 前 i 个物品,容量 = j 时的最大价值

  • 递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])(需判断状态有效性)

    • 初始状态:dp[0][0] = 0,dp[0][j] = -1(j>0 时无法装满,标记为无效)

步骤拆解

方案1:不要求装满

i)初始化:dp[0][j] = 0(所有容量默认为 0,表示不选物品)

ii)遍历物品 i(1 到 n)和容量 j(1 到 V)

iii)状态转移:

  • 不选物品 i:dp[i][j] = dp[i-1][j]

  • 选物品 i(若容量够):dp[i][j] = dp[i-1][j-v[i]] + w[i]

  • 取两者最大值

iv)答案:dp[n][V]

方案2:恰好装满

i)初始化:dp[0][0] = 0,dp[0][j] = -1(j>0,标记"无法装满")

ii)遍历物品 i(1 到 n)和容量 j(1 到 V)

iii)状态转移:

  • 不选物品 i:dp[i][j] = dp[i-1][j]

  • 选物品 i(若容量够 且上一状态有效
    dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]] + w[i]);

iv)答案案:dp[n][V] == -1 ? 0 : dp[n][V]

易错点

  • 方案2 的初始化:必须将 dp[0][j](j>0)初始化为 -1,而不是 0。0 表示"合法状态,价值为 0",-1 表示"不可达状态"。

  • 方案2 的状态转移判断:必须检查 dp[i-1][j-v[i]] != -1,否则会从"无法装满"的无效状态转移,导致错误结果。

  • 二维数组重用:两个方案共用 dp 数组时,方案2 开始前必须逐行清零(Arrays.fill(dp[i], 0)),而不是 Arrays.fill(dp, 0)(后者只清空引用)。

  • 循环边界:容量循环可以从 0 或 1 开始,但从 1 开始更清晰(因为 dp[i][0] 始终为 0)。物品循环必须从 1 到 n(包含 n)。

1.3 代码实现

import java.util.Arrays;
import java.util.Scanner;
public class Main {
    private static final int N = 1010;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 读入数据
        int n = scanner.nextInt();
        int V = scanner.nextInt();
        int[] v = new int[N];
        int[] w = new int[N];
        int[][] dp = new int[N][N];
        for (int i = 1; i <= n; i++) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        // 方案1:不要求装满
        // dp[i][j] 表示前 i 个物品,容量不超过 j 的最大价值
        // 初始状态:dp[0][j] = 0(默认值,表示不选任何物品)
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= V; j++) {
                dp[i][j] = dp[i - 1][j];  // 不选物品 i
                if (j >= v[i]) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
                }
            }
        }
        System.out.println(dp[n][V]);
        // 方案2:恰好装满
        // dp[i][j] 表示前 i 个物品,容量恰好为 j 的最大价值
        // 初始状态:dp[0][0] = 0,dp[0][j] = -1(j>0 时无法装满)
        for (int i = 0; i <= n; i++) {
            Arrays.fill(dp[i], 0);
        }
        for (int j = 1; j <= V; j++) {
            dp[0][j] = -1;  // 标记为无效状态
        }
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= V; j++) {
                dp[i][j] = dp[i - 1][j];  // 不选物品 i
                // 选物品 i:必须判断上一状态是否有效
                if (j >= v[i] && dp[i - 1][j - v[i]] != -1) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
                }
            }
        }
        System.out.println(dp[n][V] == -1 ? 0 : dp[n][V]);
        scanner.close();
    }
}

复杂度分析

  • 时间复杂度:O(nV),两层循环遍历所有状态,每个状态 O(1) 转移

  • 空间复杂度:O(nV),使用二维 dp 数组存储所有状态