Day37-动态规划,leetcode518,377,70
完全背包问题
- 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大
- 纯完全背包问题,求装满这个背包它的最大价值是多少?或者问能不能装满这个背包?那么两层for循环怎么颠倒都可以
- 完全背包在不同场景下问装满这个背包有多少种方法时,需要区分是求组合数还是排列数,组合数,不强调集合里的元素顺序,则先遍历物品再遍历背包,排列数是强调集合里的元素顺序,则先遍历背包再遍历物品
- 携带研究材料(第七期模拟笔试)
-
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。
-
小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
-
输入描述:第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量 ,接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
-
输出描述:输出一个整数,表示最大价值。
- 思路
- 完全背包问题:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
- 完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
- 01背包问题,使用一维数组时,外层先遍历物品,内层再遍历背包,若内层背包是正序遍历,则每个物品可以使用多次,若内层背包倒序遍历,则每个物品只能使用一次。
- 完全背包问题,纯完全背包,先遍历物品还是先遍历背包都可以,先遍历物品推导是从左到右,从上到下的顺序推导dp数组,先遍历背包推导是从上到下,从左到右的顺序推导dp数组,
- 1.确定dp数组定义及下标的含义:dp[i][j] 表示:只考虑前 i 种物品,背包容量为 j 时的最大价值。
- 2.确定递推公式:不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。递推公式: dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
- 3.dp数组如何初始化:从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。当j >= weight[0]时,dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个。
- 4.确定遍历顺序:01背包二维DP数组,先遍历物品还是先遍历背包都是可以的。因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。这里也一样,先遍历物品再遍历背包,还是先遍历背包再遍历物品都可以。
- 5.举例推导dp数组,打印dp数组:
/**
* 完全背包问题(二维dp版)
* n: 物品种类数
* bagWeight: 背包最大承重
* weight: 每种物品的重量数组
* value: 每种物品的价值数组
* 返回最大价值
*/
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
let input = [];
// 使用 readline 逐行读取输入。
readline.on('line', (line) => {
input.push(line.trim());
});
readline.on('close', () => {
// 第一行解析 第一行输入 n bagweight,分别表示物品种类数和背包最大承重。
const [n, bagweight] = input[0].split(' ').map(Number);
// 接下来 n 行,每行两个数,分别是每种物品的重量和价值,存入 weight 和 value 数组。
/// 剩余 n 行解析重量和价值
const weight = [];
const value = [];
for (let i = 1; i <= n; i++) {
const [wi, vi] = input[i].split(' ').map(Number);
weight.push(wi);
value.push(vi);
}
// dp[i][j]:前i种物品,容量为j时的最大价值
let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0));
// 初始化:第一种物品可以选多次
for (let j = weight[0]; j <= bagweight; j++) {
dp[0][j] = dp[0][j-weight[0]] + value[0];
}
// 状态转移
for (let i = 1; i < n; i++) {// 遍历物品
for (let j = 0; j <= bagweight; 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]);
}
}
}
console.log(dp[n - 1][bagweight]);
});
- 零钱兑换 II
- 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
- 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
- 假设每一种面额的硬币有无限个。
- 题目数据保证结果符合 32 位带符号整数。
- 思路
- 二维数组dp
- 1.确定dp数组定义及下标的含义:定义二维dp数值 dp[i][j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。
- 2.确定递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]
- 3.dp数组如何初始化:dp[0][j] = 1,如果 j 可以整除 物品0,那么装满背包就有1种组合方法。dp[i][0]=1,用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。有一种方法,即不装。
- 4.确定遍历顺序:
- 5.举例推导dp数组,打印dp数组:
const change = (amount, coins) => {
const bagSize = amount;
const n = coins.length;
// dp[i][j]: 用前i种硬币,凑成金额j的组合数
const dp = Array.from({ length: n }, () => Array(bagSize + 1).fill(0));
// 初始化最上行:只用coins[0],能整除才有1种方法
for (let j = 0; j <= bagSize; j++) {
if (j % coins[0] === 0) dp[0][j] = 1;
}
// 初始化最左列:金额为0,只有1种方法(都不选)
for (let i = 0; i < n; i++) {
dp[i][0] = 1;
}
// 状态转移
for (let i = 1; i < n; i++) { // 遍历硬币
for (let j = 0; j <= bagSize; j++) { // 遍历金额
if (coins[i] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
}
}
}
return dp[n - 1][bagSize];
}
- 一维数组dp
- 1.确定dp数组定义及下标的含义:dp[j]:凑成总金额j的货币组合数为dp[j]
- 2.确定递推公式:dp[j] += dp[j - coins[i]]
- 3.dp数组如何初始化:装满背包容量为0 的方法是1,即不放任何物品,dp[0] = 1
- 4.确定遍历顺序:先遍历物品再遍历背包得到的是组合数,先遍历背包再遍历物品得到的是排列数。组合不强调元素之间的顺序,排列强调元素之间的顺序。
- 5.举例推导dp数组,打印dp数组:
const change = (amount, coins) => {
let dp = Array(amount + 1).fill(0);
dp[0] = 1; // 只有一种方式达到0元(什么都不选)
// dp[j]:凑成金额j的组合数
// 外层循环 i 遍历每种硬币(物品)。
for(let i =0; i < coins.length; i++) { // 遍历每种硬币
// 内层循环 j 表示当前要凑成的金额(背包容量),从 coins[i](当前硬币面额)开始,一直到 amount(目标金额)。
// 为什么从 coins[i] 开始?因为金额 j 必须至少能放下当前硬币(即 j >= coins[i]),否则没法用当前硬币来组合。例如,当前硬币面额是 2,金额 1 不可能用这个硬币组合出来,所以从 2 开始。
for(let j = coins[i]; j <= amount; j++) { // 遍历金额
dp[j] += dp[j - coins[i]];
}
}
// 返回组合数
return dp[amount];
}
- 组合总和 Ⅳ
- 给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
- 题目数据保证答案符合 32 位整数范围。
- 思路
- 1.确定dp数组定义及下标的含义:dp[i]: 凑成目标正整数为i的排列个数为dp[i]
- 2.确定递推公式:dp[i] += dp[i - nums[j]]
- 3.dp数组如何初始化:dp[0] = 1,非0下标的dp[i]初始化为0
- 4.确定遍历顺序:如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for遍历背包,内层for循环遍历物品。这里是求排列数,先遍历背包再遍历物品。
- 5.举例推导dp数组,打印dp数组:
/**
* 外层循环遍历所有可能的目标和 i(从 0 到 target)。
* 内层循环遍历所有可选数字 nums[j]。
* 如果当前目标和 i 大于等于 nums[j],说明可以用 nums[j] 作为最后一个数,
* 那么 dp[i] 就可以加上 dp[i - nums[j]](即前面已经凑成 i - nums[j] 的所有排列,再加上 nums[j])。
*/
const combinationSum4 = (nums, target) => {
// dp[i] 表示:凑成目标和为 i 的排列个数。
// dp[0] = 1,表示凑成 0 只有 1 种方式(什么都不选)。
let dp = Array(target + 1).fill(0);
dp[0] = 1;
// 遍历背包容量(目标和)
for(let i = 0; i <= target; i++) {
// 遍历物品(数组元素)
for(let j = 0; j < nums.length; j++) {
if (i >= nums[j]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
};
- 爬楼梯(第八期模拟笔试)
-
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数。
-
输入描述:输入共一行,包含两个正整数,分别表示n, m
-
输出描述:输出一个整数,表示爬到楼顶的方法数。
-
输入示例:3 2
-
输出示例:3
- 思路
- 完全背包问题:1阶,2阶,.... m阶就是物品,楼顶就是背包。
- 1.确定dp数组定义及下标的含义:dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
- 2.确定递推公式:dp[i] += dp[i - j]
- 3.dp数组如何初始化:dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果
- 4.确定遍历顺序:target放在外循环,将nums放在内循环,每一步可以走多次,这是完全背包,内循环需要从前向后遍历。
- 5.举例推导dp数组,打印dp数组:
/**
* 1. dp数组定义
* dp[j] 表示:爬到第 j 阶楼梯的方法数。
* dp[0] = 1,表示爬到 0 阶只有 1 种方式(什么都不做)。
*
* 2. 状态转移
外层循环遍历每个目标阶数 j(从 1 到 n)。
内层循环遍历每次可以爬的步数 i(这里是 1 或 2)。
如果当前阶数j能由前面 j-i 阶再爬 i 阶得到,则把 dp[j-i] 的方法数加到 dp[j] 上。
* 3. 返回结果
dp[n];返回爬到第 n 阶的方法总数。
*/
var climbStairs = function (n) {
let dp = new Array(n + 1).fill(0);
dp[0] = 1;
// 排列题,注意循环顺序,背包在外物品在内
for (let j = 1; j <= n; j++) { // 遍历楼梯总阶数(背包容量)
for (let i = 1; i <= m; i++) { // 每次可以爬1阶-m阶(物品)
if (j - i >= 0) dp[j] = dp[j] + dp[j - i];
}
}
return dp[n];
}
// 支持多组输入,每行输入 n 和 m,输出爬到 n 阶的方法数。
// dp[i] 表示爬到第 i 阶的方法数,初始化 dp[0]=1。
// 外层循环遍历阶数,内层循环遍历每次可爬的步数(1~m)。
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
let input = [];
readline.on('line', (line) => {
input.push(line.trim());
});
readline.on('close', () => {
for (let idx = 0; idx < input.length; idx++) {
const [n, m] = input[idx].split(' ').map(Number);
let dp = new Array(n + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= n; i++) { // 遍历楼梯总阶数(背包容量)
for (let j = 1; j <= m; j++) { // 每次可以爬1~m阶(物品)
if (i - j >= 0) dp[i] += dp[i - j];
}
}
console.log(dp[n]);
}
});
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。

浙公网安备 33010602011771号