热门标签 | 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];}
}


推荐阅读
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • Java 类成员初始化顺序与数组创建
    本文探讨了Java中类成员的初始化顺序、静态引入、可变参数以及finalize方法的应用。通过具体的代码示例,详细解释了这些概念及其在实际编程中的使用。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 探讨如何通过编程技术实现100个并发连接,解决线程创建顺序问题,并提供高效的并发测试方案。 ... [详细]
  • Java 中 Writer flush()方法,示例 ... [详细]
  • 本文介绍了如何使用 Spring Boot DevTools 实现应用程序在开发过程中自动重启。这一特性显著提高了开发效率,特别是在集成开发环境(IDE)中工作时,能够提供快速的反馈循环。默认情况下,DevTools 会监控类路径上的文件变化,并根据需要触发应用重启。 ... [详细]
  • Java 中的 BigDecimal pow()方法,示例 ... [详细]
  • 本文介绍如何利用动态规划算法解决经典的0-1背包问题。通过具体实例和代码实现,详细解释了在给定容量的背包中选择若干物品以最大化总价值的过程。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ... [详细]
  • 在前两篇文章中,我们探讨了 ControllerDescriptor 和 ActionDescriptor 这两个描述对象,分别对应控制器和操作方法。本文将基于 MVC3 源码进一步分析 ParameterDescriptor,即用于描述 Action 方法参数的对象,并详细介绍其工作原理。 ... [详细]
  • 本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ... [详细]
  • 本文介绍了如何在C#中启动一个应用程序,并通过枚举窗口来获取其主窗口句柄。当使用Process类启动程序时,我们通常只能获得进程的句柄,而主窗口句柄可能为0。因此,我们需要使用API函数和回调机制来准确获取主窗口句柄。 ... [详细]
  • 本文探讨了如何在给定整数N的情况下,找到两个不同的整数a和b,使得它们的和最大,并且满足特定的数学条件。 ... [详细]
  • 本文详细介绍了 Apache Jena 库中的 Txn.executeWrite 方法,通过多个实际代码示例展示了其在不同场景下的应用,帮助开发者更好地理解和使用该方法。 ... [详细]
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社区 版权所有