【算法-0】背包问题(三维+二维)

一、问题描述(二维)

输入描述

你有一个背包,最多能容纳的体积是 V。

现在有 n 个物品,第 i 个物品的体积为 vi,价值为 wi。

(1)求这个背包至多能装多大价值的物品?

(2)若背包恰好装满,求至多能装多大价值的物品?

输入 :第一行两个整数 n 和 V,表示物品个数和背包体积。

接下来 n 行,每行两个数 vi 和 wi ,表示第 i 个物品的体积和价值

输出:第一行输出第一问的答案,第二行输出第二问的答案,如果无解输出0。

核心模型

  • 0-1 背包问题:每件宝物只有一件 。对它,你只有两个选择:不拿(0) 或者 拿(1)
  • 假设背包容量为 W。我们用一个二维表格 dp[i][j] 来记录状态,它代表的含义是:在前 i 件物品中进行选择,当背包容量还剩 j 时,能凑出的V价值最大价值
  • 面对第 i 件物品(占容量为 w, 价值为 v)时,你需要做一道选择题,到了状态转移方程:
  • 如果"有 3 个物品,背包最大容量(体积限制)为 5",那么初始化的二维 DP 表格大小应该是 4*6;需要留出专门的一行来表示一个物品都不选(0个物品)的初始状态
  • dp[i][j] 表示"从前 i 个物品中挑选,总体积不超过 j 的所有选法中,能选出来的最大价值
  • 不选第 i 个:那么当前的最高价值完全取决于前 i-1 个物品。背包的容量也没有被占用,依然是 j
  • 选第 i 个:背包必须装得下它,即当前容量 j 必须大于等于第 i 个物品的体积 v[i](满足 j - v[i] >= 0)

代码实现

java 复制代码
import java.util.Scanner;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        if (!sc.hasNextInt()) return;
        
        int n = sc.nextInt(); // 物品数量
        int V = sc.nextInt(); // 背包最大体积
        
        int[] v = new int[n + 1]; // 体积数组
        int[] w = new int[n + 1]; // 价值数组
        
        for (int i = 1; i <= n; i++) {
            v[i] = sc.nextInt();
            w[i] = sc.nextInt();
        }
        
        // ==================== 第一问:至多能装多大价值 ====================
        // 定义二维数组:(n+1) 行, (V+1) 列
        int[][] dp1 = new int[n + 1][V + 1]; 
        // Java 默认初始化全为 0,即 dp1[0][j] 和 dp1[i][0] 自动为 0
        
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= V; j++) {
                // 情况 1:不选第 i 个物品,直接继承前 i-1 个物品在容量 j 时的价值
                dp1[i][j] = dp1[i - 1][j];
                
                // 情况 2:选第 i 个物品(前提是背包容量 j 装得下它)
                if (j >= v[i]) {
                    dp1[i][j] = Math.max(dp1[i][j], dp1[i - 1][j - v[i]] + w[i]);
                }
            }
        }
        System.out.println(dp1[n][V]);
        
        // ==================== 第二问:若背包恰好装满(用 -1 表示无解) ====================
        int[][] dp2 = new int[n + 1][V + 1];
        
        // 初始化:全部填上 -1
        for (int i = 0; i <= n; i++) {
            Arrays.fill(dp2[i], -1);
        }
        // 唯独背包容量为 0 时,什么都不装就是"恰好装满",价值为 0
        // 注意:无论选到第几个物品,只要容量为 0,不选就是合法的装满状态
        for (int i = 0; i <= n; i++) {
            dp2[i][0] = 0;
        }
        
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= V; j++) {
                // 首先,不管能不能拿第 i 个物品,都可以选择"不拿"
                // 直接继承上一行数据(注意:如果上一行是 -1,这里也会继承 -1)
                dp2[i][j] = dp2[i - 1][j];
                
                // 其次,如果背包塞得下物品 i,且上一行剩余容量的状态存在有效解(不为 -1)
                if (j >= v[i] && dp2[i - 1][j - v[i]] != -1) {
                    // 如果刚才继承过来的是 -1(说明不拿第i个物品此容量无解),直接接受新组合
                    if (dp2[i][j] == -1) {
                        dp2[i][j] = dp2[i - 1][j - v[i]] + w[i];
                    } else {
                        // 如果原本就有解,则两者决出最大值
                        dp2[i][j] = Math.max(dp2[i][j], dp2[i - 1][j - v[i]] + w[i]);
                    }
                }
            }
        }
        
        // 如果最后答案依然是 -1,说明无法恰好装满,按题目要求输出 0
        if (dp2[n][V] == -1) {
            System.out.println(0);
        } else {
            System.out.println(dp2[n][V]);
        }
        
        sc.close();
    }
}

