Day37-动态规划,leetcode518,377,70

完全背包问题

  • 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大
  • 纯完全背包问题,求装满这个背包它的最大价值是多少?或者问能不能装满这个背包?那么两层for循环怎么颠倒都可以
  • 完全背包在不同场景下问装满这个背包有多少种方法时,需要区分是求组合数还是排列数,组合数,不强调集合里的元素顺序,则先遍历物品再遍历背包,排列数是强调集合里的元素顺序,则先遍历背包再遍历物品
  1. 携带研究材料(第七期模拟笔试)
  • 小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。

  • 小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

  • 输入描述:第一行包含两个整数,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]);
});


  1. 零钱兑换 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];
}


  1. 组合总和 Ⅳ
  • 给你一个由 不同 整数组成的数组 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];
};


  1. 爬楼梯(第八期模拟笔试)
  • 假设你正在爬楼梯。需要 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]);
    }
});



参考&感谢各路大神

posted @ 2025-07-03 09:13  安静的嘶吼  阅读(10)  评论(0)    收藏  举报