每日算法-动态规划

Posted 前端启示录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每日算法-动态规划相关的知识,希望对你有一定的参考价值。

 2020年计划,每周5道算法题,由简到难,期待2021年的自己。




动态规划(Dynamic Programming)


算法中比较难的应该就是动态规划的算法啦。看了一些资料,总结一些,只是最浅析的分析,之后会用大量动态规划的题目再深入讲解。
按照我们的WWH流程来学习。

什么是动态规划

动态规划(dynamic programming)是解决多阶段决策问题常用的最优化理论。 

该理论由美国数学家 Bellman等人在1957年提出,用于研究多阶段决策过程的优化问题。该理论提出后,立即在数学、计算机科学、经济管理和工程技术领域得到了广泛的应用,例如,最短路线、库存管理、资源分配、设备更新、排序装载等问题。

用动态规划方法往往比朴素的方法更高效。动态规划方法的原理就是把多阶段决策过程转化为一系列的单阶段决策问题。利用各个阶段之间的递推关系,逐个确定每个阶段的最优化决策。最终堆叠出多阶段决策的最优化决策结果


为什么要用动态规划

因为动态规划要比穷举更加高效。穷举的时间复杂度是指数级的。动态规划的时间复杂度要简单的多。之后我们在题目中再做分析。


如何使用动态规划

应用场景:

首先我们要明确一点,每种方法都有其局限性,动态规划也不是万能的。动态规划适合求解多阶段(状态转换)决策问题的最优解。

是否能使用动态规划来解决,看其是否满足最优化原理和子问题的无后向性。

  • 最优化原理(最优子结构性质) :最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

  • 无后向性 :将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

 实现方式:

基本的实现方式有两种:
  • 自顶向下备忘录方式:保存子问题的结果,之后在求解某个子问题时,若其用到的某个子问题且已经保存了结果,直接返回。

  • 自底向上: 在求某个子问题时,会依赖已经求出的更小的子问题的解。


第一种方法符合平常的思维模式。第二种和第一种正好相反。

基本的动态规划算法都是用 「空间换时间」。通过牺牲空间,换来了时间效率,这称作 time-memory trade-off。 (但是在一些题目中,我们也可以优化空间复杂度)
三个重要的概念:

  • 最优子结构

  • 边界

  • 状态转移公式

说的多了不理解都是废话,拿题来练。我们先来一个最简单的题目。


爬台阶

