【动态规划】详解 0-1背包问题

文章目录

  • 1. 问题引入
  • 2. 从 dfs 到动态规划
  • 3. 动态规划过程分析
  • 4. 二维 dp 的遍历顺序
  • 5. 从二维数组到一维数组
  • 6. 一维数组的遍历次序
  • 7. 背包的遍历顺序
  • 8. 代码总结
  • 9. 总结


1. 问题引入

0-1 背包是比较经典的动态规划问题,这里以代码随想录里面的例子来介绍下。总的来说就是:有 n 个物品和一个重量为 w 的背包,这 n 个物品中第 i 个物品的重量为 w[i],价值为 v[i],那么这个背包能装下的物品最大价值是多少,注意一个物品只能选一次。
在这里插入图片描述


2. 从 dfs 到动态规划

我们来把问题具体化,假设现在有一个重量为 4 的背包,有 3 个物品,物品的重量和价值如下:

重量价值
物品 0115
物品 1320
物品 2430

那么现在将物品装入背包,能装入的物品的最大价值是多少呢?要想解决动态规划问题,我们总得从 dfs 入手,那就先从 dfs 讲讲。

public class Main {public static void main(String[] args) {Main main = new Main();main.findMax(new int[]{1, 3, 4}, new int[]{15, 20, 30}, 4);main.findMax(new int[]{1, 3}, new int[]{15, 20}, 3);}public void findMax(int[] weight, int[] prices, int max){int res = dfs(weight, prices, max, weight.length - 1);System.out.println("最大价值是: " + res);}private int dfs(int[] weight, int[] prices, int max, int i) {if(i < 0){// 遍历到尾部了return 0;}// 不选当前的价值int noPick = dfs(weight, prices, max, i - 1);// 如果剩余重量大于 weight[i] 才可选return max >= weight[i] ? Math.max(prices[i] + dfs(weight, prices, max - weight[i], i - 1), noPick) : noPick;}
}

dfs 的遍历逻辑很简单,就是从后往前遍历,然后对于当前物品,可以选或者不选,但是有条件,如果选的话剩余的容量必须要大于 weight[i],否则就不能选,因为剩余重量装不下当前物品。

但是我们知道,这个方法是会超时的,如果数组长度比较大,因为里面有不少重复计算,既然这样我们就加上记忆化搜索

public class Main {public static void main(String[] args) {Main main = new Main();main.findMax(new int[]{1, 3, 4}, new int[]{15, 20, 30}, 4);main.findMax(new int[]{1, 5}, new int[]{15, 20}, 3);}public void findMax(int[] weight, int[] prices, int max){// memo[i][j] 的意思是从 [0...i] 中将物品放到重量为 j 的背包,最大值是多少int[][] memo = new int[weight.length][max + 1];for (int[] arr : memo) {Arrays.fill(arr, -1);}int res = dfs(weight, prices, max, weight.length - 1, memo);System.out.println("最大价值是: " + res);}private int dfs(int[] weight, int[] prices, int max, int i, int[][] memo) {if(i < 0){// 遍历到尾部了return 0;}if(memo[i][max] != -1){return memo[i][max];}// 不选当前的价值int noPick = dfs(weight, prices, max, i - 1, memo);return memo[i][max] = (max >= weight[i] ? Math.max(prices[i] + dfs(weight, prices, max - weight[i], i - 1, memo), noPick) : noPick);}
}

好了,在记忆化搜索的基础上,我们再来改造成动态规划,首先我们看上面的 dfs 逻辑,当前 i 的结果是基于 i - 1 得来的,也就是说我们可以得到下面的递推公式:

