来自公众号:五分钟学算法

导言

动态规划问题一直是算法面试当中的重点和难点,并且动态规划这种通过空间换取时间的算法思想在实际的工作中也会被频繁用到,这篇文章的目的主要是解释清楚 什么是动态规划,还有就是面对一道动态规划问题,一般的 思考步骤 以及其中的注意事项等等,最后通过几道题目将理论和实践结合。

什么是动态规划

如果你还没有听说过动态规划,或者仅仅只有耳闻,或许你可以看看 Quora 上面的这个 回答

How to explain dynamic

用一句话解释动态规划就是 “记住你之前做过的事”,如果更准确些,其实是 “记住你之前得到的答案”。

我举个大家工作中经常遇到的例子。

在软件开发中,大家经常会遇到一些系统配置的问题,配置不对,系统就会报错,这个时候一般都会去 Google 或者是查阅相关的文档,花了一定的时间将配置修改好。

过了一段时间,去到另一个系统,遇到类似的问题,这个时候已经记不清之前修改过的配置文件长什么样,这个时候有两种方案,一种方案还是去 Google 或者查阅文档,另一种方案是借鉴之前修改过的配置,第一种做法其实是万金油,因为你遇到的任何问题其实都可以去 Google,去查阅相关文件找答案,但是这会花费一定的时间,相比之下,第二种方案肯定会更加地节约时间,但是这个方案是有条件的,条件如下:

  • 之前的问题和当前的问题有着关联性,换句话说,之前问题得到的答案可以帮助解决当前问题

  • 需要记录之前问题的答案

当然在这个例子中,可以看到的是,上面这两个条件均满足,大可去到之前配置过的文件中,将配置拷贝过来,然后做些细微的调整即可解决当前问题,节约了大量的时间。

不知道你是否从这些描述中发现,对于一个动态规划问题,我们只需要从两个方面考虑,那就是 找出问题之间的联系,以及 记录答案,这里的难点其实是找出问题之间的联系,记录答案只是顺带的事情,利用一些简单的数据结构就可以做到。

思考动态规划问题的四个步骤

一般解决动态规划问题,分为四个步骤,分别是

  • 问题拆解,找到问题之间的具体联系

  • 状态定义

  • 递推方程推导

  • 实现

这里面的重点其实是前两个,如果前两个步骤顺利完成,后面的递推方程推导和代码实现会变得非常简单。

这里还是拿 Quora 上面的例子来讲解,“1+1+1+1+1+1+1+1” 得出答案是 8,那么如何快速计算 “1+ 1+1+1+1+1+1+1+1”,我们首先可以对这个大的问题进行拆解,这里我说的大问题是 9 个 1 相加,这个问题可以拆解成 1 + “8 个 1 相加的答案”,8 个 1 相加继续拆,可以拆解成 1 + “7 个 1 相加的答案”,… 1 + “0 个 1 相加的答案”,到这里,第一个步骤 已经完成。

状态定义 其实是需要思考在解决一个问题的时候我们做了什么事情,然后得出了什么样的答案,对于这个问题,当前问题的答案就是当前的状态,基于上面的问题拆解,你可以发现两个相邻的问题的联系其实是 后一个问题的答案 = 前一个问题的答案 + 1,这里,状态的每次变化就是 +1。

定义好了状态,递推方程就变得非常简单,就是 dp[i] = dp[i - 1] + 1,这里的 dp[i] 记录的是当前问题的答案,也就是当前的状态,dp[i - 1] 记录的是之前相邻的问题的答案,也就是之前的状态,它们之间通过 +1 来实现状态的变更。

最后一步就是实现了,有了状态表示和递推方程,实现这一步上需要重点考虑的其实是初始化,就是用什么样的数据结构,根据问题的要求需要做那些初始值的设定。

public int dpExample(int n) {    int[] dp = new int[n + 1];  // 多开一位用来存放 0 个 1 相加的结果

    dp[0] = 0;      // 0 个 1 相加等于 0

    for (int i = 1; i <= n; ++i) {        dp[i] = dp[i - 1] + 1;    }

    return dp[n];}

你可以看到,动态规划这四个步骤其实是相互递进的,状态的定义离不开问题的拆解,递推方程的推导离不开状态的定义,最后的实现代码的核心其实就是递推方程,这中间如果有一个步骤卡壳了则会导致问题无法解决,当问题的复杂程度增加的时候,这里面的思维复杂程度会上升。

