Day32-动态规划,leetcode509,70,746

动态规划

理论基础

  1. 动态规划基础类题目:
  • 佩波那契数列、爬楼梯
  • 背包问题
  • 打家劫舍
  • 股票问题
  • 子序列问题
  • 01背包问题
  • 纯01背包问题:装满背包的最大价值,二维递推公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 一维递推公式: dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);题-给一个容器,问装满容器的最大价值是多少?卡码网46
  • 01背包应用:能不能装满背包,递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);题-分割等和子集,给一个容器,能不能装满?leetcode416
  • 01背包应用:尽量装满背包,递推公式:第j个石头装还是不装,取大者,dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]) ;题-石头的重量,给一个背包,尽量装,能装多少?leetcode1049
  • 01背包应用:装满这个背包有多少种方法,dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];dp[j] = dp[j] + dp[j - nums[i]];题-目标和,给一个容器,装满这个背包有多少种方法?leetcode494
  • 01背包应用:装满这个背包最多有多少个物品,递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1),strs里的字符串有zeroNum个0,oneNum个1。leetcode474
  1. 动态规划题目步骤
  • 1.确定dp数组定义及下标的含义
  • 2.确定递推公式
  • 3.dp数组如何初始化
  • 4.确定遍历顺序
  • 5.举例推导dp数组,打印dp数组
  • 动态规划,Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

  • 动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优

  • 做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,确定最后推出的是想要的结果。然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。



题目

  1. 斐波那契数
  • 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
  • F(0) = 0,F(1) = 1
  • F(n) = F(n - 1) + F(n - 2),其中 n > 1
  • 给定 n ,请计算 F(n) 。

  • 思路
  • 1.确定dp[i]含义:dp[i] ,下标i表示第i个斐波那契数,dp[i]表示第i个斐波那契数值为dp[i]
  • 2.递推公式:dp[i] = dp[i - 1] + dp[i - 2]
  • 3.递推数组dp如何初始化:dp[0] = 0, dp[1] = 1
  • 4.确定遍历顺序:根据递推公式知道,我们需要从前往后遍历,因为计算dp[i]的时候需要前面的dp[i-1]和dp[i-2]的值
  • 5.打印dp数组,主要用来debug:
var fib = function(n) {
    let dp = [0, 1]; // 初始化dp数组,dp[0]=0, dp[1]=1
    for(let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移公式
    }
    console.log(dp); // 打印整个dp数组,方便调试
    return dp[n];    // 返回第n个斐波那契数
}

/**
1. 只用两个变量:
  pre1 表示 dp[i-1](前一个斐波那契数)
  pre2 表示 dp[i-2](前两个斐波那契数)
2. 状态转移:
  pre1 = pre1 + pre2,即当前项等于前两项之和
  pre2 = temp,把上一个 pre1 赋值给 pre2,准备下次循环
3. 空间优化:
  不需要整个 dp 数组,只需两个变量即可
4. 边界处理:
  n 为 0 或 1 时直接返回

用两个变量循环迭代,依次更新,节省空间,快速求出第 n 个斐波那契数。
 */
var fib = function(n) {
    // pre1 表示 dp[i-1],pre2 表示 dp[i-2]
    let pre1 = 1;
    let pre2 = 0;
    let temp;
    if (n === 0) return 0;
    if (n === 1) return 1;
    for(let i = 2; i <= n; i++) {
        temp = pre1;           // 暂存上一个斐波那契数
        pre1 = pre1 + pre2;    // 当前斐波那契数 = 前两个数之和
        pre2 = temp;           // pre2 更新为上一个 pre1
    }
    return pre1;
};


  1. 爬楼梯
  • 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
  • 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

  • 思路
  • 1.确定dp[i]含义:
  • dp[i] 达到i阶,有dp[i]种方法
  • dp[i-2] 达到i-2阶,有dp[i-2]种方法
  • dp[i-1] 达到i-1阶,有dp[i-1]种方法
  • 2.递推公式:dp[i] = dp[i - 2] + dp[i - 1]
  • 3.递推数组dp如何初始化:dp[1] = 1, dp[2] = 2,i从3开始递推
  • 4.确定遍历顺序:根据递推公式知道,我们需要从前往后遍历,因为计算dp[i]的时候需要前面的dp[i-1]和dp[i-2]的值
  • 5.打印dp数组,主要用来debug:
  • 1阶,爬1个台阶,1种方法
  • 2阶,爬1+1个台阶,或者一次爬2个台阶,2种方法(1阶+1阶,2阶)
  • 3阶,要么从1阶迈上来的(1阶走2步到达3阶),要么从2阶迈上来的(从2阶走1步到达3阶),3种方法(1阶+1阶+1阶,1阶+2阶,2阶+1阶)
  • 4阶,由2阶或者3阶迈上来,2阶一步迈2阶到达4阶,3阶走一步迈1阶到达4阶,5种(1阶+1阶+1阶+1阶,1阶+2阶+1阶, 1阶+1阶+2阶, 2阶+1阶+1阶, 2阶+2阶)