  • d p [ i ] [ j ] = M a t h . m a x ( d p [ i − 1 ] [ j ] , p r i c e s [ i ] + d p [ i − 1 ] [ j − w e i g h t [ i ] ] ) dp[i][j] = Math.max(dp[i-1][j], prices[i] + dp[i-1][j-weight[i]]) dp[i][j]=Math.max(dp[i1][j],prices[i]+dp[i1][jweight[i]])
  • 上面的意思是如果不选当前下标 i 的物品,那么就继续往前找
  • 如果选当前下标 i 的物品,那么价值就是 prices[i],接着 j 要减去物品 i 的价值

好了,递推公式有了,那么如何初始化呢?我们知道 dp[i][j] 的意思是:在下标 [0…i] 中选择物品装入重量为 j 的背包,能装入的最大值是多少!

  • 当 i = 0 的时候,dp[0][j] 表示下标 0 物品装入重量为 j 的背包,最大值是多少。
  • 当 j = 0 的时候,dp[i][0] 表示下标 [0…i] 的物品装入重量为 0 的背包,最大值是多少,自然是 0 了。

所以初始化如下:

int[][] dp = new int[weight.length][max + 1];
for(int j = 0; j <= max; j++){if(j > weight[0]){dp[0][j] = prices[i];}
}

下面再结合记忆化搜索的代码,就能写出来下面的动态规划代码。

public int findMaxD(int[] weight, int[] prices, int max){int[][] dp = new int[weight.length][max + 1];for(int j = 0; j <= max; j++){if(j >= weight[0]){dp[0][j] = prices[0];}}// 遍历物品for(int i = 1; i < weight.length; i++){// 遍历背包for(int j = 0; j <= max; j++){if(j < weight[i]){dp[i][j] = dp[i - 1][j];} else {dp[i][j] = Math.max(dp[i - 1][j], prices[i] + dp[i - 1][j - weight[i]]);}}}return dp[weight.length - 1][max];
}

3. 动态规划过程分析

上面我们写出了动态规划的代码,但是不知道大家有没有疑问,就是这个物品和背包的遍历是有顺序的吗?注意我这里指的是二维的 dp 数组,现在我们讨论都是在二维 dp 的基础上去讨论,后面会逐步演变成一维 dp。

首先就是递推公式

  • d p [ i ] [ j ] = M a t h . m a x ( d p [ i − 1 ] [ j ] , p r i c e s [ i ] + d p [ i − 1 ] [ j − w e i g h t [ i ] ] ) dp[i][j] = Math.max(dp[i-1][j], prices[i] + dp[i-1][j-weight[i]]) dp[i][j]=Math.max(dp[i1][j],prices[i]+dp[i1][jweight[i]]),根据这个递推公式,

通过这个递推公式,我们能发现 dp[i][j] 其实依赖当前格子的上边和左上的格子
在这里插入图片描述
那么从这个角度,我们再来看动态规划的初始化,你就知道为什么要初始化 i = 0 和 j = 0 的格子值了(j = 0 不需要初始化,因为都是 0)。
在这里插入图片描述
初始化完第一行之后,再从第二行开始通过递推公式填充格子,最终填充好的结果如下所示:
在这里插入图片描述
我用下标 (1,3)举个例子,当 i = 1,j = 3 的时候,如果不选当前物品,那么就是 dp[0][3],如果选当前物品,那么就是 dp[1 - 1][3 - 3] + 20 = 20,两者取最大值就是 20,遍历顺序是从左到右,从上到下


4. 二维 dp 的遍历顺序

好了,上面我们解析了 dp 数组的填充过程,那么如果是先遍历物品,再遍历背包,填充的过程就是 从左到右,从上到下。那么如果是先遍历背包,再遍历物品呢?

还是回到 dp 图,如果先遍历背包,再遍历物品,其实就是从从上到下,从左到右
在这里插入图片描述
那么我们想一下,如果是这种遍历顺序,在遍历到 dp[1][3] 的时候,dp[0][3] 和 dp[0][0] 初始化了吗,换句话说,当遍历到第 i 行的时候,第 i - 1 行初始化了吗?

从遍历过程就能看到,其实是初始化了的,所以我们先遍历背包,再遍历物品也是没问题的。如何遍历,遍历顺序是什么就取决于当遍历到(i,j)的时候,需要依赖的格子有没有初始化。

public int findMaxD(int[] weight, int[] prices, int max) {int[][] dp = new int[weight.length][max + 1];for (int j = 0; j <= max; j++) {if (j >= weight[0]) {dp[0][j] = prices[0];}}// 遍历背包for (int j = 0; j <= max; j++) {// 遍历物品for (int i = 1; i < weight.length; i++) {if (j < weight[i]) {dp[i][j] = dp[i - 1][j];} else {dp[i][j] = Math.max(dp[i - 1][j], prices[i] + dp[i - 1][j - weight[i]]);}}}return dp[weight.length - 1][max];
}

那么我再问一句,遍历背包能倒叙遍历吗?其实从 dp 数组的依赖就能看出,可以!!! 因为第 i 行的数据只和第 i - 1 行有关,和本行无关,而我们知道 dp 数组在处理到第 i 行的时候 i - 1就已经处理好了,所以爱怎么遍历就怎么遍历。


5. 从二维数组到一维数组

上面我们使用二维数组对 dp 进行填充了,但是大家有没有注意到,第 i 行的结果只依赖第 i - 1 行,所以我们完全可以只使用一维数组,把 i 省略掉。相当于把 i 的结果粘贴到 i - 1行的位置,所以 dp[i] 就表示重量为 i 的容量能装入的最大物品价值是多少 ,大体过程如下:

  • 初始化 dp[0]
  • 根据 dp[0] 初始化 dp[1]

初始化 dp[0] 的时候,重量为 0 的背包肯定是放不下任何物品的,所以不需要初始化,下面看遍历逻辑。

public int findMax(int[] weight, int[] prices, int max){int[] dp = new int[max + 1];// 遍历物品for(int i = 0; i < weight.length; i++){// 遍历背包for(int j = max; j >= weight[i]; j--){dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]);}}return dp[max];
}

注意下 dp 公式为:

  • d p [ j ] = M a t h . m a x ( d p [ j ] , d p [ j − w e i g h t [ i ] ] + p r i c e s [ i ] ) dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]) dp[j]=Math.max(dp[j],dp[jweight[i]]+prices[i])

大家可能好奇为什么可以直接和 dp[j] 做比较,我把二维数组的 dp 粘贴过来。
在这里插入图片描述
dp 数组初始化为 0,当 i = 0 的时候,其实就是在初始化第一行 [0,15,15,15,15]当 i = 1 的时候,记住此时 dp 记录的是 i = 0 的结果,那么 dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]) 其实就是在根据 i = 0 来更新 i = 1 的数据,一直这样遍历下去,遍历到最后就是我们要的结果了


6. 一维数组的遍历次序

上面一维数组的遍历次序是先遍历物品,再遍历背包,那么可以先遍历背包,再遍历物品吗?也就是下面的写法。

// 遍历背包
for (int j = max; j >= 0; j--) {// 遍历物品for (int i = 0; i < weight.length; i++) {if (j >= weight[i]) {dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]);}}
}

让我们看下这个过程,当 j = 4 的时候,遍历所有物品,求 dp[j] 能放下的物品的最大价值,为什么我说求 dp[j] 的最大价值,因为是倒叙遍历,同时又是 j 在外层一直往前遍历,所以 dp[j - weight[i]] 你就当成 0 就好了,相当于 dp[j] = Math.max(dp[j], prices[i])

所以最终求出来的结果就是当前这个重量下能放下的物品的最大价值(单个)。

所以这里的遍历顺序就得是:先遍历物品,再遍历背包


7. 背包的遍历顺序

public int findMax(int[] weight, int[] prices, int max){int[] dp = new int[max + 1];// 遍历物品for(int i = 0; i < weight.length; i++){// 遍历背包for(int j = max; j >= weight[i]; j--){dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]);}}return dp[max];
}

