LeetCode算法日记 - Day 108: 01背包

目录

[1. 01背包](#1. 01背包)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#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 种),计算每种组合的总体积和总价值,筛选出符合条件的最大值。

java 复制代码
// 常规解法:暴力枚举(会超时)
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 代码实现

java 复制代码
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 数组存储所有状态
相关推荐
大千AI助手1 小时前
平衡二叉树:机器学习中高效数据组织的基石
数据结构·人工智能·机器学习·二叉树·大模型·平衡二叉树·大千ai助手
九年义务漏网鲨鱼1 小时前
【多模态大模型面经】现代大模型架构(一): 组注意力机制(GQA)和 RMSNorm
人工智能·深度学习·算法·架构·大模型·强化学习
闲人编程2 小时前
CPython与PyPy性能对比:不同解释器的优劣分析
python·算法·编译器·jit·cpython·codecapsule
杜子不疼.2 小时前
【C++】深入解析AVL树:平衡搜索树的核心概念与实现
android·c++·算法
小武~2 小时前
Leetcode 每日一题C 语言版 -- 88 merge sorted array
c语言·算法·leetcode
e***U8202 小时前
算法设计模式
算法·设计模式
是苏浙2 小时前
零基础入门C语言之C语言实现数据结构之栈
c语言·开发语言·数据结构
徐子童3 小时前
数据结构----排序算法
java·数据结构·算法·排序算法·面试题
hansang_IR3 小时前
【记录】四道双指针
c++·算法·贪心·双指针