彻底理解0-1背包问题

彻底理解0-1背包问题0 1 背包问题给定 n 个重量为 w1 w2 w3 wn 价值为 v1 v2 v3 vn 的物品和容量为 C 的背包 求这个物品中一个最有价值的子集 使得在满足背包的容量的前提下 包内的总价值最大 0 1 背包问题指的是每个物品只能使用一次递归方法首先我们用递归的方式来尝试解决这个问题我们用 F n C F n C F n C 表示将前 nnn 个物品放进容量为 CCC 的背包里 得到的最大的价值 我们用自

0-1背包问题

给定 n n n个重量为 w 1 w_1 w1 w 2 w_2 w2 w 3 w_3 w3,…, w n w_n wn,价值为 v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3,…, v n v_n vn的物品和容量为 C C C的背包,求这些物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的物品总价值最大

注:0-1背包问题指的是每个物品只能使用一次

V1.0——递归方法

首先我们用最容易理解的递归方法来尝试解决这个问题

我们用 F ( n , C ) F(n,C) F(n,C)表示将前 n n n个物品放进容量为 C C C的背包里,得到的最大的价值。

我们从自顶向下的角度来看,假如我们已经进行到了最后一步(即求解将第 n n n个物品放到背包所获得的最大价值),此时我们便有两种选择

  1. 不放第 n n n个物品,此时总价值为 F ( n − 1 , C ) F(n-1,C) F(n1,C)
  2. 放置第 n n n个物品,此时总价值为 v n + F ( n − 1 , C − w n ) v_n+F(n-1,C-w_n) vn+F(n1,Cwn)

两种选择中总价值最大的方案就是我们的最终方案,递推式(有时也称之为状态转移方程)如下

F ( i , C ) = m a x ( F ( i − 1 , C ) , v ( i ) + F ( i − 1 , C − w ( i ) ) ) F(i,C)=max(F(i-1,C),v(i)+F(i-1,C-w(i))) F(i,C)=max(F(i1,C),v(i)+F(i1,Cw(i)))

编程实现如下:

public class KnapSackV1 { 
    / * 解决背包问题的递归函数 * * @param w 物品的重量数组 * @param v 物品的价值数组 * @param index 当前待选择的物品索引 * @param capacity 当前背包有效容量 * @return 最大价值 */ private static int solveKS(int[] w, int[] v, int index, int capacity) { 
    //基准条件:如果索引无效或者容量不足,直接返回当前价值0 if (index < 0 || capacity <= 0) return 0; //不放第index个物品所得价值 int res = solveKS(w, v, index - 1, capacity); //放第index个物品所得价值(前提是:第index个物品可以放得下) if (w[index] <= capacity) { 
    res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index])); } return res; } public static int knapSack(int[] w, int[] v, int C) { 
    int size = w.length; return solveKS(w, v, size - 1, C); } public static void main(String[] args){ 
    int[] w = { 
   2,1,3,2}; int[] v = { 
   12,10,20,15}; System.out.println(knapSack(w,v,5)); } } 

V2.0——记忆化搜索

我们用递归方法可以很简单的实现以上代码,但是有个严重的问题就是,直接采用自顶向下的递归算法会导致要不止一次的解决公共子问题,因此效率是相当低下的。

我们可以将已经求得的子问题的结果保存下来,这样对子问题只会求解一次,这便是记忆化搜索。

下面在上述代码的基础上加上记忆化搜索

