Day32-动态规划,leetcode509,70,746
动态规划
理论基础
- 动态规划基础类题目:
- 佩波那契数列、爬楼梯
- 背包问题
- 打家劫舍
- 股票问题
- 子序列问题
- 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.确定dp数组定义及下标的含义
- 2.确定递推公式
- 3.dp数组如何初始化
- 4.确定遍历顺序
- 5.举例推导dp数组,打印dp数组
-
动态规划,Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
-
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优
-
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,确定最后推出的是想要的结果。然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
题目
- 斐波那契数
- 斐波那契数 (通常用 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;
};
- 爬楼梯
- 假设你正在爬楼梯。需要 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];
};
- 使用最小花费爬楼梯
- 给你一个整数数组 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; // 返回到达楼顶的最小花费
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。