/**
1. dp数组定义:
  dp[i] 表示到达第 (i+1) 阶楼梯的方法数(因为数组下标从0开始)。
2. 初始化:
  dp[0] = 1:只有1阶时只有1种方法。
  dp[1] = 2:2阶时可以1+1或直接2,一共2种方法。
3. 状态转移公式:
  dp[i] = dp[i-1] + dp[i-2]
  到达第 i+1 阶的方法数等于到达前一阶的方法数加上到达前两阶的方法数。
4. 遍历顺序:
  从第3阶(下标2)开始递推到第 n 阶。
5. 返回结果:
  返回 dp[n-1],即到达第 n 阶的方法数。


用动态规划数组递推,dp[i] = dp[i-1] + dp[i-2],最后返回到达第 n 阶的方法数。
 */
var climbStairs = function(n) {
    // dp[i] 为第 i 阶楼梯有多少种方法爬到楼顶
    // dp[i] = dp[i - 1] + dp[i - 2]
    let dp = [1 , 2] // dp[0]=1(1阶),dp[1]=2(2阶)
    for(let i = 2; i < n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]  // 状态转移公式
    }
    return dp[n - 1] // 返回到达第 n 阶的方法数
};

/**
1. 边界处理:如果 n 为 0 或 1,直接返回 n。
2. dp数组定义:dp[i] 表示到达第 i 阶楼梯的方法数。
3. 初始化:dp[1]=1(1阶只有1种方法),dp[2]=2(2阶有2种方法)。
4. 状态转移:从第3阶开始,dp[i] = dp[i-1] + dp[i-2],即到达第 i 阶的方法数等于前一阶和前两阶的方法数之和。
5. 返回结果:返回 dp[n],即到达第 n 阶的方法数。

用动态规划数组递推,dp[i]=dp[i-1]+dp[i-2],最后返回到达第 n 阶的方法数。
 */
var climbStairs = function(n) {
    if (n <= 1) return n; // 只有0阶或1阶时,方法数就是n本身
    let dp = new Array(n + 1);
    dp[1] = 1;
    dp[2] = 2;
    for (let i = 3; i <= n; i++) { // 从第3阶开始递推
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};


  1. 使用最小花费爬楼梯
  • 给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
  • 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
  • 请你计算并返回达到楼梯顶部的最低花费。

  • 思路
  • 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,即 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了
  • 1.dp数组含义:dp[i],到达下标为i的楼所需要的最小花费为dp[i]
  • 2.递推公式:dp[i] = Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]),dp[i]的值与前两个值有关系
  • 3.dp数组初始化:dp[0]=0,dp[1]=0
  • 4.遍历顺序:从前往后,因为第i个值依赖于第i-1和i-2的值
  • 5.打印dp
/**
1. dp数组定义:
  dp[i] 表示到达第 i 阶(不一定踩在i上)所需的最小花费。
2. 初始化:
  dp[0]=0,dp[1]=0,因为可以从第0或第1阶开始,不需要花费。
3. 状态转移公式:
  dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
  到达第 i 阶可以从 i-1 跨一步,或从 i-2 跨两步,取两者花费较小的。
4. 返回结果:
  dp[cost.length],即到达楼顶的最小花费。

用动态规划递推,dp[i] 表示到达第 i 阶的最小花费,最后返回 dp[cost.length] 即可。
 */
var minCostClimbingStairs = function(cost) {
  const dp = [0, 0]; // dp[i]表示到达第i阶的最小花费
  for (let i = 2; i <= cost.length; ++i) {
    dp[i] = Math.min(
      dp[i - 1] + cost[i - 1], // 从i-1阶跨一步到i
      dp[i - 2] + cost[i - 2]  // 从i-2阶跨两步到i
    );
  }
  return dp[cost.length]; // 返回到达楼顶的最小花费
};

/**
1. 变量含义
  dpBefore:到达前两阶的最小花费(dp[i-2])
  dpAfter:到达前一阶的最小花费(dp[i-1])
2. 状态转移
  dpi = Math.min(dpBefore + cost[i-2], dpAfter + cost[i-1])
  到达第 i 阶可以从 i-2 跨两步,或从 i-1 跨一步,取两者花费较小的。
3. 变量更新
  每次循环后,dpBefore 和 dpAfter 向前推进一位,始终只保存最近两个状态。
4. 返回结果
  dpAfter 就是到达楼顶的最小花费。

用两个变量滚动更新,节省空间,动态规划求解到达楼顶的最小花费。
 */
var minCostClimbingStairs = function(cost) {
  let dpBefore = 0,    // 对应 dp[i-2],到达前两阶的最小花费
      dpAfter = 0;     // 对应 dp[i-1],到达前一阶的最小花费
  for(let i = 2; i <= cost.length; i++) {
      let dpi = Math.min(
          dpBefore + cost[i - 2], // 从i-2阶跨两步到i
          dpAfter + cost[i - 1]   // 从i-1阶跨一步到i
      );
      dpBefore = dpAfter; // 更新 dp[i-2]
      dpAfter = dpi;      // 更新 dp[i-1]
  }
  return dpAfter; // 返回到达楼顶的最小花费
};



参考&感谢各路大神

posted @ 2025-06-28 18:24  安静的嘶吼  阅读(3)  评论(0)    收藏  举报