接下来我们再来看看 LeetCode 上面的几道题目,通过题目再来走一下这些个分析步骤。

题目实战

爬楼梯

但凡涉及到动态规划的题目都离不开一道例题:爬楼梯(LeetCode 第 70 号问题)。

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入:2输出:2解释: 有两种方法可以爬到楼顶。

1. 1 阶 + 1 阶2. 2 阶

示例 2:

输入:3输出:3解释: 有三种方法可以爬到楼顶。

1. 1 阶 + 1 阶 + 1 阶2. 1 阶 + 2 阶3. 2 阶 + 1 阶

题目解析

爬楼梯,可以爬一步也可以爬两步,问有多少种不同的方式到达终点,我们按照上面提到的四个步骤进行分析:

  • 问题拆解

    我们到达第 n 个楼梯可以从第 n - 1 个楼梯和第 n - 2 个楼梯到达,因此第 n 个问题可以拆解成第 n - 1 个问题和第 n - 2 个问题,第 n - 1 个问题和第 n - 2 个问题又可以继续往下拆,直到第 0 个问题,也就是第 0 个楼梯 (起点)

  • 状态定义

    “问题拆解” 中已经提到了,第 n 个楼梯会和第 n - 1 和第 n - 2 个楼梯有关联,那么具体的联系是什么呢?你可以这样思考,第 n - 1 个问题里面的答案其实是从起点到达第 n - 1 个楼梯的路径总数,n - 2 同理,从第 n - 1 个楼梯可以到达第 n 个楼梯,从第 n - 2 也可以,并且路径没有重复,因此我们可以把第 i 个状态定义为 “从起点到达第 i 个楼梯的路径总数”,状态之间的联系其实是相加的关系。

  • 递推方程

    “状态定义” 中我们已经定义好了状态,也知道第 i 个状态可以由第 i - 1 个状态和第 i - 2 个状态通过相加得到,因此递推方程就出来了 dp[i] = dp[i - 1] + dp[i - 2]

  • 实现

    你其实可以从递推方程看到,我们需要有一个初始值来方便我们计算,起始位置不需要移动 dp[0] = 0,第 1 层楼梯只能从起始位置到达,因此 dp[1] = 1,第 2 层楼梯可以从起始位置和第 1 层楼梯到达,因此 dp[2] = 2,有了这些初始值,后面就可以通过这几个初始值进行递推得到。

参考代码

public int climbStairs(int n) {    if (n == 1) {        return 1;    }

    int[] dp = new int[n + 1];  // 多开一位,考虑起始位置

    dp[0] = 0; dp[1] = 1; dp[2] = 2;    for (int i = 3; i <= n; ++i) {        dp[i] = dp[i - 1] + dp[i - 2];    }

    return dp[n];}

三角形最小路径和

LeetCode 第 120 号问题:三角形最小路径和。

题目描述

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

[     [2],    [3,4],   [6,5,7],  [4,1,8,3]]

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

题目解析

给定一个三角形数组,需要求出从上到下的最小路径和,也和之前一样,按照四个步骤来分析:

  • 问题拆解:

    这里的总问题是求出最小的路径和,路径是这里的分析重点,路径是由一个个元素组成的,和之前爬楼梯那道题目类似,[i][j] 位置的元素,经过这个元素的路径肯定也会经过 [i - 1][j] 或者 [i - 1][j - 1],因此经过一个元素的路径和可以通过这个元素上面的一个或者两个元素的路径和得到。

  • 状态定义

    状态的定义一般会和问题需要求解的答案联系在一起,这里其实有两种方式,一种是考虑路径从上到下,另外一种是考虑路径从下到上,因为元素的值是不变的,所以路径的方向不同也不会影响最后求得的路径和,如果是从上到下,你会发现,在考虑下面元素的时候,起始元素的路径只会从[i - 1][j] 获得,每行当中的最后一个元素的路径只会从 [i - 1][j - 1] 获得,中间二者都可,这样不太好实现,因此这里考虑从下到上的方式,状态的定义就变成了 “最后一行元素到当前元素的最小路径和”,对于 [0][0] 这个元素来说,最后状态表示的就是我们的最终答案。

  • 递推方程

    “状态定义” 中我们已经定义好了状态,递推方程就出来了

    dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]
  • 实现

    这里初始化时,我们需要将最后一行的元素填入状态数组中,然后就是按照前面分析的策略,从下到上计算即可

参考代码