物品数和体积**+1,是为了给"边界状态"(也就是"什么都没有"的初始情况)留出专门的位置。** 如果数组不 +1,代码在运行的第一步就会直接发生数组越界异常

动态规划表格推导

  • 输入: 3 个物品,背包总体积 V = 5。

  • 物品信息:

    • 物品 1:体积 2,价值 10

    • 物品 2:体积 4,价值 5

    • 物品 3:体积 1,价值 4

i (物品) \ j (容量) 0 1 2 3 4 5 变化含义解释
i = 0 (无物品) 0 0 0 0 0 0 没物品可选,价值全是 0。
i = 1 (v=2, w=10) 0 0 10 10 10 10 容量>= 2 时,能放下物品1,价值变 10。
i = 2 (v=4, w=5) 0 0 10 10 10 10 物品2性价比低,不如只拿物品1(保持不选的状态)。
i = 3 (v=1, w=4) 0 4 10 14 14 14 容量3时:拿物品3后剩容量2,去上一行查 dp[1][2]=10,得到 4+10=14。
[第一问(至多装多大):初始化为 0]

dp[3][5] = 14(选第1、3个物品,总体积 3,没装满,但价值最大)

i \ j 0 1 2 3 4 5 变化含义解释
i = 0 0 -1 -1 -1 -1 -1 只有容量0能被"什么都不装"恰好塞满。
i = 1 (v=2, w=10) 0 -1 10 -1 -1 -1 容量2恰好被物品1装满,价值10。其他容量装不满。
i = 2 (v=4, w=5) 0 -1 10 -1 5 -1 容量4恰好被物品2装满,价值5。
i = 3 (v=1, w=4) 0 4 10 14 5 9 容量5时:拿物品3后剩容量4,上一行 dp[2][4]=5 是合法的,所以 4+5=9(恰好装满!)。
[第二问表格(恰好装满):初始化为 -1]

状态含义: dp[i][j] 表示从前 i 个物品中选,总体积恰好等于 j 的最大价值(-1 表示无法恰好装满)dp[3][5] = 9(选第2、3个物品,总体积 4+1=5,恰好装满)

代码优化

①利用滚动数组做空间上的优化

实现第 i 行 j 列的时候,只需要判断第 i-1 行和 j 列,还有第 i-1 行和 j-vi 列,也就是只需要上一行 i-1 行的数据;这个时候,我们只需要一个2行 V+1列的数组

②再优化,使用一维数组,只需要一个1行 V+1列的数组

从左往右更新的时候会覆盖掉前面原来的值。所以我们要从右往左进行更新dp[i][j]

java 复制代码
import java.util.Scanner;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        if (!sc.hasNextInt()) return;
        
        int n = sc.nextInt(); // 物品数量
        int V = sc.nextInt(); // 背包最大体积
        
        int[] v = new int[n + 1]; // 体积数组
        int[] w = new int[n + 1]; // 价值数组
        
        for (int i = 1; i <= n; i++) {
            v[i] = sc.nextInt();
            w[i] = sc.nextInt();
        }
        
        // ==================== 第一问:至多能装多大价值 ====================
        int[] dp1 = new int[V + 1]; // 默认初始化全为 0
        
        for (int i = 1; i <= n; i++) {
            for (int j = V; j >= v[i]; j--) {
                dp1[j] = Math.max(dp1[j], dp1[j - v[i]] + w[i]);
            }
        }
        System.out.println(dp1[V]);
        
        // ==================== 第二问:若背包恰好装满(用 -1 表示无解) ====================
        int[] dp2 = new int[V + 1];
        // 初始化:全部填上 -1,代表目前所有容量都无法被恰好装满
        Arrays.fill(dp2, -1);
        // 唯独容量为 0 的背包,什么都不装就是恰好装满,价值为 0
        dp2[0] = 0; 
        
        for (int i = 1; i <= n; i++) {
            for (int j = V; j >= v[i]; j--) {
                // 【核心修改点】:只有当剩余容量的状态不是 -1(即该容量可以被恰好装满)时,才能转移
                if (dp2[j - v[i]] != -1) {
                    // 如果当前 dp2[j] 也是 -1,说明之前没装满过,直接接受新组合
                    // 如果 dp2[j] 不是 -1,则跟新组合决出最大值
                    if (dp2[j] == -1) {
                        dp2[j] = dp2[j - v[i]] + w[i];
                    } else {
                        dp2[j] = Math.max(dp2[j], dp2[j - v[i]] + w[i]);
                    }
                }
            }
        }
        
        // 如果最后答案依然是 -1,说明无法恰好装满,按题目要求输出 0
        if (dp2[V] == -1) {
            System.out.println(0);
        } else {
            System.out.println(dp2[V]);
        }
        
        sc.close();
    }
}

