算法练习笔记(一)
记录下每天练习的算法题,一天两道~
10.08
①最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [0]
输出:0
示例 4:
输入:nums = [-1]
输出:-1
示例 5:
输入:nums = [-100000]
输出:-100000
提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-subarray
思路
:
使用动态规划如下:
- 确定dp数组以及下标的含义
dp[i]:表示包括下标i之前的最大连续子序列和
- 确定递推方式
dp[i]有两种方向
①当dp[i-1]+nums[i]>nums[i],nums[i]加入当前连续子序列和时,dp[i] = dp[i-1]+nums[i]
②当dp[i-1]+nums[i]<nums[i],需要从头开始计算当前连续子序列和时,dp[i] = nums[i]
所以dp[i] = max(dp[i-1]+nums[i],nums[i])
- dp数组如何初始化
从递推公式可以看出来dp[i]是依赖于dp[i-1]的状态,dp[0]就是递推公式的基础。
很明显dp[0] = nums[0]
- 确定遍历顺序
dp[i]是依赖于dp[i-1]的状态,需要从前向后遍历
- 举例推导递推dp数组
nums = [-2,1,-3,4,-1,2,1,-5,4]
通过动态规划可以看到,最大连续子序列和为dp[6]=6,也就是连续子数组 [4,-1,2,1] 的和最大。
代码
:
const maxSubArray = function(nums){
//缓存数组长度
const len = nums.length
//创建dp数组
const dp = new Array(len)
dp[0] = nums[0]
//初始化最大连续子序列和为dp[0]
let maxSub = dp[0]
//从第2个元素开始,遍历整个数组
for(let i=1;i<len;i++){
dp[i] = Math.max(dp[i-1]+nums[i],nums[i])
//及时更新最大连续子序列和
if(dp[i]>maxSub){
maxSub = dp[i]
}
}
//遍历完毕,最后到手的就是最大连续子序列和
return maxSub
}
②爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs
思路
:
动态规划是一个自底向上的过程。它要求站在已知的角度,通过定义已知和未知之间的关系,一步一步向前推到,进而求解出未知的值。
在这道题中,已知f(1)=1,f(2)=2,要求解未知的f(n),需要找出其等价关系。
根据题意,每次可以爬 1 或 2 个台阶。那么第n阶就是从第n-1阶或第n-2阶上来。
- 上到n-1阶时,有f(n-1)种方法,此时再上一个台阶就是第n阶了
- 上到n-2阶时,有f(n-2)种方法,此时再上两个台阶就是第n阶了
所以f(n) = f(n-1)+f(n-2)
代码
:
const climbStairs = function(n){
//初始化状态数组
const f = []
//初始化已知值
f[1] = 1
f[2] = 2
//动态更新每一层梯对应的结果
for(let i=3;i<=n;i++){
f[i] = f[i-2]+f[i-1]
}
//返回目标值
return f[n]
}
10.09
①杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
提示:
1 <= numRows <= 30
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/pascals-triangle
思路
:
假设用数组dp代表整个杨辉三角。
根据题意,每个数是它左上方和右上方的数的和。可以得出每一行每个元素与上一行之间的关系
dp(i,j)=dp(i-1,j-1)+dp(i-1,j)
在这道题中,还有一个已知条件,每一行第一个元素及末尾元素都为1,那么可以推出完整的关系
代码
:
const generate = function(numRows){
//结果数组,保存杨辉三角
let res = []
for(let i=0;i<numRows;i++){
//记录第i行杨辉三角
let dp=[]
for(let j=0;j<=i;j++){
if(j==0||j==i){
dp.push(1)
}else{
//左上方的数若不存在则视为0
let left = res[i-1][j-1]||0
//右上方的数
let right = res[i-1][j]
dp.push(left+right)
}
}
//每计算完第i行杨辉三角就保存进结果数组
res.push(dp)
}
//返回结果数组
return res
}
②买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock
思路
:
根据题意,买卖股票流程需要先买后卖。在这个过程中,需要确保卖出价格高于买入价格,同时要找到最小的买入价格使得利润最大化。
代码
:
const maxProfit = function(prices){
//缓存数组长度
const len = prices.length
//表示获取的最大利润,初始化为0
let maxProfit = 0
//表示股票的最低买入价格,初始化为第一天股票的价格
let minPrice = prices[0]
//假设第一天买了股票,遍历从第一天后的股票买卖利润情况
for(let i=1;i<len;i++){
//判断卖出价格是否大于买入价格
if(prices[i]<minPrice){
//卖出价格小于买入价格,替换为当前买入价格,然后跳过后续卖出操作
minPrice = prices[i]
continue
}
//计算卖出操作是否能获得最大利润
maxProfit = Math.max(maxProfit,prices[i]-minPrice)
}
//返回结果
return maxProfit
}
10.10
①最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
思路
和代码
:
这题没有思路55555,贴下别人的答案
function longestPalindrome(s){
let n = s.length
if(n<2){
return s
}
//初始化二维数组
const dp = []
for(let i=0;i<n;i++){
dp[i] = []
for(let j=0;j<n;j++){
//当i=j时,dp[i][j]为true,1个字符是回文初始化为true,其他元素都初始化为false
if(i===j){
dp[i][j]=true
}else{
dp[i][j]=false
}
}
}
let start = 0
let maxLength = 1
//因为已知值在对角线,所以从下往上,从左到右遍历
//左选择s[i]和s[j]
for(let i=n-1;i>=0;i--){
for(let j=i+1;j<n;j++){
if(s[i]!=s[j]){
dp[i][j] = false
}else{
//[i..j]长度小于3一定是回文
if(j-i<3){
dp[i][j] = true
}else{
//表示i到j之间,如果i+1到j-1之间是回文串,那当s[i]和s[j]相等时,i到j之间也是回文
dp[i][j] = dp[i+1][j-1]
}
}
//更新结果
//dp[i][j]===true,表示子串[i..j]是回文,记录回文长度j-i+1和起始位置
if(dp[i][j]===true&&j-i+1>maxLength){
maxLength = j-i+1
start = i
}
}
}
return s.substring(start,start+maxLength)
}
②括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
有效括号组合需满足:左括号必须以正确的顺序闭合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/generate-parentheses
思路
:
这题适合采用递归。
代码
:
var generateParenthesis = function(n){
let res = []
generate("",0,0)
//cur :当前字符 left:当前字符左括号 right:当前字符右括号
function generate(cur,left,right){
if(cur.length === 2*n){
res.push(cur)
return
}
//如果左括弧未用完则继续增加左括弧
if(left<n){
generate(cur+"(",left+1,right)
}
// 如果右括弧少于左括弧则继续增加右括弧
if(right<left){
generate(cur+")",left,right)
}
}
return res
}
10.11
①跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/jump-game
思路
:
由题意可知,每人所能跳的最长距离(到达位置) = 下标+代表的值
遍历每个人,每次都和之前的人比较,看看谁能跳得更远
①如果遍历到的当前这个人跳不到终点,但是前面的人能跳到,也是可以直接返回true的
②如果前面的人不能跳到当前遍历的人(位置),那么应当返回false。连当前位置都跳不动,那更别说跳到终点了。
代码
:
var canJump = function(nums){
//表示终点
const lastIndex = nums.length-1
//表示最长距离
let maxJump = 0
for(let i=0;i<nums.length;i++){
//判断前面的人能不能跳到当前遍历到的人
if(i>maxJump) return false
//判断能否跳到终点
if(maxJump>=lastIndex)return true
//更新所能跳的最长距离
maxJump = Math.max(maxJump,nums[i]+i)
}
return false
}
②不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-paths
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
示例 1:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 2:
输入:m = 7, n = 3
输出:28
示例 3:
输入:m = 3, n = 3
输出:6
思路
:
- dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
- dp
[i][j]
,只能有两个方向来推导出来,即dp[i - 1][j]
和 dp[i][j - 1]
,因为只有从这两个方向过来才能到达(i,j),所以dp[i][j]
= dp[i - 1][j]
+ dp[i][j - 1]
- dp数组的初始化:首先dp
[i][0]
一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]
也同理。
代码
:
var uniquePaths = function(m, n) {
let dp = new Array(m)
//初始化数组
for ( let i=0;i< m; i++){
dp[i]= new Array(n)
dp[i][0]=1
for( let j=0; j< n; j++){
dp[0][j]=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]
};
- 时间复杂度:O(m * n)
- 空间复杂度:O(m* n)
优化版
使用"滚动数组",让数组"滚动"起来,确保每个时刻
空间内的数据都是当前真正会用到的最新数据。
已知,下一行的值 = 当前行的值+上一行的值,使用了滚动数组后,可以将dp[i][j]
= dp[i - 1][j]
+ dp[i][j - 1]
简化成dp[j] = dp[j]+dp[j-1]。此时的dp[j-1]代表上一阶段dp[j]的值
var uniquePath = function(m,n){
let dp = new Array(n).fill(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]
}
- 时间复杂度:O(m * n)
- 空间复杂度:O( n)
10.12
①解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
'A' -> 1
'B' -> 2
...
'Z' -> 26
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:
"AAJF" ,将消息分组为 (1 1 10 6)
"KJF" ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "0"
输出:0
解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 'J' -> "10" 和 'T'-> "20" 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。
示例 4:
输入:s = "06"
输出:0
解释:"06" 不能映射到 "F" ,因为字符串含有前导 0("6" 和 "06" 在映射中并不等价)。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/decode-ways
思路
:
用 dp[i]来切分子问题, 那么 dp[i]表示的意思是当前字符串的以索引 i 结尾的子问题。
- 对于一个数字来说[1,9]这九个数字能够被识别为一种编码方式
- 对于两个数字来说[10, 26]这几个数字能被识别为一种编码方式
dp[i] = 以自身去编码(一位) + 以前面的元素和自身去编码(两位)
代码
:
var numDecoding = function(s){
if(s==null||s.length==0){
return 0
}
const dp = (new Array(s.length+1)).fill(0)
dp[0] = 1
dp[1] = s[0]!="0"?1:0
for(let i=2;i<s.length+1;i++){
const one = +s.slice(i-1,i)
const two = +s.slice(i-2,i)
if(two>=10&&two<=26){
dp[i] = dp[i-2]
}
if(one>=1&&one<=9){
dp[i]+=dp[i-1]
}
}
return dp[dp.length-1]
}
②买卖股票的最佳时机 II
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii
思路
:
因为可以多次买卖一支股票,所以在I的基础上,添加几个变量判断买卖情况
①第一次买入,应尽量选择最低价格
②第一次卖出时,所得利润 = 卖出价格-买入价格
③多次买入股票,应该确保买入价格不能比上次卖出价格高,并更新每次买入价格
④多次卖出股票,比较从第一次买入一直持有到现在才卖的利润
和上次卖出+这次卖出所得利润
谁更高,使得利益最大化
代码
:
var maxProfit = function(prices){
//记录最低价格
let minPrice = prices[0]
//缓存数组长度
const len = prices.length
//结果
let res = 0
//记录交易次数(买+卖算一次)
let times = 0
//记录最新买入价格
let lastPrice = 0
for(let i=1;i<len;i++){
//寻找第一次买入时最低价格
if(times===0&&prices[i]<minPrice){
minPrice = prices[i]
continue
}
//更新每次买入价格,买入价格不能比上次卖出价格高
if(times>0&&prices[i]<lastPrice){
lastPrice = prices[i]
continue
}
//第一次卖出
if(times===0){
res = prices[i]-minPrice
times++
}else{
res = Math.max(res+prices[i]-lastPrice,prices[i]-minPrice)
}
lastPrice = prices[i]
}
return res
}
学习下别人的思路:
- 只要股票价格上涨,上涨的部分就是我的利润,可以理解为上涨期间第一天买入,然后一直持有到上涨最后一天即下跌前一天再卖出
- 只要股票价格下跌,那我肯定在下跌前一天卖了,而且下跌期间永远不会买入
作者:chitanda-eru
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/solution/yu-zhi-wei-lai-zhe-mai-mai-gu-piao-de-si-lu-by-hut/
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
let profit = 0;
for (let i = 0; i < prices.length - 1; i++) {
if (prices[i + 1] > prices[i]) profit += prices[i + 1] - prices[i];
}
return profit;
};
10.13
①分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/palindrome-partitioning
思路+代码:
var partition = function(s){
const n = s.length
const dp = new Array(n).fill([]).map(()=>new Array(n).fill(true))
//动态规划预处理 两次循环遍历 存储字符串所有子串是否为回文字符串 true/false
for(let i=n-1;i>=0;i--){
for(let j=1+1;j<n;j++){
dp[i][j] = (s[i]===s[j])&&dp[i+1][j-1]
}
}
const ans = [],res = []
//回溯从0开始进行深度优先遍历
const dfs = (i)=>{
if(i===n) return res.push(ans.slice())
//循环的目的是可以取到字符串s的起始位置为i的所有子字符
for(let j=i;j<n;j++){
//如果是子串会进入下一层
if(dp[i][j]){
ans.push(i,j+1)
dfs(j+1)
//本次递归完需要回到本次循环的上一层状态(回溯)
ans.pop()
}
}
}
dfs(0)
return res
}
②单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-break
思路+代码
dp[i]表示0-i之间的字符串是否可以被拆分并满足题设条件存在于wordDict中。
假设拆分点为j,那么状态转移方程为:
dp[i] = dp[j] && s.substring(j+1, i+1)存在于wordDict
作者:GuYueJiaJie
链接:https://leetcode-cn.com/problems/word-break/solution/javascript-dong-tai-gui-hua-by-guyuejiajie/
来源:力扣(LeetCode)
var wordBreak = function(s,wordDict){
// dp[i]表示0-i之间的字符串是否可以被拆分并满足题设条件存在于wordDict中
let dp = new Array(s.length).fill(false)
let set = new Set(wordDict)
for(let i=0;i<s.length;i++){
// 检查0-i之间的字符串是否直接存在于wordDict中
if(set.has(s.subString(0,i+1))){
dp[i] = true
continue
}
// 这一步是为了检查。假如s.substring(0,i)不直接存在于wordDict中
// 那么判断拆分之后是否存在于wordDict中
for(let j=0;j<i;j++){
if(dp[j]&&s.subString(j+1,i+1)){
dp[i] = true
break
}
}
}
return dp[s.length-1]
}
10.14
①乘积最大子数组
给你一个整数数组 nums
,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-product-subarray
思路
:
得考虑负数情况,需要算出最小值,因为最小值(<0时)*负数可能就为最大值
代码
:
var maxProduct = function(nums){
let maxF = nums[0],minF = nums[0],maxProduct=maxF
for(let i=1;i<nums.length;i++){
let max = maxF,min = minF
maxF = Math.max(max*nums[i],Math.max(nums[i],nums[i]*minF))
minF = Math.min(min*nums[i],Math.min(nums[i],nums[i]*maxF))
maxProduct = Math.max(maxProduct,maxF)
}
return maxProduct
}
②打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robbe
思路
dp[i]表示i以内的房屋最多可以偷窃的金额
如果偷第i间房,dp[i] = dp[i-2]+nums[i]
如果不偷第i间房,dp[i] = dp[i-1],偷窃金额与偷i-1内的房屋一样
根据题意,初始化dp数组
只有一间房时,dp[0] = nums[0] 没得选_
只有两间房时,dp[1] = Math.max(nums[0],nums[1]) 看这相邻的房子谁的钱多就偷谁
代码
:
var rob = function(nums){
const len = nums.length
const dp = new Array(len)
dp[0] = nums[0]
dp[1] = Math.max(nums[0],nums[1])
for(let i = 2;i<len;i++){
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1])
}
return dp[len-1]
}
10.15
① 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/perfect-squares
代码
:
var numSquares = function(n){
const dp = new Array(n+1).fill(0)
for(let i=1;i<=n;i++){
dp[i] = i
for(let j=1;i-j*j>=0;j++){
dp[i] = Math.min(dp[i],dp[i-j*j])
}
}
return dp[n]
}
②最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
思路
:
dp[i]表示前i个元素中最长上升子序列的长度。若想求出dp[i],则需要关注到第i个元素与前i-1个元素范围内的最长上升子序列的关系。它们之间的关系有两种可能:
1.若第i+1个元素比前i个元素中某一个元素要大,此时就可以在这个元素所在的上升子序列的末尾追加第i+1个元素(延长原有的子序列),得到一个新的上升子序列。
2.若第i+1个元素并不比前i个元素中所涵盖的最长上升子序列中的某一个元素大,则维持原状,子序列不延长。
若存在一个索引j,一个比当前元素小的值:则意味着遇到了一个可以延长的上升子序列,故更新当前元素索引位对应的状态,此时dp[i] = Math.max(dp[i],dp[j]+1)
``代码`
var lengthOfLIS = function(nums) {
//缓存数组长度
const len = nums.length
if(!len) {
return 0
}
//创建dp数组并初始化状态值
const dp = new Array(len).fill(1)
//初始化最大上升子序列的长度为1
let maxLen = 1
//从第2个元素开始,遍历数组
for(let i=1;i<len;i++){
//每遍历一个新元素,都要“回头看”,看看能不能延长原有的上升子序列
for(let j=0;j<i;j++){
if(nums[i]>nums[j]){
//若遇到了一个比当前元素小的值:则意味着遇到了一个可以延长的上升子序列,故更新当前元素索引位对应的状态
dp[i] = Math.max(dp[i],dp[j]+1)
}
}
//及时更新上升子序列长度的最大值
if(dp[i]>maxLen){
maxLen = dp[i]
}
}
//返回最大值
return maxLen
};
③零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change
代码
:
const coinChange = function(coins,amount){
//用于保存每个目标总额对应的最小硬币个数
const f = []
//提前定义已知情况
f[0] = 0
//遍历[1,amount]这个区间的硬币总额
for(let i=1;i<=amount;i++){
//求的是最小值,因此预设为无穷大,确保它一定会被更小的数更新
f[i] = Infinity
//循环遍历每个可用硬币的面额
for(let j=0;j<coins.length;j++){
//若硬币面额小于目标总额,则问题成立
if(i-coins[j]>=0){
f[i] = Math.min(f[i],f[i-coins[j]]+1)
}
}
}
//若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解。返回-1
if(f[amount]===Infinity){
return -1
}
return f[amount]
}