public int minimumTotal(List<List<Integer>> triangle) {    int n = triangle.size();

    int[][] dp = new int[n][n];

    List<Integer> lastRow = triangle.get(n - 1);

    for (int i = 0; i < n; ++i) {        dp[n - 1][i] = lastRow.get(i);    }

    for (int i = n - 2; i >= 0; --i) {        List<Integer> row = triangle.get(i);        for (int j = 0; j < i + 1; ++j) {            dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + row.get(j);        }    }

    return dp[0][0];}

最大子序和

LeetCode 第 53 号问题:最大子序和。

题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],输出: 6解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶:

如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

题目解析

求最大子数组和,非常经典的一道题目,这道题目有很多种不同的做法,而且很多算法思想都可以在这道题目上面体现出来,比如动态规划、贪心、分治,还有一些技巧性的东西,比如前缀和数组,这里还是使用动态规划的思想来解题,套路还是之前的四步骤:

  • 问题拆解:

    问题的核心是子数组,子数组可以看作是一段区间,因此可以由起始点和终止点确定一个子数组,两个点中,我们先确定一个点,然后去找另一个点,比如说,如果我们确定一个子数组的截止元素在 i 这个位置,这个时候我们需要思考的问题是 “以 i 结尾的所有子数组中,和最大的是多少?”,然后我们去试着拆解,这里其实只有两种情况:

    i 这个位置的元素自成一个子数组;

    i 位置的元素的值 + 以 i - 1 结尾的所有子数组中的子数组和最大的值

    你可以看到,我们把第 i 个问题拆成了第 i - 1 个问题,之间的联系也变得清晰

  • 状态定义

    通过上面的分析,其实状态已经有了,dp[i] 就是 “以 i 结尾的所有子数组的最大值

  • 递推方程

    拆解问题的时候也提到了,有两种情况,即当前元素自成一个子数组,另外可以考虑前一个状态的答案,于是就有了

    dp[i] = Math.max(dp[i - 1] + array[i], array[i])

    化简一下就成了:

    dp[i] = Math.max(dp[i - 1], 0) + array[i]
  • 实现

    题目要求子数组不能为空,因此一开始需要初始化,也就是 dp[0] = array[0],保证最后答案的可靠性,另外我们需要用一个变量记录最后的答案,因为子数组有可能以数组中任意一个元素结尾

参考代码

public int maxSubArray(int[] nums) {    if (nums == null || nums.length == 0) {        return 0;    }

    int n = nums.length;

    int[] dp = new int[n];

    dp[0] = nums[0];

    int result = dp[0];

    for (int i = 1; i < n; ++i) {        dp[i] = Math.max(dp[i - 1], 0) + nums[i];        result = Math.max(result, dp[i]);    }

    return result;}

总结

通过这几个简单的例子,相信你不难发现,解动态规划题目其实就是拆解问题,定义状态的过程,严格说来,动态规划并不是一个具体的算法,而是凌驾于算法之上的一种 思想

这种思想强调的是从局部最优解通过一定的策略推得全局最优解,从子问题的答案一步步推出整个问题的答案,并且利用空间换取时间。从很多算法之中你都可以看到动态规划的影子,所以,还是那句话 技术都是相通的,找到背后的本质思想是关键

- END -

如果看到这里,说明你喜欢这篇文章,请转发、点赞。扫描下方二维码或者微信搜索「perfect_iscas」,添加好友后即可获得10套程序员全栈课程+1000套PPT和简历模板向我私聊「进群」二字即可进入高质量交流群。

扫描二维码进群↓

在看 

