用 javascript 分类刷 leetcode3. 动态规划 (图文视频讲解)
什么是动态规划
动态规划,英文:Dynamic Programming
,简称DP
,将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多重叠子问题,使用动态规划来解是比较有效的。
求解动态规划的核心问题是穷举,但是这类问题穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」才能正确地穷举。重叠子问题、最优子结构、状态转移方程就是动态规划三要素
动态规划和其他算法的区别
动态规划和分治的区别:动态规划和分治都有最优子结构 ,但是分治的子问题不重叠
动态规划和贪心的区别:动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优解,所以它永远是局部最优,但是全局的解不一定是最优的。
动态规划和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算
动态规划的解题方法
递归+记忆化(自顶向下)
动态规划(自底向上)
解动态规划题目的步骤
根据重叠子问题定义状态
寻找最优子结构推导状态转移方程
确定 dp 初始状态
确定输出值
斐波那契的动态规划的解题思路
暴力递归
递归 + 记忆化
动态规划
滚动数组优化
动态规划 + 降维,(降维能减少空间复杂度,但不利于程序的扩展)
343. 整数拆分 (medium)
视频讲解:传送门
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。示例 2:
输入: n = 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
2 <= n <= 58
思路:
dp[i]
为正整数 i 拆分之后的最大乘积,循环数字 n,对每个数字进行拆分,取最大的乘积,状态转移方程:dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j)
,j*(i-j)
表示把 i 拆分为j
和 i-j 两个数相乘,j * dp[i-j]
表示把i
拆分成j
和继续把(i-j)
这个数拆分,取(i-j)
拆分结果中的最大乘积与 j 相乘复杂度:时间复杂度
O(n^2)
,两层循环。空间复杂度O(n)
,dp
数组的空间
js:
152. 乘积最大子数组 (medium)
视频讲解:传送门
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
子数组 是数组的连续子序列。
示例 1:
输入: nums = [2,3,-2,4]输出: 6 解释: 子数组 [2,3] 有最大乘积 6。示例 2:
输入: nums = [-2,0,-1]输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
提示:
1 <= nums.length <= 2 * 104-10 <= nums[i] <= 10nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数
方法 1.动态规划
思路:
状态定义:
dp[i][0]
表示从第 0 项到第 i 项范围内的子数组的最小乘积,dp[i][1]
表示从第 0 项到第 i 项范围内的子数组的最大乘积初始状态:
dp[0][0]=nums[0], dp[0][1]=nums[0]
分情况讨论:
不和别人乘,就
nums[i]
自己num[i]
是负数,希望乘上前面的最大积num[i]
是正数,希望乘上前面的最小积状态转移方程:
dp[i] [0]=min(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])
dp[i] [1]=max(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])
状态压缩:
dp[i][x]
只与dp[i][x]-1
,所以只需定义两个变量,prevMin = nums[0]
,prevMax = nums[0]
状态压缩之后的方程:
prevMin = Math.min(prevMin * num[i], prevMax * num[i], nums[i])
prevMax = Math.max(prevMin * num[i], prevMax * num[i], nums[i])
复杂度:时间复杂度
O(n)
,空间复杂度O(1)
js:
62. 不同路径 (medium)
视频讲解:传送门
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7 输出:28 示例 2:
输入:m = 3, n = 2 输出:3 解释:从左上角开始,总共有 3 条路径可以到达右下角。
向右 -> 向下 -> 向下
向下 -> 向下 -> 向右
向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3 输出:28 示例 4:
输入:m = 3, n = 3 输出:6
提示:
1 <= m, n <= 100 题目数据保证答案小于等于 2 * 109
方法 1.动态规划
思路:由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的总和,状态转移方程:
f[i][j] = f[i - 1][j] + f[i][j - 1]
;复杂度:时间复杂度
O(mn)
。空间复杂度O(mn)
,优化后O(n)
js:
312. 戳气球 (hard)
视频讲解:传送门
有 n 个气球,编号为 0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1 或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
示例 1:输入:nums = [3,1,5,8]输出:167 解释:nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []coins = 315 + 358 + 138 + 181 = 167 示例 2:
输入:nums = [1,5]输出:10
提示:
n == nums.length1 <= n <= 3000 <= nums[i] <= 100
方法 1:动态规划
思路:
dp[i][j]
表示开区间(i,j)
能拿到的的金币,k 是这个区间 最后一个 被戳爆的气球,枚举i
和j
,遍历所有区间,i-j
能获得的最大数量的金币等于 戳破当前的气球获得的金钱加上之前i-k
、k-j
区间中已经获得的金币复杂度:时间复杂度
O(n^3)
,n 是气球的数量,三层遍历。空间复杂度O(n^2)
,dp 数组的空间。
js:
279. 完全平方数 (medium)
视频讲解:传送门
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4 示例 2:
输入:n = 13 输出:2 解释:13 = 4 + 9 提示:
1 <= n <= 104
方法 1:动态规划
思路:
dp[i]
表示i
的完全平方和的最少数量,dp[i - j * j] + 1
表示减去一个完全平方数j
的完全平方之后的数量加 1 就等于dp[i]
,只要在dp[i]
,dp[i - j * j] + 1
中寻找一个较少的就是最后dp[i]
的值。复杂度:时间复杂度
O(n* sqrt(n))
,n是输入的整数,需要循环n次,每次计算dp方程的复杂度sqrt(n)
,空间复杂度O(n)
js:
509. 斐波那契数(easy)
视频讲解:传送门
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1F(n) = F(n - 1) + F(n - 2),其中 n > 1 给定 n ,请计算 F(n) 。
示例 1:
输入:n = 2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1 示例 2:
输入:n = 3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2 示例 3:
输入:n = 4 输出:3 解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
方法 1.动态规划
思路:自底而上的动态规划
复杂度分析:时间复杂度
O(n)
,空间复杂度O(1)
Js:
64. 最小路径和 (medium)
视频讲解:传送门
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]输出:7 解释:因为路径 1→3→1→1→1 的总和最小。示例 2:
输入:grid = [[1,2,3],[4,5,6]]输出:12
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 2000 <= grid[i][j] <= 100
思路:
dp[i][j]
表示从矩阵左上角到(i,j)
这个网格对应的最小路径和,只要从上到下,从左到右遍历网格,当前最小路径和就是当前的数值加上上面和左边左小的。复杂度:时间复杂度
O(mn)
,m、n 分别是矩阵的长和宽。空间复杂度如果原地修改是O(1)
,如果新建 dp 数组就是O(mn)
js:
0-1 背包问题
0-1 背包问题指的是有n
个物品和容量为j
的背包,weight
数组中记录了n
个物品的重量,位置i
的物品重量是 weight[i],value
数组中记录了n
个物品的价值,位置 i 的物品价值是vales[i]
,每个物品只能放一次到背包中,问将那些物品装入背包,使背包的价值最大。
举例:
我们用动态规划的方式来做
状态定义:
dp[i][j]
表示从前 i 个物品里任意取,放进容量为 j 的背包,价值总和最大是多少状态转移方程:
dp[i][j] = max(dp[i - 1][j]
,dp[i - 1][j - weight[i]] + value[i])
; 每个物品有放入背包和不放入背包两种情况当
j - weight[i]<0
:表示装不下i
号元素了,不放入背包,此时dp[i][j] = dp[i - 1][j]
,dp[i] [j]取决于前i-1
中的物品装入容量为j
的背包中的最大价值当
j - weight[i]>=0
:可以选择放入或者不放入背包。放入背包则:dp[i][j] = dp[i - 1][j - weight[i]] + value[i]
,dp[i - 1][j - weight[i]]
表示i-1
中的物品装入容量为j-weight[i]
的背包中的最大价值,然后在加上放入的物品的价值value[i]
就可以将状态转移到dp[i][j]
。不放入背包则:dp[i][j] = dp[i - 1] [j]
,在这两种情况中取较大者。初始化 dp 数组:
dp[i][0]
表示背包的容积为 0,则背包的价值一定是 0,dp[0][j]
表示第 0 号物品放入背包之后背包的价值最终需要返回值:就是 dp 数组的最后一行的最后一列
循环完成之后的 dp 数组如下图
js:
状态压缩
根据状态转移方程dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
,第 i 行只与第 i-1 行状态相关,所以我们可以用滚动数组进行状态压缩,其次我们注意到,j 只与 j 前面的状态相关,所以只用一个数组从后向前计算状态就可以了。
416. 分割等和子集 (medium)
视频讲解:传送门
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。示例 2:
输入:nums = [1,2,3,5]输出:false 解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 2001 <= nums[i] <= 100
思路:本题可以看成是 0-1 背包问题,给一个可装载重量为
sum / 2
的背包和 N 个物品,每个物品的重量记录在 nums 数组中,问是否在一种装法,能够恰好将背包装满?dp[i][j]
表示前 i 个物品是否能装满容积为 j 的背包,当dp[i][j]
为 true 时表示恰好可以装满。每个数都有放入背包和不放入两种情况,分析方法和 0-1 背包问题一样。复杂度:时间复杂度
O(n*sum)
,n 是 nums 数组长度,sum 是 nums 数组元素的和。空间复杂度O(n * sum)
,状态压缩之后是O(sum)
js:
322. 零钱兑换 (medium)
视频讲解:传送门
给你一个整数数组 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
提示:
1 <= coins.length <= 121 <= coins[i] <= 231 - 10 <= amount <= 104
不能用贪心做,反例,coins=[1, 3, 5, 6, 7]
,amount=30
,用贪心先用最大的面额 7,在用 2 个 1,4 * 7 + 2 * 1 = 30
,但是我们用 5 个 6,5 * 6 = 30
就能用最少的硬币兑换完成
方法 1.动态规划
思路:
dp[i]
表示兑换面额i
所需要的最少硬币,因为硬币无限,所以可以自底向上计算dp[i]
,对于dp[0~i]
的每个状态,循环coins
数组,寻找可以兑换的组合,用i
面额减去当前硬币价值,dp[i-coin]
在加上一个硬币数就是dp[i]
,最后取最小值就是答案,状态转移方程就是dp[i] = Math.min(dp[i], dp[i - coin] + 1)
;复杂度分析:时间复杂度是 O(sn),s 是兑换金额,n 是硬币数组长度,一共需要计算 s 个状态,每个状态需要遍历 n 个面额来转移状态。空间复杂度是
O(s)
,也就是 dp 数组的长度
Js:
198. 打家劫舍 (medium)
视频讲解:传送门
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
提示:
1 <= nums.length <= 1000 <= nums[i] <= 400
思路:
dp[i]
表示 0-i 能偷的最大金额,dp[i]
由两种情况中的最大值转移过来dp[i - 2] + nums[i]
表示偷当前位置,那么 i-1 的位置不能偷,而且需要加上dp[i-2]
,也就是前 i-2 个房间的金钱dp[i - 1]
表示偷当前位置,只偷 i-1 的房间复杂度:时间复杂度
O(n)
,遍历一次数组,空间复杂度O(1)
,状态压缩之后是O(1)
,没有状态压缩是O(n)
js:
评论