前端菜鸟的学习笔记,大佬慎看!!
746. 使用最小花费爬楼梯
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
示例 1:
输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
示例 2:
输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。
思路: 动规练手题,还是比较简单的
- 首先要理解题目,求到达楼顶的最小体力值,楼顶并不在数组中,可以想象为
楼顶在最后一位后面 - 那么最小体力值就是
dp[数组长度减一], dp[数组长度减二]的最小值 - 递推公式:
某一个阶梯的最小体力值 = 前一个阶梯和前二个阶梯的最小值 + 本阶梯所需花费的体力值 - 看递推公式,某个阶梯需要依赖前面阶梯的最小体力值,从前往后遍历。
var minCostClimbingStairs = function (cost) {
let l = cost.length;
let dp = []
dp[0] = cost[0]; // 初始化
dp[1] = cost[1]; // 初始化
for (let i = 2; i < cost.length; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i] // 取前面2个的最小值
}
return Math.min(dp[l - 1], dp[l - 2])
};
滚动数组:
还可以优化一下空间复杂度,这里为O(n).
每次递推都只需要前面一个,前面两个的值,所以用2位来保存即可
也可以不用数组。
var minCostClimbingStairs = function (cost) {
let l = cost.length
let dp = []
dp[0] = cost[0] // 初始化
dp[1] = cost[1] // 初始化
for (let i = 2; i < cost.length; i++) {
let res = Math.min(dp[0], dp[1]) + cost[i] // 取前面2个的最小值
dp[0] = dp[1]
dp[1] = res
}
return Math.min(dp[0], dp[1])
};
96. 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入: n = 3
输出: 5
示例 2:
输入: n = 1
输出: 1
解题思路:
当有n = 1时,只有一种排列方式,等于1
当有n = 2时,有两种排列方式,等于2
当有n = 3时:
1为头节点,二叉搜索树的左子树为空,右子树为2,也就是1(左子树为空,只有一种排列方式) * 2个节点时的二叉搜索树的种数 = 22为头节点,二叉搜索树的左子树为1,右子树为3,也就是 1(左子树只能为1) * 1(右子树只能为3) = 13为头节点,二叉搜索树的左子树为2, 右子树为空,也就是2(左子树有两个节点) * 1(右子树没有节点) = 22 + 1 + 2 = 5
所以dp[i]的含义就是 有i个节点时,共有几种二叉搜索树的种数
递推公式:n个节点的二叉搜索树种数 = 当不同的值作为头结点,左子树节点j的排列方式 * 右子树节点 i - j的排列方式
遍历顺序:从前往后
var numTrees = function(n) {
let dp = []
dp[0] = 1 // 看看dp[i]的含义,假如左子树为空,那么就返回右子树的排列顺序, 1 * 右子树
dp[1] = 1 // 一个节点没得排
for(let i = 2; i <= n; i++) {
dp[i] = 0
for(let j = 0; j <= i; j++) { // j 也就是 不断替换头节点
dp[i] += dp[j] * dp[i - j - 1] // 左节点为j个,右节点就是 i - j - 1,还要减头节点
}
}
return dp[n]
}
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。
dp思路
一个整数从1开始拆分,例如9 -> (1, 8) -> (2, 7) -> (3, 6)
那么该整数的最大乘积为 拆分的两个数直接相乘,或者某个拆分,某个不拆分的两个数相乘,或者两个都拆 分相乘它们中的最大值
当n = 2时, 此时拆分为 1 + 1,最大乘积为 1
当n = 3时, 此时拆分为 1 + 2,最大乘积为 1 * 2 或者 1 * dp[2]
(j 为拆出来的第一个数, n - j就是拆出来的第二个数)
当n时, dp[n] = Math.max(dp[n], Math.max(j * (n - j), j * dp[n - j]))
Tip: 上面的 j * (n - j), 是两个整数都不继续拆分,而 j * dp[n - j]是只拆分了 n - j, 这是为什么呢
假如n = 10,j = 5, (n - j) = 5, 如果两个数都继续拆分, (2 * 3) * (2 * 3), 这里的2是肯定不能继续拆分了,否者就变小为1,那么上面就是 2 * dp[8],在循环中j * dp[i - j]中已经计算过了
也就是总有你不能拆分的,那些两个都拆的,总是可以转换成一个不拆,一个拆的情况,例如上面的5 * 5,可以转换成 2 * dp[8]
function integerBreak(n) {
let dp = []
dp[1] = 1 // 会有拆分出1和某个值的情况,初始化dp[1]为1
dp[2] = 1
for(let i = 3; i <= n; i++) {
dp[i] = 0
for(let j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]))
// 可以换成 dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j)))
// 不拆分i- j, 拆分j 是一样的
}
}
return dp[n]
}
数学解法和优化几乎完全看不懂,qaq 太菜了
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 2:
输入:m = 7, n = 3
输出:28
示例 3:
输入:m = 3, n = 3
输出:6
思路:
- 机器人只能
向右走,或者向下走。 - dp数组含义:
dp[m][n], 到达第m行第n列表格,总共有多少种办法。 - 递推公式:
到达m行n列,等于m-1行n列 + m行n-1列,参考第1、2点。 - 初始化: 第一行和第一列的只有一种方法。
向右一直走或向下一直走。
let uniquePaths = function(m, n) {
// 怎么创建dp数组取决于你怎么遍历m, n
// 一定是从左到右,从上到下遍历。因为某一点的值依赖与前一行同列,同行前一列的值
// 先从左到右,还是先从上到下,随意
let dp = Array(m).fill().map(ele => Array(n).fill(1)) // 创建dp数组,然后初始化为1
for(let i = 1; i < m; i++) {
for(let j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
}
}
return dp[m - 1][n - 1]
}
滚动数组:
某个点的值 取决于上面和左边的值。可以用一维数组表示。
一维数组先初始化, 例如保存第一行,[1,1,1,1...],此时求解第二行的值,第一位永远是1
dp[n] = dp[n] + dp[n - 1]
等式右边的dp[n]表示上一行同列的值。相当于上边的值。
dp[n - 1] 表示同一行,上一列的值,所以dp[n] = dp[n] + dp[n - 1]
画画图就能看懂了,我刚开始也看了好久。。。
太菜了
let uniquePaths = function(m, n) {
let dp = Array(n).fill(1) //初始化数组为1
for(let i = 1; i < m; i++) {
for(let j = 1; j < n; j++) {
dp[j] = dp[j] + dp[j - 1]
}
}
return dp[n - 1]
}
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
思路:
跟上题差不多,但是多了障碍物
- 主要难点在于
初始化,初始化好了,其它感觉差不多。 - 当第一行某列出现一个障碍物,
从这列开始,包括这列,后面每一列都是0 - 当第一列某一行出现障碍物,
从这行开始,包括这行,后面每一行都是0 - 当某点出现障碍物时,这点为0
- 其它跟上面的差不多
var uniquePathsWithObstacles = function (obstacleGrid) {
let m = obstacleGrid.length // 行数
let n = obstacleGrid[0].length // 列数
let dp = Array(m).fill().map(ele => Array(n).fill(0)) // 全部初始化为0
for(let i = 0; i < n && obstacleGrid[0][i] == 0; i++) {
dp[0][i] = 1 // 第一行,某列没有障碍物就有1条路径
}
for(let i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1 // 第一列,某行没有障碍物就有1条路径
}
for(let i = 1; i < m; i++) {
for(let j = 1; j < n; j++) {
if(obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
}
}
}
return dp[m - 1][n - 1]
}
不会优化了,告辞