public class KnapSack01 { 
    private static int[][] memo; / * 解决背包问题的递归函数 * * @param w 物品的重量数组 * @param v 物品的价值数组 * @param index 当前待选择的物品索引 * @param capacity 当前背包有效容量 * @return 最大价值 */ private static int solveKS(int[] w, int[] v, int index, int capacity) { 
    //基准条件:如果索引无效或者容量不足,直接返回当前价值0 if (index < 0 || capacity <= 0) return 0; //如果此子问题已经求解过,则直接返回上次求解的结果 if (memo[index][capacity] != 0) { 
    return memo[index][capacity]; } //不放第index个物品所得价值 int res = solveKS(w, v, index - 1, capacity); //放第index个物品所得价值(前提是:第index个物品可以放得下) if (w[index] <= capacity) { 
    res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index])); } //添加子问题的解,便于下次直接使用 memo[index][capacity] = res; return res; } public static int knapSack(int[] w, int[] v, int C) { 
    int size = w.length; memo = new int[size][C + 1]; return solveKS(w, v, size - 1, C); } public static void main(String[] args) { 
    int[] w = { 
   2, 1, 3, 2}; int[] v = { 
   12, 10, 20, 15}; System.out.println(knapSack(w, v, 5)); } } 

V3.0——动态规划算法

在这里插入图片描述
我们可以很轻松地填充二维表的第一行数据,所代表的含义是 F ( 0 , C ) F(0,C) F(0,C)

即在背包容量从0到5时,选择物品0所能获得的最大价值

在这里插入图片描述
继续填充第二行数据,此时的含义是,当背包容量从0到5时,待选择物品为物品1时背包的最大价值

注意:我这里说的 不是 选择物品1时 也不是 不选择物品1时的背包最大价值,因为选或不选都有可能

除了第一行以外,其他行的计算都包含当前行物品选或不选的两种可能,按照上文的递推公式来计算

  • 当选择将物品1放入背包时(前提是放得下),此时背包的最大价值为物品1的价值+二维表[0][1]处的值,因为现在的背包容量需要减去物品1所占用的空间(3-2=1)
  • 当不选择将物品1放入背包时,此时背包的最大价值为二维表中[0][3]的值
public class KnapSack01 { 
    public static int knapSack(int[] w, int[] v, int C) { 
    int size = w.length; if (size == 0) { 
    return 0; } int[][] dp = new int[size][C + 1]; //初始化第一行 //仅考虑容量为C的背包放第0个物品的情况 for (int i = 0; i <= C; i++) { 
    dp[0][i] = w[0] <= i ? v[0] : 0; } //填充其他行和列 for (int i = 1; i < size; i++) { 
    for (int j = 0; j <= C; j++) { 
    dp[i][j] = dp[i - 1][j]; if (w[i] <= j) { 
    dp[i][j] = Math.max(dp[i][j], v[i] + dp[i - 1][j - w[i]]); } } } return dp[size - 1][C]; } public static void main(String[] args) { 
    int[] w = { 
   2, 1, 3, 2}; int[] v = { 
   12, 10, 20, 15}; System.out.println(knapSack(w, v, 5)); } } 

V4.0——动态规划空间复杂度的极致优化

上面的动态规划算法使用了 O ( n ∗ C ) O(n*C) O(nC)的空间复杂度(因为我们使用了二维数组来记录子问题的解)

其实我们完全可以只使用一维数组来存放结果,但同时我们需要注意的是,为了防止计算结果被覆盖,我们必须从后向前分别进行计算。为什么呢?往下看。

在这里插入图片描述

假设我们要计算 F ( i , 4 ) F(i,4) F(i,4),我们需要用到的值为 F ( i − 1 , 4 ) F(i-1,4) F(i1,4) F ( i − 1 , 4 − w ( i ) ) F(i-1,4-w(i)) F(i1,4w(i)),因此为了防止结果被覆盖,我们需要从后向前依次计算结果

经过V3.0版本的思考,你们画个图就能理解刚才的解释了,给出最终的动态规划代码

public class KnapSack01 { 
    public static int knapSack(int[] w, int[] v, int C) { 
    int size = w.length; if (size == 0) { 
    return 0; } int[] dp = new int[C + 1]; //初始化第一行 //仅考虑容量为C的背包放第0个物品的情况 for (int i = 0; i <= C; i++) { 
    dp[i] = w[0] <= i ? v[0] : 0; } for (int i = 1; i < size; i++) { 
    for (int j = C; j >= w[i]; j--) { 
    dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]); } } return dp[C]; } public static void main(String[] args) { 
    int[] w = { 
   2, 1, 3, 2}; int[] v = { 
   12, 10, 20, 15}; System.out.println(knapSack(w, v, 5)); } } 

