前端面试--什么是动态规划?

前言

最近作了一些leetcode的动态规划的算法题,原本我一个小小菜鸡是不配来写这个东西的,可是也壮着胆子来写一篇本身对于动态规划的理解和作题的思路,望各路大佬留情。算法

什么是动态规划

引用百度百科的一句话:数组

动态规划算法一般用于求解具备某种最优性质的问题。在这类问题中,可能会有许多可行解。每个解都对应于一个值,咱们但愿找到具备最优值的解。markdown

动态规划是一种思想,与分治思想很相似网络

分治问题的核心思想是:把一个问题分解为相互独立的子问题,逐个解决子问题后,再组合子问题的答案,就获得了问题的最终解。函数

动态规划的思想和“分治”有点类似。不一样之处在于,“分治”思想中,各个子问题之间是独立的:而动态规划划分出的子问题,每每是相互依赖、相互影响的。优化

何时该用动态规划

引用修言大佬的一句话ui

  1. 最优子结构 2.重叠子问题

最优子结构它指的是问题的最优解包含着子问题的最优解——无论前面的决策如何,此后的状态必须是基于当前状态(由上次决策产生)的最优决策。而重叠子问题,它指的是在递归的过程当中,出现了反复计算的状况。this

作题

首先上一道leetcode简单题:303. 区域和检索 - 数组不可变spa

示例:prototype

输入:
["NumArray""sumRange""sumRange""sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]

解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) 
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

来源:力扣(LeetCode)
连接:https://leetcode-cn.com/problems/range-sum-query-immutable
著做权归领扣网络全部。商业转载请联系官方受权,非商业转载请注明出处。
复制代码

这题为何定义为简单呢,你们能够想一下若是这题用暴力解法,那么咱们很容易获得结论

var NumArray = function (nums) {
    this.nums = nums
};
NumArray.prototype.sumRange = function (i, j) {
    const arr = this.nums.slice(i, j + 1)
    return arr.reduce((a, c) => a + c)
};
复制代码

我给你们跑一下运行结果:

结果非常惨淡,执行用时1064ms,内存消耗47.1MB。为何呢?由示例咱们能够看到,sumRange的这个函数是会屡次调用的,而咱们在里面评分的切割数组,而且使用reduce去求和,这个消耗无疑是很大的。

那还有什么解决方法呢? 动态规划能够吗?能够!

思路:为了不上面的屡次调用的状况,咱们能够把该作的操做在new的时候一步到位,在调用sumRange的函数的直接返回就行。

那到底该怎么用动态规划来优化呢?

个人思路是,在new这个函数的时候在函数内部维护一个dp数组dp数组内存储传入数组nums的当前项nums[i]与i以前全部项的和。这样咱们在调用sumRange函数时只须要返回dp[j]-dp[i]

new函数代码以下:

var NumArray = function (nums) {
    this.dp = []
    const dp = this.dp
    dp[0] = nums[0]
    for (let i = 1; i < nums.length; i++) {
        dp[i] = dp[i - 1] + nums[i]
    }
};
复制代码

此时咱们就维护好了dp数组,接下来就很简单了

   NumArray.prototype.sumRange = function (i, j) {
    const dp = this.dp;
    if (i === 0) return dp[j]
    return dp[j] - dp[i - 1]
};
复制代码

为何要加一个i===0的判断?

由于咱们在i=0的时候,就至关于拿前j项的和,而且此时时没有i-1项的。当i>1时,数学你们都学的比我好,很容易得出dp[j] - dp[i - 1]

咱们来跑下结果

完美!

第二题:leetcode中等题:198. 打家劫舍

题目:

示例:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,而后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

来源:力扣(LeetCode)
连接:https://leetcode-cn.com/problems/house-robber
著做权归领扣网络全部。商业转载请联系官方受权,非商业转载请注明出处。
复制代码

这题目简单的理解就是,规则是不能取数组相邻项,返回规则下的最大值

这题咱们主要考虑两种状况(对于第i家):

    1. 偷:若是咱们偷了第i家,那么咱们确定不能偷第i-1家,那么此时咱们的最大值就是第i家的钱加上咱们i-2时偷到的最大值。
    1. 不偷:若是咱们不偷第i家,那么此时咱们偷到钱的最大值,就是在第i-1家时偷到的最大值。

对于示例的数组,咱们画图分析: 对于第一家,第一家比较穷,只有1块钱,全部咱们第一家偷到的最多钱就是1块钱,对应的脑图和dp数组为:

第二家,中层领导,有2块钱,此时咱们有两种方案:

    1. 偷第二家,此时咱们就不能偷第一家,由于他们相邻,都偷会引来警察叔叔,因此这时候咱们只偷第二家。对于脑图和dp数组为:

此时咱们能偷到的最多钱为2

    1. 不偷,有钱我不偷,哎就是玩!那么此时咱们的脑图和dp数组维持第一家的状况

显然 偷第二家比较划算 此时咱们dp[1]=2;

此时,偷到第二家能偷到的最多钱咱们很容易获得Math.max(dp[0],dp[1]) 也就是等于dp[1],也就是2块钱

对于第三家:高层领导,有3块钱,咱们依然有两种方案:

    1. 偷第三家,此时咱们就不能偷第二家,因此这时候咱们能偷到的最大钱数就是第三家的钱跟在第一家的时候偷到的钱的总和。对于脑图和dp数组为:
    1. 不偷第三家,那咱们能偷到的最多钱就是在第二家偷到的最多钱:

此时咱们用Math.max(dp[1]+nums[3],dp[2])取出最大值为4,这就是咱们第三家能偷到的最多钱。

对于第四家,一样的道理,你们能够本身理一下。这里我就直接上代码了

var rob = function (nums) {
    const len = nums.length
    if (len === 0) return 0;
    if (len === 1) return nums[0]
    if (len === 2) return Math.max(...nums)
    const dp = []
    dp[0] = nums[0]
    dp[1] = Math.max(nums[0], nums[1])
    for (let i = 2; i < len; i++) {
        dp[i] = Math.max((nums[i] + dp[i - 2]), dp[i - 1])
    }
    return dp[dp.length - 1]
};
复制代码

最后跑一下运行结果:

完美经过!

总结

个人理解为:咱们在遇到一些题目的题解跟拆分出来的每一项小问题有关时,能够考虑使用动态规划的解题思路,能够清晰的理清题目的逻辑,帮助咱们快速找到答案!

因为本人技术有限,文内若有错误,敬请与我联系,谢谢!