常见变体

背包类型 规则定义 生活场景类比 解题核心差异
0-1 背包 每种物品只有 1 件 珠宝店盗宝(每件古董独一无二) 遍历容量时需要逆序(防止同一件物品被重复放入)。
完全背包 每种物品有 无限件 超时货架购物(薯片管够,随便拿) 遍历容量时需要顺序(允许同一件物品重复叠加)。
多重背包 每种物品有 限定的数量 N_i 件 菜市场买菜(土豆只有3个,西红柿只有5个) 可以拆解为 0-1 背包,或者用二进制拆分优化。
分数背包 物品可以 任意切分(拿走一部分)。 散装散称的黄金粉末、散装大米 极其简单,直接用贪心算法(优先挑性价比最高的切)。

二、类比背包问题--(三维背包问题)一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

链接:https://leetcode.cn/problems/ones-and-zeroes/description/

输入描述

示例 1

复制代码
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 

示例 2

复制代码
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

物品 (i) :题目给出的字符串数组 strs 中的每一个二进制字符串(如 "10", "0001"

第一种代价 (v_i) :当前字符串中 0 的个数 (对应背包问题里的体积);限制m个

第二种代价 (m_i) :当前字符串中 1 的个数 (对应背包问题里的重量);限制n个

物品的价值 (w_i) :每选一个字符串,子集的长度+1;物品的价值固定为 1

状态转移方程

初始化的三维 DP 表格大小应该是 (n+1) * (V+1)* (M+1)

表示从前 i 个物品中挑选,总体积不超过 j 、总重量不超过 k 的所有选法中,能选出来的最大价值

不选第 i 个:当前的最高价值完全取决于前 i-1 个物品。背包的体积和重量都没有被占用,依然是 j 和 k

选第 i 个 :背包必须同时装得下它的体积和重量!即当前体积 j >= v[i] 且当前重量 k >= m[i] ;拿走它,并去前 i-1 个物品里找扣除它体积和重量后的最大价值

代码实现

java 复制代码
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        
        // 1. 读取数组大小和限制条件
        // 对应图 3 样例里的 strs 长度为 5, m = 5, n = 3
        int len = sc.nextInt(); 
        int m = sc.nextInt();   
        int n = sc.nextInt();   
        
        // 2. 读取所有的字符串物品
        String[] strs = new String[len];
        for (int i = 0; i < len; i++) {
            strs[i] = sc.next();
        }
        
        // 3. 初始化动态规划数组
        // 大小加 1 是因为容量可以为 0(代表 0 个 '0' 或 0 个 '1' 的边界状态)
        int[][] dp = new int[m + 1][n + 1];
        
        // 4. 遍历每一个字符串
        for (int i = 0; i < len; i++) {
            String str = strs[i];
            
            // 统计当前字符串里 0 和 1 的个数
            int zeros = 0;
            int ones = 0;
            for (int s = 0; s < str.length(); s++) {
                if (str.charAt(s) == '0') {
                    zeros++;
                } else {
                    ones++;
                }
            }
            
            // 5. 状态转移(核心部分):必须逆序从大到小遍历
            for (int j = m; j >= zeros; j--) {
                for (int k = n; k >= ones; k--) {
                    // 对比"不选"与"选择"当前字符串,哪个能装得更多
                    dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1);
                }
            }
        }
        
        // 6. 打印最终结果(对应图 3 的输出:4)
        System.out.println(dp[m][n]);
        
        sc.close();
    }
}
相关推荐
葫三生1 小时前
《论三生原理》对《周易》《道德经》的一次根本性重写?
人工智能·算法·计算机视觉·区块链·量子计算
whuhewei1 小时前
手写Promise
开发语言·javascript·ecmascript
心中有国也有家1 小时前
ascend-boost-comm:一次写完,到处复用——算子公共平台的 M×N 哲学
人工智能·经验分享·笔记·分布式·算法
AI科技星1 小时前
空间圆柱螺旋运动第一性原理终极推导·证明·核验·全量纲闭环
开发语言·人工智能·算法·计算机视觉·量子计算
shinelord明1 小时前
【云计算】k8sclient API 镜像操作 Java 类封装
java·kubernetes·云计算
invicinble1 小时前
spring事务相关信息量的沉淀
java·后端·spring
basketball6161 小时前
C++ 多态完全指南:同一个接口,千变万化的行为
java·开发语言·c++
川冰ICE1 小时前
JavaScript入门⑤|数组方法全攻略,map/filter/reduce三剑客
开发语言·javascript·ecmascript
KANGBboy2 小时前
java知识二(程序流程控制)
java·开发语言