热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

0/1背包问题(动态规划+动规优化)

题目地址动态规划(二维数组)dp优化(一维数组)根据dp数组获取选择的物品编号动态规划(二维数组)

题目地址

  • 动态规划(二维数组)
  • dp优化(一维数组)
  • 根据dp数组获取选择的物品编号


动态规划(二维数组)

大致描述就是有 N 个物品,每件物品有各自的重量 W 及价值 V,有一个最大容量为 C 的背包,求在不超过这个背包的容量下能装入物品的最大价值。(N,W,V,C都为整数)

假如对这些物品从1~i 进行编号,并用两个数组V、W分别保存物品的价值和重量,相应的 V[ i ] 和 W[ i ] 表示的意义是第 i 件物品的价值和重量。

首先定义F[ i ][ j ] = 在背包容量为 j 的情况下从前 i 件物品中能选取到的最大价值。这个定义的理解非常重要,它表示的是不同限制情况下的最优解。注意此处的 j 并不是题中描述的最大容量C,而是可以取从0~C的所有整数。

那么根据这个定义可以得到状态转移方程:F[ i ] [ j ] = max( F[ i-1 ] [ j ],F[ i-1 ] [ j-W[ i ] ] + V[ i ])

这个方程表示的意义是,在背包容量 j 的情况下对于物品 i :

  • 如果W[ i ]>j,物品重量超出背包容量肯定不能选,那么从前 i 个物品中装入等价于从前 i-1 个物品中装入,
    对应的状态转移方程 F[ i ] [ j ] = F[ i-1 ] [ j ] 。
  • 如果W[ i ]≤j,假设选入,注意这里是假设,那么有 F[ i ] [ j ] = F[ i-1 ] [ j-W[ i ] ] + V[ i ] ,也就是在背包总容量减去物品 i 重量后的基础上,前 i-1 件物品中的最优解+物品 i 的价值。

注意上面的 j 均表示的是背包总容量,而不是减去某物品重量后剩余的。

现在回到大方程 F[ i ] [ j ] = max( F[ i-1 ] [ j ],F[ i-1 ] [ j-W[ i ] ] + V[ i ]),它是在上面两种情况下取最大值,这也是为什么上面强调假设的原因,它表明的意义是即使物品 i 重量不大于背包总容量,也并不一定是非选它不可, 具体还要看能不能从前 i-1 件中选出更大的价值。

从大方程可以看出,背包容量 j 下前 i 件物品中的最优解,只和背包容量 j 下前 i-1 件物品中的最优解和背包容量减去物品 i 重量下前 i-1 件物品中的最优解有关。因此,可以通过循环迭代将不同情况限制下的最优解填写成一张表,类似下图,并可以根据这张表直接得到不同限制情况下的最优解。
在这里插入图片描述
物品编号和背包容量都含0是为了编程方便,并根据表示的实际意义,物品编号为0或背包容量为0时的最大价值应该设为0。

同样下面代码中W,V数组传入时也要将第一位先设为0。比如上图对应的W数组:{0,7,5,4,8,12},V数组:{0,13,8,20,11,6},但这里设0只是起到了占位的作用,也可以设为其它数字。

