代码随想录 刷题记录-19 动态规划(3)完全背包理论、习题

本文主要是介绍代码随想录 刷题记录-19 动态规划(3)完全背包理论、习题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、完全背包理论

52. 携带研究材料

有N种物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

首先从二维dp上来讲:

1.dp数组及下标含义:

        dp[i][j] : 考虑前 i种物品,背包容量为j时所能装入的最大价值

2.递推公式

        dp[i][j] = Math.max( dp[i-1][j] , dp[i][j-weight[i]] + value[i] )

注意这里与01背包不同的是,每种物品可以取无限多次,公式右边的第一项代表不考虑物品i的情况,公式右边第二项代表考虑物品i 1个或多个的情况,由于是依赖于本行的值,当第一次进入时如果选择右边就是拿了1个,第二次在j增加后还选择右边,依赖的左边的本行值已经更新过了,所以再次选择右边就包括了多个的情况,即右边这一项是物品无限件的体现,它的核心在于使用本行的值而不是上一行的值进行更新。

3.初始化

        根据递推公式,计算时依赖于上一行和本行左边,对物品为0的第一行进行初始化,当 j >= weight[0] 时, dp[0][j] = dp[0][j-weight[0]] +value[0]

4.遍历顺序

        先物品后容量 或者 先容量或物品都可以,这里先物品后容量

        正序遍历,因为需要用到本行左边已经计算的值。

5.dp模拟

代码如下:

import java.util.Scanner;public class Main{public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();int capacity = scanner.nextInt();int[] weight = new int[n];int[] value = new int[n];for(int i = 0; i < n ; i++){weight[i] = scanner.nextInt();value[i] = scanner.nextInt();}int[][] dp = new int[n][capacity+1];for(int j = weight[0]; j <= capacity ; j++){dp[0][j] = dp[0][j-weight[0]] + value[0];}for(int i = 1 ; i < n ; i++){for(int j = 0; j <= capacity ; j++){if(j < weight[i]) dp[i][j] = dp[i-1][j];else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);}}System.out.println(dp[n-1][capacity]);}
}

接下来考虑使用滚动数组的情况:

01背包和完全背包唯一不同就是体现在遍历顺序上,这里直接针对遍历顺序进行分析。

首先再回顾一下01背包的核心代码

for(int i = 0; i < weight.size(); i++) { // 遍历物品for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}

即完全背包恰好利用了01背包如果正序遍历则会覆盖的特点,因为完全背包需要的数据是本行的左边,这要求左边必须计算过,所以要中序遍历。

也是因为完全背包对背包容量是正序遍历的,这也就导致了他的物品循环和背包容量循环的嵌套顺序是可以互换的(在二维里相当于从左到右一列一列的,所需要的数据在本行左边和上边,已经计算过了)

代码如下:

import java.util.Scanner;public class Main{public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();int capacity = scanner.nextInt();int[] weight = new int[n];int[] value = new int[n];for(int i = 0; i < n ; i++){weight[i] = scanner.nextInt();value[i] = scanner.nextInt();}int[] dp = new int[capacity+1];for(int i = 0 ; i < n ; i++){for(int j = weight[i]; j <= capacity ; j++){dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i]);}}System.out.println(dp[capacity]);}
}

二、习题

1.518.零钱兑换II

回溯法,但是会超时:

class Solution {int[] coins;int sum = 0;int amount;int result = 0;public int change(int amount, int[] coins) {this.coins = coins;this.amount = amount;dfs(0);return result;}public void dfs(int startIndex){if(sum == amount){result++;return;}if(sum > amount){return ;}for(int i = startIndex ; i < coins.length ; i++){sum += coins[i];dfs(i);sum -= coins[i];}}
}

考虑动态规划:

每个钱币能选择的数量不限,属于完全背包问题。

本题和纯完全背包不一样,纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数。

注意这里属于组合数,而不是排列数。

动规五步曲:

