前端算法系列(10):动态规划

483 阅读2分钟

概述

动态规划和很多其他算法都是通过子问题的解决来解决整体问题。

贪心是每一步都达到当前最优,是否最终最优不一定,这个过程要有一定证明。

深度优先搜索通过递归或栈解决子问题,其本身就是穷举+剪枝。

动态规划将问题分为重复子问题后,记录中间状态避免重复计算。

解决以上问题时都是要找出子问题。

这里暂时有个结论(没验证),贪心是特殊的动态规划,动态规划是特殊的搜索。因此贪心问题都可以通过动态规划来解决,动态规划问题都是可以搜索解决。

动态规划的基本使用

动态规划是自底向上,首先解决子问题,并记录结果,常规的搜索是自上而下,解决父问题过程中会重复遇到同一个子问题,因此首次计算时就保存下来。
这里通过结合搜索理解一下动态规划。

一般用动态规划获取最终结果,用搜索获取所有路径。

斐波那契数列为例,使用最常规的搜索

var fib = function (n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
};

中间有很多重复计算,接下来我们把每次的求解保存下来,下一次直接使用

var fib = function (n) {
  const map = new Map();
  return calc(n);
  function calc(n) {
    if (n < 2) return n;
    if (!map.has(n)) {
      map.set(n, calc(n - 1) + calc(n - 2));
    }
    return map.get(n);
  }
};

以上都是自上而下的搜索,接下来我们从子问题开始动态规划。

动态规划的几个步骤如下

  • 确定子问题,即dp[i]指的什么
  • 确定状态转移方程,dp[i]=f(i)
  • 初始化dp
  • 遍历填充dp

具体过程为:

  • 子问题是计算数列的每一项,即dp[i]指第i项的值
  • 状态转移方程是,当i>1时,dp[i]=dp[i-1]+dp[i-2]
  • 初始化,前两项的值
  • 遍历填充,依次计算各个值,直到第n项
var fib = function (n) {
  if (n < 2) return n;
  let dp = new Map([
    [0, 0],
    [1, 1],
  ]);

  for (let i = 2; i <= n; i++) {
    dp.set(i, dp.get(i - 1) + dp.get(i - 2));
  }
  return dp.get(n);
};

前面我们将n之前的值都保存了下来,但事实上,我们计算第i项时只需要i-1和i-2两项,因此我们要压缩空间只保存对应两项。

var fib = function (n) {
  if (n < 2) return n;
  let left = 0,
    right = 1;
  for (let i = 2; i <= n; i++) {
    [left, right] = [right, left + right];
  }
  return right;
};

dp结构

dp是用来保存中间过程的数据结构,每一步需要保存用来计算下一步的所有结果,元素之间的关系和每个元素需要保存的数据结构共同决定dp的最终结构。
通常来说,包括

  • 一维数组 元素本身是线性结构,且需要保存的数据只有一个
  • 二维数组 元素本身是二维结构,或需要保存的数据是数组
  • 其他结构 比如元素本身是树状的,需要用Map保存每个元素的数值,或每个元素对应的数值不是单个数或数组

例题

一维结构比如等差数列划分,子问题是以每个位置元素为结尾的等差数列数量,每个位置只需要记录这个数量

var numberOfArithmeticSlices = function(nums) {
let len=nums.length
if(len<3) return 0
const dp=new Array(len).fill(0)
for(let i=2;i<len;i++){
    if(nums[i]-nums[i-1]===nums[i-1]-nums[i-2]){
        dp[i]=dp[i-1]+1
    }
}
return dp.reduce((pre,cur)=>pre+cur,0)
};

二维结构比如不同路径,需要处理的元素本身位于一个二维数组中,因此dp也是二维的,子问题以dp[i][j]为结尾的矩阵有多少不同路径,每个位置只需要记录这个路径数


var uniquePaths = function(m, n) {
const dp=new Array(m).fill('').map(()=>new Array(n).fill(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]
};

其他结构,比如打家劫舍 III,元素本身就在非线性结构中,因此我们可以使用map保存各个节点对应的值,每个值是一个长度为2的数组。

var rob = function(root) {
let map=new Map()
map.set(null,[0,0])
dfs(root)
return Math.max(...map.get(root))

function dfs(root){
  if(root){
 dfs(root.left)
 dfs(root.right)
 map.set(root,[
   root.val+map.get(root.left)[1]+map.get(root.right)[1],
   Math.max(...map.get(root.left))+Math.max(...map.get(root.right))
  ])
  }
}
};

常见题型

这部分内容太多了,可参考代码随想录,未完待续。