继续回到遍历逻辑,注意到背包是从后往前遍历的,那么为什么不能从前往后遍历呢?

我们仔细看下 dp 的意义,由于从二维降到一维,我们在遍历的时候是不断用新获取的 dp[j] 覆盖原来的 dp[j],还是从二维数组的 dp 看下。
在这里插入图片描述
我上面说的意思相当于说,现在一维 dp 的数组是物品 0 所在的这行数据 [0,15,15,15,15]。当 i = 1 的时候,求出来的 20 会立马覆盖回数组,这时候数组是 [0,15,15,20,15],j = 3,继续往前遍历。

而如果缓存背包从前往后遍历,结果会是怎么样呢?我把物品的重量和价格粘贴过来。

重量价值
物品 0115
物品 1320
物品 2430

这次我们就看 i = 0 的遍历结果,初始化数组全是 0。
在这里插入图片描述

  • dp[0] = 0
  • dp[1] = Math.max(dp[1], dp[1-weight[0]] + prices[0]) = 15
  • dp[2] = Math.max(dp[2], dp[2-weight[0]] + prices[0]) = 30
  • dp[3] = Math.max(dp[3], dp[3-weight[0]] + prices[0]) = 45

最终结果就变成了:
在这里插入图片描述
其实出现这种情况,就是完全背包的做法了,也就是一个物品被选择了多个。

那么为什么会出现这种情况呢?其实我们还是可以从一维 dp 入手。

  • d p [ j ] = M a t h . m a x ( d p [ j ] , d p [ j − w e i g h t [ i ] ] + p r i c e s [ i ] ) dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]) dp[j]=Math.max(dp[j],dp[jweight[i]]+prices[i])

上面是一维的公式,假设现在数字组初始化为 0,那么在初始化 i = 0 的时候假设初始化了 dp[1] = 15,大家记住,这里的 dp 是实时覆盖的,所以这时候的状态如下:
在这里插入图片描述
这时候 dp[0] 和 dp[1] 都计算过了并且覆盖会原数组下标,而 dp[2]、dp[3]、dp[4] 还保留初始化的状态,啥意思呢,换成 i - 1 和 i,意思就是这时候 dp[0] 和 dp[1] 是 i = 1 计算出来的,而 dp[2]、dp[3]、dp[4] 则还是 dp[i-1] 的状态

我们接下来继续看 dp[2],我们知道 dp[2] = Math.max(dp[2], dp[1] + 15) = 30,意思就是在计算 dp[2] 的时候使用到了 dp[1],而这个 dp[1] 已经被覆盖过了,意思就是这个 dp[1] 不是 i - 1 的值了,而是 i 的值,所以就造成了多次选择。

在二维数组中计算 dp[i] 的时候是使用 dp[i-1] 的值,因为不会被覆盖,所以遍历顺序就无所谓。但是一维数组就不一样的,因为会实时覆盖,所以只能从后往前遍历,否则就会用前面已经计算过的值来计算当前下标的值了。


8. 代码总结

好了,到这里我们就解析完0-1背包了,分为二维和一维,其实说了这么多,大家只需要记住两个版本就行了。

public int findMaxD(int[] weight, int[] prices, int max){int[][] dp = new int[weight.length][max + 1];for(int j = 0; j <= max; j++){if(j >= weight[0]){dp[0][j] = prices[0];}}// 遍历物品for(int i = 1; i < weight.length; i++){// 遍历背包for(int j = 0; j <= max; j++){if(j < weight[i]){dp[i][j] = dp[i - 1][j];} else {dp[i][j] = Math.max(dp[i - 1][j], prices[i] + dp[i - 1][j - weight[i]]);}}}return dp[weight.length - 1][max];
}

一维的遍历如下:

