动态规划
1.动态规划概述
动态规划,Dynamic Programming,简称DP
递归和动态规划的区别
递归是自顶而上的,从顶部开始分解问题,然后通过解决分解出的小问题,最后解决整个问题。
动态规划是自底而上的,从底部的初始化着手开始解决问题,然后逐步扩大问题的规模从而解决整个问题。
1.1 暴力枚举 && 回溯 && 动态规划的关系
(1) 暴力枚举:即通过暴力手段算出所有的可能性,然后找出最优解。从结果出发,这是没有问题的,但是从手段上来讲,这是“劳民伤财”的。有的人唯“结果论”,认为只要我算出结果就行了,管他什么方式手段。但是打个不恰当的比方,为了得到这个结果,我们付出了惨痛的代价,而且这个代价本不必要。所以暴力枚举就是通过极其惨烈的代价,得到一个结果,而这个代价本来可以避免。因此,对手段的优化是很有必要的,要用最少的代价,得到想要的结果。
(2) 回溯算法:回溯的本质也是暴力枚举的一种,每次走到岔路口的时候,尝试当前的所有分支,当我们走到不通的分支之后可以通过递归的return回到上一个函数中。
(3) 动态规划:动态规划同样也有岔路口和分支的概念。只不过走到分叉路口时,可以直接根据前面各分支的表现,直接推导出下一步的最优解!然而无论是直接推导,还是前面各分支判断,都是有条件的。
1.2 动态规划(三个条件)
1. 存在最优子结构
2. 存在重复子问题
这里指的是,能否判定子问题和原问题属于同一性质的问题。也就是说,当我们题目的变量缩小的时候,题目的性质是否发生变化。比如:爬楼梯问题中楼梯有20个,和楼梯有19个,问题的性质不变。比如:字符串S1,S2的最大公共字符串,如果S1,S2同时去掉最后一个字符,问题的性质是否不变。
此外,问题范围所有后,能否和原问题建立联系。即dp[i-1]和dpp[i]能够相互转化。
3. 无后效性
2.动态规划分类
2.1 一维动态规划
2.2 二维动态规划
最长递增自序列
最长重复子数组
3.动态规划问题
3.1 最长递增子序列
var lengthOfLIS = function(nums) {
const dp = []
dp[0] = 1
for (let index = 1; index < nums.length; index++) {
dp[index] = 1;
for (let preIndex = 0; preIndex < index; preIndex++) {
if (nums[preIndex] < nums[index] && (dp[preIndex] + 1) > dp[index]) {
dp[index] = dp[preIndex] + 1
}
}
}
return Math.max(...dp)
};
3.2 零钱兑换【中等】
什么时候要考虑动态规划?:
从直觉上来看,这种题目属于答案存在多种组合都可以完成,但是我们需要从这些组合中找出一种最优的方案,这个时候一般就可以来考虑动态规划了!
解析:
我们先来看看贪心算法的问题,按照贪心算法的逻辑是,我每次尽兑换最大面额的硬币,以便尽可能保证消耗的金额数减少。但是他的问题在于,当剩下的面额只够最小面额的硬币兑换的时候那么此时数量上不一定最优。例如:1,5,7面额,但是兑换的总钱数是17,那么按照贪心算法先兑换最大的两个7,然后剩下3只能兑换3个1.但是如果我兑换1个7和两个5,那么才是硬币数量最少的情况。那么我们来看重复子问题:零钱兑换面额为17的问题,可不可以转化为当零钱兑换面额分别为17-7,17-5,17-1的时候,面额最少的情况,然后在这种情况下+1,就是当前最少的零钱数量。
那么基于这种情况,我们知道状态转移方程是:Math.max(DP[sum - 7], DP[sum - 5], DP[sum - 1])。那么我们在看下边界条件:
var coinChange = function(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity)
const dpMap = new Map()
for (let index = 0; index < (amount + 1); index++) {
if (index === 0) {
dp[0] = 0
} else {
for (let coin of coins) {
if (index - coin < 0) {
continue
} else {
dp[index] = Math.min(dp[index], dp[index - coin] + 1)
}
}
}
}
console.log(111, dp)
return dp[amount] === Infinity ? -1: dp[amount]
};
3.3 最长公共子序列问题
解析
首先先把问题定性,公共子序列和公共子串是不一样的,子序列中每个字符串是可以分隔的,而子串必须是连续的。 其次,从哪里知道需要通过“动态规划”来解决这个问题 然后,在已知动态规划的作为解题方法的前提下,怎么确定状态转移方程,怎么确定dp数组是一维数组还是二维数组。数组下标的[i][j]分别代表的含义是什么。
dp构建二维数组表:
这里dp[i][j]的含义是字符串strs-1的前i个元素和字符串strs-2的前j个元素,dp[i][j],就是在这两个字符串的最大公共子串长度。
那么对应的状态转移方程是:max { dp[i-1][j], dp[i][j-1] }
3.4 打家劫舍
function rob(nums: number[]): number {
let result = 0
let dp = []
for (let index = 0; index < nums.length; index++) {
if (index === 0) {
dp[0] = nums[index]
}
if (index === 1) {
dp[1] = Math.max(dp[0], nums[1])
}
if (index > 1) {
dp[index] = Math.max((dp[index - 2] + nums[index]), dp[index - 1])
}
}
console.log(11111, dp)
return dp[nums.length - 1]
};
3.5 最长回文子序列【中等】
3.6 最大子数组和【中等】
case1
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。
case2
输入: nums = [1]
输出: 1
看到求最值问题,可以尝试往动态规划上考虑一下。
先来分析问题
问题定性:一个数组中找出“和最大”的子数组。那么数组肯定是连续的。也就是说答案是中间某一段连续的,不是可以跳跃的。这也是与“最长递增子序列”问题的区别。
问题的主要矛盾:我怎么知道是中间哪一段呢?这个时候我们可以先将问题简单化,再把问题复杂化。
先将【问题简单化】,即将问题的范围缩小,即如果数组是一个元素【-2】那么这个时候最大子数组和是不是立马就知道了。如果数组是两个元素【-2,1】那么结果是不是也可以算出来。
然后再将【问题复杂化】:那么如果数组长度为i-1, 和数组长度为i,那么我能不能算出来呢? 即如果我在已知数组i-1结果的前提下,能不能算出多一个元素arr【i】的数组i的结果?
其实本质上,多出一个元素arr【i】对数组i-1和i的统计结果的具体影响是什么?不就是多出来的以arr【i】为结尾的一系列子数组吗?那么我只需要将这一系列子数组中找出最大和,再和原来的i-1问题最大值比较,是不是就是问题i的最大值?
如果还不理解,我们可以先从宏观上看这个问题,是不是可以先将这个数组中所有的子数组列下来,归类就是所有以向前元素为结尾的数组,
所以问题的核心是:
DP【i】的定义是以arr【i】为结尾的数组和最大值,也就是说我们要构建的dp数组是以每一个元素为结果的数组最大值,然后问题的解决是从dp数组中再找出最大值,就是我们最后求的结果。有时候我们不能一口吃成胖子,希望dp【i】的结果就是问题i的结果,也可能max(dp)的结果才是问题的结果,不可急于求成。
而状态转移方程是:dp【i】= Math.max(dp【i-1】+arr【i】, arr【i】),即在前一个i-1为结尾的最大结果数组后加上arr【i】,再和单独新增的子数组【arr【i】】相比较。
解题答案:
解答1:
function maxSubArray(nums: number[]): number {
const dp = []
for (let index = 0; index < nums.length; index++) {
if (index === 0) {
dp[0] = nums[0]
} else {
dp[index] = Math.max(dp[index - 1] + nums[index], nums[index])
}
console.log(111, dp)
}
return Math.max(...dp)
};
优化存储空间后:
解答2:
function maxSubArray(nums: number[]): number {
let preDp
let max
for (let index = 0; index < nums.length; index++) {
if (index === 0) {
preDp = nums[0]
max = nums[0]
} else {
const curDp = Math.max(preDp + nums[index], nums[index])
preDp = curDp
max = Math.max(max, curDp)
}
}
return max
};
个人认为先理解优化前,构建dp数组的方案,理解dp数组如何构建才是能否解题的主要矛盾。