1.dp数组及下标含义

dp[j] : 当前遍历的前i种钱币中,容量为j下有 dp[j] 种方案凑满。

2.递推公式

dp[j]  += dp[j - coins[i]]

这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在这篇494. 目标和 中就讲解了,求装满背包有几种方法,公式都是:dp[j] += dp[j - nums[i]];

3.初始化

dp[0] = 1;

首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。

那么 dp[0] = 1 有没有含义,其实既可以说 凑成总金额0的货币组合数为1,也可以说 凑成总金额0的货币组合数为0,好像都没有毛病。

但题目描述中,也没明确说 amount = 0 的情况,结果应该是多少。

这里我认为题目描述还是要说明一下,因为后台测试数据是默认,amount = 0 的情况,组合数为1的。

下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j]

dp[0]=1还说明了一种情况:如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。

4.遍历顺序

外层是钱币种类。

内层是容量。

因为可选是无限次,所以正序。

在滚动数组下,必须外层是钱币种类,内层是容量,这样求出来的是组合数,反之求出的是排列数:

or

or

在二维dp数组下,遍历的内外层顺序可以交换。

5.dp模拟

代码如下:

public int change(int amount, int[] coins) {int[] dp = new int[amount+1];dp[0] = 1;for(int i = 0 ; i < coins.length ; i++){for(int j = coins[i] ; j <= amount ; j++){dp[j] += dp[j - coins[i]];}}return dp[amount];}

2.377. 组合总和 Ⅳ

本题是完全背包+排列数量和 问题,本题题目要求组合数,但是指出顺序不同的是不同组合,其实就是求排列数。

在学习回溯算法专题的时候,一定做过这两道题目回溯算法:39.组合总和 和回溯算法:40.组合总和II会感觉这两题和本题很像。

但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。

如果本题要把排列都列出来的话,只能使用回溯算法爆搜

动规五部曲分析如下:

1.dp数组及下标含义:

dp[j] : 凑成正整数为j的排列个数 为 dp[j]

2.确定递推公式

dp[j] += dp[j - nums[i]]

3.初始化

dp[0] = 1

4.遍历顺序

因为是求排列数,所以先遍历容量,再遍历物品,这样排列不同的算作不同。

由于每个物品可以用无限次,对容量正序遍历。

5.dp模拟

3.57. 爬楼梯

这其实是一个完全背包问题。

1阶,2阶,.... m阶就是物品,楼顶就是背包。

每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。

问跳到楼顶有几种方法其实就是问装满背包有几种方法。

动规五部曲:

1.dp数组及下标含义

dp[j] : 到第j层有 dp[j] 种方法

2.递推公式

dp[j] += dp[j-i]   i从1到m进行遍历

3.初始化

dp[0] = 1;

4.遍历顺序

先跨1层后跨2层 与 先跨2层后跨1层 是不一样的方案,所以求的是排列数,外层容量,内层物品。

完全背包,正序遍历。

5.dp模拟

3.322. 零钱兑换

完全背包问题

动规五部曲:

1.dp数组及下标含义:

dp[j] :凑足金额j所需要的最少的硬币个数为 dp[j] 个

2.递推公式

dp[j] = Math.min(dp[j] , dp[j-i] +1)

3.初始化

凑足金额0所需要的硬币个数为0,所以dp[0] = 0。

考虑到递推公式取最小值,初始化的值必须不影响取最小的操作,所以除了dp[0]以外初始化成Interger.MAX_VALUE,否则就有可能在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。

4.遍历顺序

本题并不强调集合是组合还是排列。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

在动态规划专题我们讲过了求组合数是动态规划:518.零钱兑换II (opens new window),求排列数是动态规划:377. 组合总和 Ⅳ (opens new window)。

所以本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!

本题是背包问题,背包容量遍历是正序。

5.dp模拟

以输入:coins = [1, 2, 5], amount = 5为例

