动态规划介绍
动态规划能做的,递归也能做,只是递归中会有很多重复的子问题,动态规划将这些子问题存储起来,需要用到的时候直接访问,相当于用空间复杂度换取时间复杂度,有时候甚至保存的内容不是很长(比如斐波那契数列),空间复杂度可以只有O(1)
将「动态规划」的概念关键点抽离出来描述就是这样的:
- 动态规划法试图只解决每个子问题一次
- 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
509. 斐波那契数列
var fib = function(N) {
if(N === 0){
return 0
}else if(N === 1){
return 1
}else{
// return fib(N - 1) + fib(N - 2)
//dp
let dp = [0, 1]
for(let i = 2; i <= N; i++){
let temp = dp[1]
dp[1] = dp[0] + dp[1]
dp[0] = temp
}
return dp[1]
}
};
70. 爬楼梯
var climbStairs = function(n) {
// //递归 超时
// if (n === 1) return 1;
// if (n === 2) return 2;
// return climbStairs(n - 1) + climbStairs(n - 2)
//动态规划 空间复杂度减少了,因为狠多的重复计算没有了
if (n <= 2) return n;
let pre = 2,
prepre = 1;
for (i = 3; i <= n; i++) {
//第i层的爬的方法等于 i-1 和i-2层的方法和
cur = pre + prepre;
prepre = pre;
pre = cur;
}
return cur;
};
62. 不同路径
mxn的网格中,到达右下角的方式。
行遍历,每次只存一行。
var uniquePaths = function(m, n) {
//动态规划
// if ( m <= 1 && n <= 1) return 1;
// let dp = [];
// for (i = 0; i < m; i++) {
// dp[i] = [];
// for (j = 0; j < n; j++) {
// if (i == 0 || j == 0) {
// dp[i][j] = 1;
// } else {
// dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
// }
// }
// }
// return dp[m - 1][n - 1];
//动态规划——空间优化版
if ( m <= 1 && n <= 1) return 1;
let dp = [];
for (i = 0; i < m; i++) {
//一次写完一行,下一行时, dp[i][j] 需要d[i-1][j]和d[i][j-1],这两个一个是上一行的,一个是刚才更新的,也正好就是当前这行的 j 和j -1
for (j = 0; j < n; j++) {
if (i == 0 || j == 0) {
dp[j] = 1;
} else {
dp[j] = dp[j] + dp[j - 1]; //上边和左边
}
}
}
return dp[n - 1]
};
53. 最大子序和
var maxSubArray = function(nums) {
//暴力双重循环,超时
// let maxSum = 0
// for(let i=0;i<nums.length;i++){
// let sum = 0
// for(let j = i;i<nums.length;j++){
// sum += nums[j]
// maxSum = Math.max(maxSum, sum)
// }
// }
// return maxSum
//dp,当前的最大和只与前一个有关
let dp = new Array(nums.length)
dp[0] = nums[0]
for(let i=1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1], 0) + nums[i] //包含i的并且到i为止的最大连续和,只要之前的和为负,就抛弃前面的。
}
return Math.max(...dp)
};
322. 零钱对换
从0-target的dp都进行计算
var coinChange = function(coins, amount) {
//使用回溯和剪枝会超时
//每次都可以使用当前,也可以不使用当前,当价格相等的时候就可以比较,大于就返回
//dp
let len = coins.length
let dp = new Array(amount + 1).fill(Infinity) //已经初始化了
dp[0] = 0
for(let i = 1 ; i <= amount; i++){
//1-amount 的dp依次计算
for(let coin of coins){
if(coin <= i){ //相等时为 dp置为 1,大于的话不可能
dp[i] = Math.min(dp[i], dp[i - coin] + 1) //不可能就存为无穷,当前价格减去当前硬币面值的价格所需的最少个数,也就是对当前价格分别减去所有硬币价格的价格进行比较,得出最大的。
}
}
}
return dp[amount] > amount ? -1 :dp[amount] //也可能不存在解
};
64. 最小路径和
遍历到的点,按照边界情况,左和上分别考虑即可
var minPathSum = function(grid) {
if (grid.length == 0) return 0;
if (grid.length === 1 && grid[0].length === 1) return grid[0][0];
//动态规划
let m = grid.length,
n = grid[0].length,
dp = [...grid]; //会修改原数组,但是这里前面的变化不会影响后面的,所以没关系
for(i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
if (i == 0 && j == 0) dp[i][j] = grid[i][j];
else if (i == 0 && j != 0) dp[i][j] = grid[i][j] + dp[i][j - 1];
else if (i != 0 && j == 0) dp[i][j] = grid[i][j] + dp[i - 1][j];
else dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m - 1][n - 1];
};
96. 不同的二叉搜索树
//动态规划
//因为左边和右边都是连续数字,所以不管是多少,只要连续的数字个数一样,那么能组成的树的个数也一样
var numTrees = function (n) {
let dp = new Array(n+1).fill(0)
dp[0] = 1
dp[1] = 1
for(let i = 2;i<=n;i++){
//i个数字的的子树有i-1个节点
for(let j = 0;j<=i-1;j++){ //j为左边个数
dp[i] +=dp[i-1-j]*dp[j]
}
}
return dp[n]
};
211. 最大正方形
var maximalSquare = function(matrix) {
//注意,是字符串
//返回的是面积
//动态规划,当前点作为正方形的右下角
//为0,则大小为0
//为1,则看左边,上边,左上边的点各自最大正方形,短板原理,三者合并,三者中最短的+1就是当前点的最大正方形
let dp = []
let len = matrix.length
if (len < 1) return 0
let result = 0
for (let i = 0; i < len; i++) { //行遍历
dp[i] = []
let len2 = matrix[i].length
for (let j = 0; j < len2; j++) { //列遍历
if (i === 0 || j === 0) {
dp[i][j] = parseInt(matrix[i][j])
} else {
if (matrix[i][j] === '1') {
dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j - 1], dp[i - 1][j]) + 1
} else {
dp[i][j] = 0
}
}
result = Math.max(dp[i][j], result)
}
}
return result * result
};
1277. 统计全为1的正方形子矩阵
var countSquares = function(matrix) {
// 与211基本一样
//当前点所在的最大正方形的,边长的长度就是以当前点为右下角的正方形的个数
//动态规划,当前点作为正方形的右下角
//为0,则大小为0
//为1,则看左边,上边,左上边的点各自最大正方形,短板原理,三者合并,三者中最短的+1就是当前点的最大正方形
let dp = []
let len = matrix.length
if (len < 1) return 0
let result = 0
for (let i = 0; i < len; i++) { //行遍历
dp[i] = []
let len2 = matrix[i].length
for (let j = 0; j < len2; j++) { //列遍历
if (i === 0 || j === 0) {
dp[i][j] = matrix[i][j]
} else {
if (matrix[i][j] === 1) {
dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j - 1], dp[i - 1][j]) + 1
} else {
dp[i][j] = 0
}
}
result += dp[i][j]
}
}
return result
};
338. 比特位计数
计数为是前一个偶数位加1,偶数位与二分之一一样
var countBits = function(num) {
//动态规划
//奇数的1的个数,是前一个最后一位变1,所以是dp[i] = dp[i-1]
//偶数是x/2往左移一位得到的,所以与其1的个数一样
let dp=new Array(num + 1)
dp[0] = 0
for(let i = 1; i <= num; i++){
if(i % 2 === 1){
dp[i] = dp[i - 1] + 1
}else{
dp[i] = dp[i / 2]
}
}
return dp
};
198. 打家劫舍
var rob = function(nums) {
//不是直接奇偶就解决了!!!
//
let prepre = 0, //前前一家抢没抢的最大值
pre = 0, //前一家抢没抢的最大值
cur = 0;
for (i = 0; i < nums.length; i++) {
cur = Math.max(prepre + nums[i], pre); //当前抢不抢的最大值 ==========>把这间想成是最后一间(如果前一个没抢,那么pre就是prepre,如果前一个抢了,就可以比大小了)
//k-1 如果抢了,那么k不能抢,k-1没抢,那么等价于k-2的,此时k可以抢
prepre = pre;
pre = cur;
}
return cur
};
343. 整数拆分
要0-n 所有拆分的最优值,与零钱兑换有点类似
var integerBreak = function(n) {
let max = 0
let dp = new Array(n + 1).fill(0)
dp[0] = 0
dp[1] = 0
dp[2] = 1
for(let i = 3; i <= n; i++){
for(let j = 1; j<=i-1;j++){ // 1 ~ i-1
//按照前面拆过的/ 拆一次/拆一次之后剩下的部分还需要继续拆
dp[i] = Math.max(dp[i], j * (i - j), j * dp[i - j])
}
}
return dp[n]
};
股票问题系列
121. 买卖股票的最佳时机
注意:除了初始状态,每个状态都是由2部分取 max 得来
只能买一次(买入与之前的都无关,就的那个前面什么都没操作)
var maxProfit = function(prices) {
// let dp = [] //一个存状态dp[i][0/1] 第二个状态表示是否持有,1下标存储持有
// for(let i = 0; i < prices.length; i++){
// dp[i] = []
// if(i === 0){
// dp[0][1] = -prices[0] //开始就持有,说明开始就买了
// dp[0][0] = 0
// continue
// }
// dp[i][1] = Math.max(- prices[i], dp[i - 1][1]) //如果持有的话要么当前买的要么之前买了一直持有,与之前不持有的无关
// dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
// }
// return dp[prices.length - 1]? dp[prices.length - 1][0] : 0 //最后持有的话,肯定不是最优的, 不如不买!!,输入为空的情况
// //空间优化
// let dp = [] //是否持有,只需要存前一天的即可
// for(let i = 0; i < prices.length; i++){
// if(i === 0){
// dp[1] = -prices[0]
// dp[0] = 0
// continue
// }
// dp[1] = Math.max(- prices[i], dp[1])
// dp[0] = Math.max(dp[0], dp[1] + prices[i])
// }
// return dp[0]
//一次遍历,不是动态规划
//遍历的过程中,记录之前出现的最低点, 当前卖出的话一定是希望之前是在最低点买入的
let minPrice = prices[0]
let result = 0
for(let i = 1; i< prices.length; i++){
minPrice = Math.min(minPrice, prices[i])
result = Math.max(result, prices[i] - minPrice)
}
return result
};
122. 买卖股票的最佳时机2
买的次数任意,再次买入的时候不用重置
var maxProfit = function(prices) {
// let dp = [] //一个存状态dp[i][0/1] 第二个状态表示是否持有
// for(let i = 0; i < prices.length; i++){
// dp[i] = []
// if(i === 0){
// dp[0][1] = -prices[0]
// dp[0][0] = 0
// continue
// }
// dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1])
// dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i])
// }
// return dp[prices.length - 1]? dp[prices.length - 1][0] : 0 //最后持有的话,肯定不是最优的, 不如不买!!,输入为空的情况
//空间优化
// let dp = [0] //当前是否持有
// for(let i = 0; i < prices.length; i++){
// if(i === 0){
// dp[1] = - prices[0]
// dp[0] = 0
// continue
// }
// dp[1] = Math.max(dp[0] - prices[i], dp[1])
// dp[0] = Math.max(dp[0], dp[1] + prices[i])
// }
// return dp[0]
//只用2个变量 不用数组
let dp_0 = 0 //当前是否持有
let dp_1 = - prices[0]
for(let i = 1; i < prices.length; i++){
dp_1 = Math.max(dp_0 - prices[i], dp_1)
dp_0 = Math.max(dp_0, dp_1 + prices[i])
}
return dp_0
};
188. 最多交易k次
- 第一天初始化,每天的从未卖出过单独考虑,只记录一天的,因为后一天的只需要有前一天的最优结果即可。
- max_k 为输入的 k 与 二分之一天数的最小值。
var maxProfit = function(k, prices) {
//空间优化,还是只需要存上一天的就行
if(prices.length <=1) return 0
let max_k = Math.floor(Math.min(prices.length / 2, k))
let dp = new Array(max_k + 1) //存放每一天还能卖出次数对应的最优值
for(let j = 0; j <= max_k; j++){
dp[j] = []
}
for(let i = 0; i < prices.length; i++){
for(let j = 0; j <= max_k; j++){
if(i === 0){
if(j === max_k){
dp[j][0] = 0 //第一天啥都每做
dp[j][1] = -prices[i] //第一天买入
}else{
dp[j][0] = -Infinity
dp[j][1] = -Infinity
}
}else{
if(j === max_k){ //边界单独处理
dp[j][0] = dp[j][0]
dp[j][1] = Math.max(dp[j][1], dp[j][0] - prices[i])
}else{
dp[j][0] = Math.max(dp[j][0], dp[j + 1][1] + prices[i]) //上一天的未持有,上一天的持有在今天卖出了
dp[j][1] = Math.max(dp[j][1], dp[j][0] - prices[i]) //上一天持有,上一天未持有今天买了
}
}
}
}
return dp.reduce((pre,cur)=>{
return Math.max(pre, cur[0]) //最后一次一定是未持有比较好
},0)
};