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

我心飞翔 分类:javascript

前言

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

什么是动态规划

引用百度百科的一句话:

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

动态规划是一种思想,与分治思想很类似

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

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

什么时候该用动态规划

引用修言大佬的一句话

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

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

做题

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

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

示例:

输入:
["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]
};

最后跑一下运行结果:

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

完美通过!

总结

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

由于本人技术有限,文内如有错误,敬请与我联系,谢谢!

回复

我来回复
  • 暂无回复内容