public int findMax(int[] weight, int[] prices, int max){int[] dp = new int[max + 1];// 遍历物品for(int i = 0; i < weight.length; i++){// 遍历背包for(int j = max; j >= weight[i]; j--){dp[j] = Math.max(dp[j], dp[j - weight[i]] + prices[i]);}}return dp[max];
}

9. 总结

我们总结下二维数组和一维数组的遍历顺序:

  • 二维数组

    • 背包和物品的遍历顺序可以颠倒
    • 遍历背包的时候可以正序和倒叙遍历
  • 一维数组

    • 先遍历物品,再遍历背包
    • 遍历背包需要倒叙遍历





如有错误,欢迎指出!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/70171.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode每日精进:20.有效的括号

题目链接&#xff1a;20.有效的括号 题目描述&#xff1a; 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以…

llama.cpp部署 DeepSeek-R1 模型

一、llama.cpp 介绍 使用纯 C/C推理 Meta 的LLaMA模型&#xff08;及其他模型&#xff09;。主要目标llama.cpp是在各种硬件&#xff08;本地和云端&#xff09;上以最少的设置和最先进的性能实现 LLM 推理。纯 C/C 实现&#xff0c;无任何依赖项Apple 芯片是一流的——通过 A…

Web后端 - Maven管理工具

一 Maven简单介绍 Maven是apache旗下的一个开源项目&#xff0c;是一款用于管理和构建java项目的工具。 Maven的作用 二 Maven 安装配置 依赖配置 依赖传递 依赖范围 生命周期 注意事项&#xff1a;在同一套生命周期中&#xff0c;当运行后面的阶段时&#xff0c;前面的阶段都…

[LeetCode力扣hot100]-C++常用数据结构

0.Vector 1.Set-常用滑动窗口 set<char> ans;//根据类型定义&#xff0c;像vector ans.count()//检查某个元素是否在set里&#xff0c;1在0不在 ans.insert();//插入元素 ans.erase()//删除某个指定元素 2.栈 3.树 树是一种特殊的数据结构&#xff0c;力扣二叉树相…

vite+vue3开发uni-app时低版本浏览器不支持es6语法的问题排坑笔记

重要提示&#xff1a;请首先完整阅读完文章内容后再操作&#xff0c;以免不必要的时间浪费&#xff01;切记&#xff01;&#xff01;&#xff01;在使用vitevue3开发uni-app项目时&#xff0c;存在低版本浏览器不兼容es6语法的问题&#xff0c;如“?.” “??” 等。为了方便…

《计算机视觉》——角点检测和特征提取sift

角点检测 角点的定义&#xff1a; 从直观上理解&#xff0c;角点是图像中两条或多条边缘的交点&#xff0c;在图像中表现为局部区域内的灰度变化较为剧烈的点。在数学和计算机视觉中&#xff0c;角点可以被定义为在两个或多个方向上具有显著变化的点。比如在一幅建筑物的图像…

WWW 2025 | 中南、微软提出端到端双重动态推荐模型,释放LLM在序列推荐中的潜力...

©PaperWeekly 原创 作者 | 殷珺 单位 | 中南大学硕士研究生 研究方向 | 大语言模型、推荐系统 论文题目&#xff1a; Unleash LLMs Potential for Sequential Recommendation by Coordinating Dual Dynamic Index Mechanism 论文链接&#xff1a; https://openreview.net…

c# 2025/2/17 周一

16. 《表达式&#xff0c;语句详解4》 20 未完。。 表达式&#xff0c;语句详解_4_哔哩哔哩_bilibili

数据结构与算法面试专题——堆排序

完全二叉树 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 完全二叉树中如果每棵子树的最小值都在顶部就是小根堆 设计目标&#xff1a;完全二叉树的设计目标是高效地利用存储空间&#xff0c;同时便于进行层次遍历和数组存储。它的结构使得每个节点的子节点都可以通过简…

iOS开发书籍推荐 - 《高性能 iOS应用开发》(附带链接)