public class Knapsack_01 {public static int knapsack(int[] W, int[] V, int C) {// 创建二维数组用来保存不同限制条件下的最优解// 注意dp[0][],dp[][0]虽然没有显式初始化0但由于是int数组,默认都为0int[][] dp &#61; new int[W.length][C &#43; 1];for (int i &#61; 1; i < W.length; i&#43;&#43;) {for (int j &#61; 1; j < C &#43; 1; j&#43;&#43;) {if (W[i] > j) {dp[i][j] &#61; dp[i - 1][j];} else {dp[i][j] &#61; Math.max(dp[i - 1][j], dp[i - 1][j - W[i]] &#43; V[i]);}}}// 数组最右下角就是最大价值return dp[W.length - 1][C];}public static void main(String[] args) {int[] W &#61; { 0, 7, 5, 4, 8, 12 };int[] V &#61; { 0, 13, 8, 20, 11, 6 };System.out.println(knapsack(W, V, 20));}
}

设物品个数 n&#xff0c;背包最大容量 m&#xff0c;因双层循环所以上面代码的时间复杂度O(n∗m)Ο(n*m)O(nm)&#xff0c;空间复杂度即二维数组的空间也是O(n∗m)Ο(n*m)O(nm)

dp优化&#xff08;一维数组&#xff09;

优化主要是对二维数组的优化&#xff0c;从上面的状态转移方程可以看出&#xff0c;下一行的数据只与上一行的数据有关&#xff0c;更确切的说&#xff0c; 任意 F[ i ] [ j ] 的结果只与它上一行左边的 F[ i-1 ] [ 0 ] ~ F[ i-1 ] [ j ] 这一部分区间数据相关。利用这一特点可以将二维数组优化为一维数组。

优化过程网上看了很多都没有讲的太详细&#xff0c;理解了好久~

如果上面的dp数组是保留2行的话&#xff0c;相信很多人都知道怎么优化&#xff0c;可以通过滚动数组来解决。其实2行1行道理都差不多&#xff0c;上面说了&#xff0c;对于任意 F[ i ] [ j ] 的结果只与它上一行左边的这一部分区间数据相关&#xff0c;还是这张图
在这里插入图片描述
假设以物品序号和背包容量为坐标描述的话&#xff0c;(1,20)位置的值13只与(0,0)~(0,20)处的数据有关&#xff0c;对于第一行的数据填写时&#xff0c;从右往左填写&#xff0c;而得到的值并不是直接写在(1,20)的位置上&#xff0c;而是写在上一行相同的位置(0,20)&#xff0c;为什么可以这么写&#xff1f;因为下次确定(1,19)处的值的时候已经用不上这个位置的数据了。

因此&#xff0c;虽然只有一行数据&#xff0c;但却包括了逻辑上的两行&#xff0c;每次所填写的位置左边的数据都属于上一行的数据&#xff0c;填写位置及右边的数据都属于下一行&#xff0c;理解了这&#xff0c;就能明白为什么不能从左往右填写&#xff1a;因为第二行新的数据会覆盖第一行可能后面还要参与计算的数据。

这样从右往左填写一遍便可以得到第一行的所有数据&#xff0c;类似的得到第二行&#xff0c;第3行…&#xff0c;直到最后一行&#xff0c;而事实上需要的也仅是最后一行的最后一个位置上的数字。

优化后新的状态转移方程&#xff1a;F[ j ] &#61; max( F[ j ] ,F[ j-W[ i ] ] &#43; V[ i ] )&#xff0c;
方程中括号外面的 F[ j ] 相当于原方程中的 F[ i ] [ j ]&#xff0c;
括号里面的 F[ j ] 相当于原方程中的 F[ i-1 ] [ j ]&#xff0c;
括号里面的 F[ j-W[ i ] ] &#43; V[ i ] 相当于原方程中的 F[ i-1 ] [ j-W[ i ] ] &#43; V[ i ]。
对比原新方程可以发现&#xff0c;区别就是少了表示行数的 i 。

优化后的代码

public class Knapsack_01 {public static int knapsack(int[] W, int[] V, int C) {int[] dp &#61; new int[C &#43; 1];for (int i &#61; 1; i < W.length; i&#43;&#43;) {for (int j &#61; C; j >&#61; 1; j--) {if (W[i] < j)dp[j] &#61; Math.max(dp[j], dp[j - W[i]] &#43; V[i]);}}return dp[C];}public static void main(String[] args) {int[] W &#61; { 0, 7, 5, 4, 8, 12 };int[] V &#61; { 0, 13, 8, 20, 11, 6 };System.out.println(knapsack(W, V, 20));}
}

优化后的时间复杂度不变还是O(n∗m)Ο(n*m)O(nm)&#xff0c;而空间复杂度减小到了O(m)Ο(m)O(m)

根据dp数组获取选择的物品编号

将没有优化过的状态转移方程反向运用

int j &#61; C;
for (int i &#61; W.length - 1; i >&#61; 1; i--) {if (dp[i][j] &#61;&#61; dp[i - 1][j - W[i]] &#43; V[i]) {System.out.print("物品" &#43; i &#43; " ");j &#61; j - W[i];}
}


推荐阅读
  • 深入解析for与foreach遍历集合时的性能差异
    本文将详细探讨for循环和foreach(迭代器)在遍历集合时的性能差异,并通过实际代码示例和源码分析,帮助读者理解这两种遍历方式的不同之处。文章内容丰富且专业,旨在为编程爱好者提供有价值的参考。 ... [详细]
  • JavaScript 基础语法指南
    本文详细介绍了 JavaScript 的基础语法,包括变量、数据类型、运算符、语句和函数等内容,旨在为初学者提供全面的入门指导。 ... [详细]
  • 异常要理解Java异常处理是如何工作的,需要掌握一下三种异常类型:检查性异常:最具代表性的检查性异常是用户错误或问题引起的异常ÿ ... [详细]
  • 本文将探讨Java编程语言中对象和类的核心概念,帮助读者更好地理解和应用面向对象编程的思想。通过实际例子和代码演示,我们将揭示如何在Java中定义、创建和使用对象。 ... [详细]
  • 深入解析Java枚举及其高级特性
    本文详细介绍了Java枚举的概念、语法、使用规则和应用场景,并探讨了其在实际编程中的高级应用。所有相关内容已收录于GitHub仓库[JavaLearningmanual](https://github.com/Ziphtracks/JavaLearningmanual),欢迎Star并持续关注。 ... [详细]
  • 本题来自WC2014,题目编号为BZOJ3435、洛谷P3920和UOJ55。该问题描述了一棵不断生长的带权树及其节点上小精灵之间的友谊关系,要求实时计算每次新增节点后树上所有可能的朋友对数。 ... [详细]
  • 实用正则表达式有哪些
    小编给大家分享一下实用正则表达式有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下 ... [详细]
  • 本文探讨了在Java中如何正确地将多个不同的数组插入到ArrayList中,避免所有数组在插入后变得相同的问题。我们将分析代码中的问题,并提供解决方案。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 深入解析Redis内存对象模型
    本文详细介绍了Redis内存对象模型的关键知识点,包括内存统计、内存分配、数据存储细节及优化策略。通过实际案例和专业分析,帮助读者全面理解Redis内存管理机制。 ... [详细]
  • 深入理解Lucene搜索机制
    本文旨在帮助读者全面掌握Lucene搜索的编写步骤、核心API及其应用。通过详细解析Lucene的基本查询和查询解析器的使用方法,结合架构图和代码示例,带领读者深入了解Lucene搜索的工作流程。 ... [详细]
  • Java 实现二维极点算法
    本文介绍了一种使用 Java 编程语言实现的二维极点算法。该算法用于从一组二维坐标中筛选出极点,适用于需要处理几何图形和空间数据的应用场景。文章不仅详细解释了算法的工作原理,还提供了完整的代码示例。 ... [详细]
  • 本文深入探讨了SQL数据库中常见的面试问题,包括如何获取自增字段的当前值、防止SQL注入的方法、游标的作用与使用、索引的形式及其优缺点,以及事务和存储过程的概念。通过详细的解答和示例,帮助读者更好地理解和应对这些技术问题。 ... [详细]
  • ------------------------------————————————————————————————1.定义一个类,实现与被增强对象相同的接口2.在类中定义一个对象,记住被增强 ... [详细]
  • 本文介绍如何从字符串中移除大写、小写、特殊、数字和非数字字符,并提供了多种编程语言的实现示例。 ... [详细]
author-avatar
平凡洗护店
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有