有了四步解题法模板,再也不害怕动态规划!相关推荐

  1. 波利亚名著《怎样解题》笔记:四步解题法

    文章目录 一.四步解题法 第一步.彻底理解问题 第二步.形成解决思路 第三步.执行 第四步.总结 二.解题过程 1.什么是问题? 2.问题的解法 3.弄清问题 4.拟定计划 5.实现计划 6.回顾解答 ...

  2. “完形填空”五步解题法

    中考英语试题中的"完形填空"目前难度并不大,但是在初中阶段养成良好的解题习惯不仅为英语学习奠定一定基础,也为升入高中学习打下一定的阅读基本功. 现将五步解题法教给大家.经常操练,可 ...

  3. 新媒体推广如何策划出一个引爆众人参与的推广活动?黎想首次公布四步“引爆法”,一步解决推广烦恼

    "小李,快过年了,咱们企业充分利用这些多新媒体渠道策划一个推广活动,要能够引爆市场,快速的提升销量!你觉得可以吗" "老板,您的预算是多少?要根据预算制定促销活动啊!&q ...

  4. 亲测有用!轻松get新技能的四步学习法

    全文共1965字,预计学习时长7分钟 图源:unsplash 置身于滚滚的庞大信息流当中,我们常常会不知所措.新框架和新技术接踵而至,每个人都在声称自己的比其他框架和技术更快.更安全.更高效.作为开发 ...

  5. 四步相移法怎么获得相位信息_不一样的费曼学习法!|高中篇|”

    01 大家有没有遇到这种情况,老师讲课时,带着大家一起解题的时候,你感觉自己学会了. 可是等到自己做题时,却总是感觉又不会了.只是模糊的记得一些解题步骤,但是中间的细节却又不太明了. 你的学霸同学教你 ...

  6. PPT-Record01—屌丝ppt的四步修改法

    目录 屌丝PPT的共同点 1. 还在用宋体.黑体.楷体.... 2. 标题不突出 3. 配色很杂乱 4. 没有配图或乱配图 敲写高品质PPT的四步法 Step1: 统一字体 Step2: 突出标题 S ...

  7. 四步相移法怎么获得相位信息_如何拥有超强的学习能力?世界公认的费曼学习法了解一下...

    我们都听说过"方法比努力更重要"这句话,在孩子的学习过程中,用错了方法会浪费大量的时间,一个好的方法能够事半功倍. 比如孩子记了笔记却不复习,和不记笔记是一样的,如果将知识理解后再 ...

  8. 四步相移法怎么获得相位信息_涨知识!5G的调制方式,到底是怎么实现的?

    本文来源:微信公众号中兴文档大家好,今天我们来聊聊调制.说到调制,我想很多同学马上会联想到这些关键词:BPSK.QPSK.调幅.调相.QAM.星座图--众所周知,调制和解调是通信基本业务流程中的重要组 ...

  9. 五步移相法matlab程序_四步移相法提取相位(相位解包裹)基础问题

    最近在自学四步移相法提取相位,有个初级疑问: 有四幅移相图片a.b.c.d如下:完整解包裹代码如下: 程序运行后,主要是figure(7)和figure(8)没看太明白,;从figure(5)和fig ...

最新文章

  1. 反思编写页面追加页面元素的方法,目的:加快开发速度 节省开发时间 需求:点击搜索清空表格内容进行增加新的数据行
  2. swiper叠加轮播效果 (含源码) - 案例篇
  3. gitHub上传项目
  4. .Net FSO简单小结(简单到不能再简单了)
  5. 浏览器是如何工作的:Chrome V8让你更懂JavaScript
  6. 研究生英语单词学习——Learning English
  7. java u盘检测工具,BadUSB测试记录
  8. 【CODETOOL】文件比较Beyond Compare使用介绍
  9. 计算机桌面空白图标如何删除,桌面出现空白图标的删除方法
  10. 通讯网关 api网关_伟大的api网关迁移
  11. AltiumDesigner之Logo制作
  12. 【利用Python进行数据分析——经验篇2】计算微博转发/评论/点赞h指数的Python代码
  13. navicat连接数据库出错,但是SQLyog可以正常连接
  14. [时间复杂度]为什么采用二叉排序树查找的平均查找长度为O(log2n)
  15. Django REST framework(十)路由集routers的使用
  16. c语言背景音乐,背景图,背景字体
  17. HTML span 标签 的详细用法
  18. 杨辉三角中第 n 行第 m 个数字
  19. vue-router基本概念总结
  20. 蓝桥杯JAVA答题技巧,第九届蓝桥杯大赛个人赛省赛(软件类)C/C++ 大学B组比赛心得(还在更新)...

热门文章

  1. HOGDescriptor 描述类
  2. IEEE迎来首位华人主席,马里兰大学终身教授刘国瑞当选
  3. 讯飞智能语音先锋者:等到人机交互与人类交流一样自然时,真正的智能时代就来了...
  4. 从LeNet到GoogLeNet:逐层详解,看卷积神经网络的进化
  5. 这就是芬兰:先让全国1%的人学起AI!
  6. 1024程序员节,你是我们要找的那条锦鲤吗?
  7. 《极限挑战》罗志祥遭套路“破产”,我们却看到了更大的危机
  8. 面试官问:select......for update会锁表还是锁行?
  9. Linux内存、Swap、Cache、Buffer详细解析
  10. Spring Validation最佳实践及其实现原理,参数校验没那么简单!