引言 在 iOS 开发的过程中&#xff0c;随着应用功能的增加和用户需求的提升&#xff0c;性能优化成为了不可忽视的一环。尤其是面对复杂的界面、庞大的数据处理以及不断增加的后台操作&#xff0c;如何确保应用的流畅性和响应速度&#xff0c;成为开发者的一大挑战。《高性能 …

微信小程序的制作

制作微信小程序的过程大致可以分为几个步骤&#xff1a;从环境搭建、项目创建&#xff0c;到开发、调试和发布。下面我会为你简要介绍每个步骤。 1. 准备工作 在开始开发微信小程序之前&#xff0c;你需要确保你已经完成了以下几个步骤&#xff1a; 注册微信小程序账号&…

LabVIEW 中dde.llbDDE 通信功能

在 LabVIEW 功能体系中&#xff0c;位于 C:\Program Files (x86)\National Instruments\LabVIEW 2019\vi.lib\Platform\dde.llb 的 dde.llb 库占据着重要的地位。作为一个与动态数据交换&#xff08;DDE&#xff09;紧密相关的库文件&#xff0c;它为 LabVIEW 用户提供了与其他…

gitte远程仓库修改后,本地没有更新,本地与远程仓库不一致

问题 &#xff1a;gitte远程仓库修改后&#xff0c;本地没有更新&#xff0c;本地与远程仓库不一致 现象&#xff1a; [cxqiZwz9fjj2ssnshikw14avaZ rpc]$ git push Username for https://gitee.com: beihangya Password for https://beihangyagitee.com: To https://gitee.c…

组合模式详解(Java)

一、组合模式基本概念 1.1 定义与类型 组合模式是一种结构型设计模式,它通过将对象组织成树形结构,来表示“部分-整体”的层次关系。这种模式使得客户端可以一致地对待单个对象和组合对象,从而简化了客户端代码的复杂性。组合模式的核心在于定义了一个抽象组件角色,这个角…

LabVIEW危化品仓库的安全监测系统

本案例展示了基于LabVIEW平台设计的危化品仓库安全监测系统&#xff0c;结合ZigBee无线通信技术、485串口通讯技术和传感器技术&#xff0c;实现了对危化品仓库的实时无线监测。该系统不仅能提高安全性&#xff0c;还能大幅提升工作效率&#xff0c;确保危化品仓库的安全运营。…

【私人笔记】Web前端

Vue专题 vue3 vue3 页面路径前面添加目录 - 路由base设置 - vite设置base https://mbd.baidu.com/ma/s/XdDrePju 修改vite.config.js export default defineConfig({base: /your-directory/,// 其他配置... }); vue2 uniapp 【持续更新】uni-app学习笔记_uniapp快速复制一…

数仓搭建:DWB层(基础数据层)

维度退化: 通过减少表的数量和提高数据的冗余来优化查询性能。 在维度退化中&#xff0c;相关的维度数据被合并到一个宽表中&#xff0c;减少了查询时需要进行的表连接操作。例如&#xff0c;在销售数据仓库中&#xff0c;客户信息、产品信息和时间信息等维度可能会被合并到一…

【Linux】进程间通信——进程池

文章目录 进程池什么进程池进程池的作用 用代码模拟进程池管道信息任务类InitProcesspool()DisPatchTasks()任务的执行逻辑&#xff08;Work&#xff09;CleanProcessPool() 封装main.ccChannel.hppProcessPool.hppTask.hppMakefile 总结总结 进程池 什么进程池 进程池&#…

13-跳跃游戏 II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最…

Qt的QToolBox的使用

QToolBox 是 Qt 框架中的一个控件&#xff0c;用于创建一个可折叠的“工具箱”界面&#xff08;类似 Windows 资源管理器的侧边栏&#xff09;。每个子项可以展开或折叠&#xff0c;适合用于分组显示多个功能模块。以下是其基本用法和示例&#xff1a; 1. 基本用法 创建并添加…