322.零钱兑换

dp[amount]为最终结果。

代码如下:

(注意在递推时要排除初始值的影响)

dp[j] = Math.mim(dp[j], dp[j-coins[i] +1 ) ,如果不判断是否是Integer.MAX_VALUE的话,dp[j-coins[i]] +1可能移除,为负值,影响递推结果。

for(int i = 0 ; i < coins.length ; i++){for(int j = coins[i] ; j <= amount ; j++){if(dp[j - coins[i]]!=Integer.MAX_VALUE)dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);}}
class Solution {public int coinChange(int[] coins, int amount) {int[] dp = new int[amount+1];dp[0] = 0;for(int i = 1 ; i < dp.length ; i++){dp[i] = Integer.MAX_VALUE;}for(int i = 0 ; i < coins.length ; i++){for(int j = coins[i] ; j <= amount ; j++){if(dp[j - coins[i]]!=Integer.MAX_VALUE)dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);}}return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount] ;}
}

4.279.完全平方数

完全背包问题

1.dp数组及下标含义

dp[j]:组成数j最少需要 dp[j]个完全平方数

2.递推公式

dp[j] = Math.min(dp[j] , dp[j - nums[i]] + 1)

3.初始化

dp[0] = 0;

dp的其他元素初始化成Integer.MAX_VALUE

4.遍历顺序

外层物品,内层背包容量,可以交换。

完全背包,容量正序遍历。

5.dp模拟

已输入n为5例,dp状态图如下:

279.完全平方数

代码如下:

class Solution {public int numSquares(int n) {int[] dp = new int[n+1];dp[0] = 0;for(int i = 1 ; i <=n  ; i++){dp[i] = Integer.MAX_VALUE;}for(int i = 0; i*i <= n ; i++){for(int j = i*i ; j <= n ; j++){if(dp[j - i*i] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j], dp[j-i*i]+1);}}return dp[n];}
}

总结

本周的主题其实就是背包问题中的遍历顺序!

我这里做一下总结:

求组合数:动态规划:518.零钱兑换II (opens new window)求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包) (opens new window)求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)

此时我们就已经把完全背包的遍历顺序研究的透透的了!

5.139.单词拆分

可以使用回溯法,会时间超限

考虑动态规划

1.dp数组及下标含义:

dp[i] : dp[i] 为true 表示前i个字符组成的字符串可以分割成字典中字符串。

2.递推公式

如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

for(int j = 0; j < i ; j++){if(dp[j] && check(j,i)) dp[i] = true;
}

3.初始化

从递推公式看出,dp[i] 依赖于 在dp数组中其前面的元素。

故dp[0] 必须为 true

dp[0] = true;

dp[0]有没有意义呢?

dp[0]表示如果字符串为空的话,说明出现在字典里。

但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。

下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

4.遍历顺序

字典中的字符串可以使用多次,属于完全背包,正序遍历。

本题对顺序有要求,属于排列,需要外层遍历物品,内层遍历背包。

5.dp模拟

class Solution {public boolean wordBreak(String s, List<String> wordDict) {Set<String> set = new HashSet<>(wordDict);boolean[] dp = new boolean[s.length()+1];dp[0] = true;for(int i = 1; i <= s.length() ; i++){for(int j = 0;  j < i ; j++){if(dp[j] && set.contains(s.substring(j,i))) dp[i] = true;}}return dp[s.length()];}
}

这篇关于代码随想录 刷题记录-19 动态规划(3)完全背包理论、习题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1118138

相关文章

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例

Java使用POI-TL和JFreeChart动态生成Word报告

《Java使用POI-TL和JFreeChart动态生成Word报告》本文介绍了使用POI-TL和JFreeChart生成包含动态数据和图表的Word报告的方法,并分享了实际开发中的踩坑经验,通过代码... 目录前言一、需求背景二、方案分析三、 POI-TL + JFreeChart 实现3.1 Maven