利用背包问题的思想解决问题

leetcode 416 Partition Equal Subset Sum

给定一个仅包含正整数的非空数组,确定该数组是否可以分成两部分,要求两部分的和相等

问题分析

该问题我们可以利用背包问题的思想进行求解。

假设给定元素个数为 n n n的数组arr,数组元素的和为sum,对应于背包问题,等价于有 n n n个物品,每个物品的重量和价值均为为arr[i],背包的限重为sum/2,求解背包中的物品最大价值为多少?

class Solution { 
    private boolean knapSack(int[] nums,int sum){ 
    int size = nums.length; boolean[] dp = new boolean[sum + 1]; for (int i = 0;i <= sum;i ++){ 
    dp[i] = i == nums[0]; } for (int i = 1;i < size;i++){ 
    for (int j = sum;j >= nums[i];j--){ 
    dp[j] = dp[j] || dp[j-nums[i]]; } } return dp[sum]; } public boolean canPartition(int[] nums) { 
    int sum = 0; for (int item : nums){ 
    sum += item; } //如果数组元素和不是2的倍数,直接返回false if (sum % 2 != 0) return false; return knapSack(nums,sum/2); } } 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/198881.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月26日 下午2:18
下一篇 2026年3月26日 下午2:19


相关推荐

  • DOS中Copy命令合并文件[通俗易懂]

    DOS中Copy命令合并文件[通俗易懂]今天在查找DOS中合并文件的命令时,发现使用该命令还可以在有些情况下加密一些帐户信息,遂转。OriginalURL: http://hi.baidu.com/leland/item/a55f753f60a61480b611dbf0我们都知道DOS中Copy命令的主要作用是复制文件,它还有一个作用是合并文件。一般情况下,它主要用于合并相同类型的文件,比如将两个文本文件合并为一个文本

    2022年7月18日
    17
  • Java单例模式的9种实现方式

    Java单例模式的9种实现方式JAVA 单例模式的 9 种实现方式一 饿汉式二 懒汉式 线程不安全三 懒汉式 线程安全四 双检锁 双重校验锁 DCL 即 double checkedlocki 五 静态内部类六 枚举七 双检锁 双重校验锁 DCL 即 double checkedlocki 加 volatile 八 使用 ThreadLocal 实现单例九 使用 CAS 锁实现单例

    2026年3月19日
    2
  • 修改密码passwd鉴定令牌操作错误_命令行修改用户密码

    修改密码passwd鉴定令牌操作错误_命令行修改用户密码修改Linux下一个用户的密码,输入passwdfmuser,提示鉴定令牌操作错误:查看/etc/group/etc/passwd/etc/shadow文件权限输入:lsattr/etc/group/etc/passwd/etc/shadow设置i权限:chattr-i/etc/group/etc/passwd/etc/shadow然后再次查…

    2025年9月19日
    9
  • oracle恢复表数据

    oracle恢复表数据误删表或者deletefromXXX没有带条件清空表后不要慌,能恢复的,咱有flashbacktable咱怕啥只要删除的人没有加PURGE就好。oracle还是够抗造的一、删表恢复flashbacktabletablename_has_deletedtobeforedrop二、清表数据恢复1.确认一下数据对不对,是不是你想恢复的节点select*fromTABLENAME_DATA_CLEANEDasoftimestampto_timestamp(‘误操作的

    2026年2月23日
    3
  • Android开发ListView使用OnScrollListener实现分页加载数据

    Android开发ListView使用OnScrollListener实现分页加载数据

    2022年1月17日
    63
  • validation怎么用_什么是确认validation

    validation怎么用_什么是确认validation引入文件环境在jQuery下,所有先要引入jQuery1<%–校验样式–%>2<linkrel=”stylesheet”href=”<%=basePath%>css/validationEngine.jquery.css”>3<%–校验及自定义规则规则–%>4<scripttype=”t…

    2022年10月4日
    8

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号