有n阶台阶,你每次只能爬1个 或者 2个 ,问爬到顶会有多少种不同的方式。
这个题是动态规划中最简单的题,我们先从最简单的开始,逐步优化。首先一看当前题目,每次只能爬1 或者 2,分析一波。
分析F[ ]表示 i 个台阶有多少种爬的方式。
  • F] = 1;

  • ] = 2;

  • F] = F[ n-1 ] + F[ n-2 

典型的递归结构
求n阶台阶需要知道爬n-1阶和n-2阶台阶,有多少种爬的方式。以此递推,不断向下查找。
我们传统的方式就是直接递归。我们称之为「暴力法」。
代码实现如下:
function climbStairs(n) { if (n === 0 || n ===1 || n ===2) { return n; }  // 一直递归搜索 return climbStairs(n - 1) + climbStairs(n - 2);}climbStairs(40); 
时间复杂度:

每日算法-动态规划

一般这个方法,任何时间复杂度检测网站,都会报超时的。因为是指数级别的。

每日算法-动态规划

不难看出,我们在递归的过程中会存在一些节点重复计算的问题,上图中相同颜色的部分。就是因为这些重复会导致我们的时间浪费太多,如何解决一下?
比较简单的解决办法就是用一个备忘录来记录一下哪些被算过,如果之后的计算台阶数在备忘录中存在的话,就直接取用,不需要重新计算。使用记忆化方式,减少进入递归的次数,通过查备忘录的方式,直接获取结果。对于相同的台阶数,只计算一次。这样能极大的加快算法。
我们将这种称谓备忘录法」。

代码实现如下:

每日算法-动态规划

复杂度分析

  • 时间复杂度:O(n),树形递归的大小可以达到 n。

  • 空间复杂度:O(n),递归树的深度可以达到 n。

备忘录法可以极大程度上降低我们的时间复杂度。

以上是我们传统的思维和优化方式,接下来我们看一下动态规划的分析和实现。

动态规划法

我们使用动态规划需要了解三个重要的概念。

  • 最优子结构

  • 边界

  • 状态转移公式


我们来拆解一下这道题 假设 我们 有 10个台阶, 最后一步我们怎么走?  
只有两种方式: 
  1. 在第8台阶 上2阶 到达 

  2. 在第9台阶 上1阶 到达 到达

第10台阶只有两种方法,我们暂且不管如何到达第8阶和第9阶, 最后一步必然是从8阶或者9阶开始。  
假设: 0->8 有 x种方式 0->9 有 y种方式
到达10的总方式 就是 x+y,这个应该很好理解吧,我们计算的是多少种方式,而不是多少步数。
为了方便表达,我们写为 F(10) 到达10阶的总方式数 F(9) 到达9阶的总方式数 F(8) 到达8阶的总方式数 简写为:

F(10) = F(9)+F(8) 

按照刚才的思路接着往下推算

F(9) = F(8)+F(7) F(8) = F(7)+F(6) ······

到此,我们正在将一个复杂的问题分阶段的简化,逐步分解成简单的问题。 这就是动态规划的思想。 回到我们上面提到的三个重要的概念。
  • 最优子结构 :F(10) = F(9)+F(8) F(9) 和 F(8) 就是 F(10)的最优子结构

  • 边界:思考当只有一阶台阶的时候,我们只有一中方式,就是迈一步, 当只有二阶台阶的时候,我们有两种方式,1+1 或者 2。 所以F(1) 和 F(2) 就是当前问题的边界。 如果一个问题没有边界,那将永远无法得到有限的结果

  • 状态转移公式:按照上面的推理过程我们可以总结出 F(n) = F(n-1)+F(n-2) 状态转移公式是动态规划的核心,决定了问题的每一个阶段和下一阶段的关系。


自顶向下的解法:

每日算法-动态规划


改变一下思维:
F(1) = 1; F(2) = 2; 
F(3) = F(1)+F(2); 
F(4) = F(2)+F(3); 
...... 
3 只依赖 1和2 ;
4 只依赖 2和3 ;
5 只依赖 3和4 ;
由此可见,每次我们只要记住上两个状态,就可以推导出第三个状态。 而不需要像备忘录那样保存全部的状态。 这才是动态规划的实现。

代码从 i=3 开始迭代,一直到 i=n 结束。每一次迭代,都会计算出多一级台阶的走法数量。迭代过程中只需保留两个临时变量a和b,分别代表了上一次和上上次迭代的结果。为了便于理解,引入了temp变量。temp代表了当前迭代的结果值。
以上是爬楼梯这个最简单题目的分析、迭代、动态规划解法。其实动态规划都是可以通过画表格的方式推导出来的,鉴于此题比较简单就没有画图,之后我会分析一下稍微复杂一点的题目,使用表格的方式来展示。

(PS.页面排版突然不能插入代码了,就只能插图片了┓( ´∀` )┏)


 


以上是关于每日算法-动态规划的主要内容,如果未能解决你的问题,请参考以下文章

⚡冲刺大厂每日算法&面试题⚡动态规划21天——第一天

冲刺大厂每日算法&面试题,动态规划21天——第七天

冲刺大厂每日算法&面试题,动态规划21天——第六天

冲刺大厂每日算法&面试题,动态规划21天——第九天

冲刺大厂每日算法&面试题,动态规划21天——第十天

冲刺大厂每日算法&面试题,动态规划21天——第五天