文章目录
动态规划基础知识
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
DP解题思路:
① 确定dp数组(dp table)以及下标的含义
② 确定递推公式
③ dp数组如何初始化
④ 确定遍历顺序
⑤ 举例推导dp数组
简单
爬楼梯
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶
示例2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
思路
- 确定dp数组以及下标的含义
dp[i]的定义为:爬i阶台阶的方法 - 确定递推公式
状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]; - dp数组如何初始化
dp[1] = 1;
dp[2] = 2; - 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的 - 举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
1 2 3 5 8 13 21 34 55 89
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
基础
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
let dp=[]
dp[1]=1,dp[2]=2
for(let i=3; i<=n; i++){
dp[i]=dp[i-1]+dp[i-2]
}
return dp[n]
};
优化
优化空间复杂度:dp[n]只与dp[n-1]、dp[n-2]有关。只需用两个变量存储
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
let pre=1,cur=2
if(n<3) return n
for(let i=3; i<=n; i++){
let tmp=pre+cur
pre=cur
cur=tmp
}
return cur
};
使用最小花费爬楼梯
使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
思路
基础
var minCostClimbingStairs = function (cost) {
let res = 0,
len = cost.length;
let dp = [];
if (len == 1) {
res = cost[0];
return res;
}
if (len == 2) {
res = Math.min(cost[0], cost[1]);
return res;
}
(dp[0] = cost[0]), (dp[1] = cost[1]);
for (let i = 2; i < len; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
res = Math.min(dp[len - 1], dp[len - 2]);
return res;
};
打家劫舍
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
基础
二维数组
dp[i][0]:第i家不偷时的最多钱
dp[i][1]:第i家偷时的最多钱
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
let len=nums.length
let dp=Array(len).fill(0).map(item => Array(2).fill(0))
dp[0][0]=0, dp[0][1]=nums[0]
let res=0
if(len==1) return nums[0]
for(let i=1; i<len; i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1])
dp[i][1]=dp[i-1][0]+nums[i]
res=Math.max(dp[i][0],dp[i][1])
}
return res
};
优化一
一维数组
var rob = function(nums){
let dp=[],len=nums.length
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-1],dp[i-2]+nums[i])
}
return dp[len-1]
}
优化二
优化空间复杂度:dp[n]只与dp[n-1]、dp[n-2]有关。只需用两个变量存储
let pre=0,cur=0
for(let num of nums){
let tmp=Math.max(cur,pre+num)
pre=cur
cur=tmp
}
return cur
中等
不同路径
不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
思路
第一感觉用深度优先搜索,把问题就可以转化为求二叉树叶子节点的个数。
写出来,发现超时:
这棵树的深度其实就是m+n-1(深度按从1开始计算)。
那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已)
所以上面深搜代码的时间复杂度为 O ( 2 m + n − 1 − 1 ) O(2^{m + n - 1} - 1) O(2m+n−1−1),可以看出,这是指数级别的时间复杂度,是非常大的。
基础
var uniquePaths = function (m, n) {
let dp = new Array(m).fill(0).map((item) => new Array(n).fill(0));
for (let i = 0; i < m; i++) {
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];
};
不同路径Ⅱ
不同路径Ⅱ
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
思路
基础
var uniquePathsWithObstacles = function (obstacleGrid) {
let m = obstacleGrid.length,
n = obstacleGrid[0].length;
let dp = new Array(m).fill(0).map((item) => new Array(n).fill(0));
for (let i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (let j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 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];
};
最长公共子序列
最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
在这里插入代码片
最长公共子串
问题:有两个字符串str和str2,求出两个字符串中最长公共子串长度。
比如:str=acbcbcef,str2=abcbced,则str和str2的最长公共子串为bcbce,最长公共子串长度为5。
思路
基础
var getLCSLength = function (str1, str2) {
let len1 = str1.length,
len2 = str2.length;
let res = 0;
let item = new Array(len1).fill(0).map((v) => new Array(len2).fill(0));
for (let i = 0; i < len1; i++) {
for (let j = 0; j < len2; j++) {
if (str1[i] == str2[j]) {
if (i > 0 && j > 0) {
item[i][j] = item[i - 1][j - 1] + 1;
} else {
item[i][j] = 1;
}
res = Math.max(res, item[i][j]);
}
}
}
return res;
};
背包问题
对于面试来说,一般掌握01背包问题和完全背包问题就够了
01背包
完全背包
背包的应用
分割等和子集
分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
注意:题目来源于力扣。