前言:

一、Why?为什么需要动态规划

二、What?什么是动态规划

三、How?如何利用动态规划做题

四、思路总结


前言:

对于很多学习算法的同学来说,往往是跟着老师和学校教材上课,然后去刷题练熟练度;或者从网上的文章中寻求套路。对于这两个选择,我觉得都挺好,但是如果从0开始去接触这些算法思想,我总是想问why?waht?how?,然后利用这个寻求到的套路过关斩将(主要是斩杀面试题),去刷LeetCode。希望文章会对刚刚接触的你或已入题海的你有帮助,我也会不断改进,毕竟我也是一只菜猫。文章借鉴到的资料都会列出出处,可以直接去查看其它相关文章(本次主要采用C++代码,后面的解题文章两种代码都会给出C++/JAVA)。


一、Why?为什么需要动态规划

如果我问为什么1+1=2是正确的,可能你不能很系统的回答我,或者直接怼我一句。但“为什么需要动态规划”这个问题倒是有答案的,首先我们从解题角度来看,给出最简单的例子:

题目要求斐波那契数列Fib(n)的值?

在学习递归的课堂上,这个例子总是会被拿出来鞭尸,我们总是知道Fib(n) = Fib(n-1) + Fib(n-2),于是你可以很快的写出如下的代码:

int Fib(int n)
{if(n==1||n==2) return 1;return Fib(n-1) + Fib(n-2);
}

如果你可以很快写出这个式子,那么递归已经进入门槛了,其中Fib(7)=13,我们可以画一个随意的递归树查看我们递归的过程:

你可能会很开心的提交这个程序,然后输入7之后得到13,非常的开心,可是当你输入40的时候,等待总是漫长的(要注意精度问题),等了十几秒钟之后,你可能会不耐烦的关掉程序窗口。这个时候,你仔细的看看递归树,就会发现算法的时间复杂度竟然是O(2^n)指数级别的,你就想到需要更快的算法来帮助你。

那么问题的关键在于自顶向下的过程中,总是会重复的计算很多子问题,例如我们的F(5)就重复进行运算,那么我们就要想办法不在重复的计算这些值。你可能会想到备忘录的方法,定义一个数组,去保存已经计算过的值,这样就可以减少重复计算子问题,从而我们只需要计算出n个值即可,计算的数量就从2^n降低到n,完全是降维打击。例如下面的代码:

int Fib(int n) {if(n<1) return 0;//初始化备忘录 vector<int>memo(n+1,0);return helper(memo,n);
}long helper(vector<int>&memo,int n) {if(n==1||n==2) return 1;if(memo[n] != 0) return memo[n];//有值的话直接使用memo[n] = helper(memo,n-1) + helper(memo,n-2);//存储新的值进入数组return memo[n];
}

这样当你再次输入40的时候,一下就出来了结果(不同的编程语言代码实现不一样,但是思想却是一样的;这里C++可能用vector,在java中可能就是hashmap,所以不必在意是哪一种编程语言实现的代码)。而再一次计算时间复杂度,我们发现,的确是O(n)级别

对于这样有重复子问题的题目,我们在这里使用递归,然后使用备忘录的方式进行优化的确没有问题。可是我们都知道,整个问题我们是自顶向下的方式进行的,可以返回递归树在看一眼题目。那如果是自顶向上呢,我们一步一个台阶的走,最后走到终点呢?动态规划正是自顶向上的方法,循环迭代就能得到答案,那么相比备忘录呢?我们可以直接看DP(动态规划)的代码:

int Fib(int n) {vector<int>dp(n+1,0);//dp数组dp[1] = dp[2] = 1;//初始状态for(int i=3;i<=n;i++) {//循环迭代自顶向上得到答案 dp[i] = dp[i-1] + dp[i-2];}return dp[n];
}

相比于上面备忘录的代码,最直观的感觉就是代码量更少了,而且我们可以看到,不需要递归,直接循环迭代就可以得到结果,使用dp table来实现:

整个代码的运算就是从f(1)开始不断迭代得到最后一个数字,其实我们仅仅用到了两个位子就能计算第三个,所以这个dp table还可以进行空间的优化(状态压缩:缩小dp table大小,只记录必要数据),像这样:

int Fib(int n) {if(n==1||n==2) return 1;//初始化状态 int prev = 0,curr = 1;//只需要两个位子就可以得到答案 for(int i=0;i<n-1;i++) {//不断向前移动就可以得到最后的答案//观察dp table就会发现除去初始化的值仅仅需要计算n-1次,所以这里迭代为n-1次 int sum = prev + curr;prev = curr;curr = sum;}return curr;
}

这样一看,时间复杂度和空间复杂度都及其优化了。所以看来动态规划解决这样的重复子问题还有一手,那么你知道为什么要用动态规划了么?

如果从上面来看,你可能会回答我:“因为对于重复子问题比递归更方便使用,因为自顶向上的循环迭代比较符合思维”。我觉得你说的都对,可是我还想告诉你,除了重复子问题,动态规划其实解决的是拥有最优子结构的最值问题。比如,最长子序列问题、石子游戏啊这些。那么动态规划长啥样呢?可能从上面的例子你可以隐约看出一些东西,但是让我们更加系统的去了解它。


二、What?什么是动态规划

首先引用维基百科的介绍:

动态规划在寻找很多重叠子问题的情况的最佳解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并存储,从简单的问题知道整个问题都被解决。因此,动态规划存储递归时的结果,因而不会在解决同样的问题时花费时间。

动态规划只能应用于有最佳子结构的问题。最佳子结构的意思是局部最佳解能决定全域最佳解(对于有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单来说,问题能够分解成子问题来解决。

提炼出“重叠子问题”,“最佳子结构”这个概念出来,结合上面的例子,有那么一点理解什么样子的题目可以使用动态规划来做,可以使用自顶向上的循环迭代方法。但是什么是动态规划呢?

这就需要我们理解“状态转移方程”的概念,或者我更愿意把它理解为高中数学中数列的通项公式(或者说函形似函数公式)。上面的斐波那契数列不正是这样吗?a(n)= a(n-1) + a(n-2),然后我们要求n=7,我们往往会先要求出初始状态的值(比如a(0),a(1)),然后手动的计算往上推,就得到a(7)的值。这其中n-1状态和n-2状态相加转移才得到n的状态,所以称之为状态转移方程

所以,动态规划这里就理解为通过规划子状态得到最终状态的算法,它的特点为拥有重叠的子问题和最优子结构(即子结构互相独立)。利用f(ab)=f(a)*f(b)来理解就是要想f(ab)的值最大,那么a的值和b的值就要最大,不存在a增加而b减小的情况,a、b互相独立不制约。悄悄往上看斐波那契数列只是有着重叠子问题,而不存在最优子结构,所以严格来说不算动态规划,但是它作为入门却是一个经久不衰的例子。

那接下来就让我们看符合以上两个点的题目,一起来看动态规划的具体套路(成为套路王---->很多套路都是备忘录、dp table,下面我们直接dp就好)。


三、How?如何利用动态规划做题

首先给出我们的套路如下:

所以,看题目之后确定可不可以用动态规划(是否有重叠子问题,是否有最优子结构),如果是,那么其实写出状态转移方程就已经完成了大半的题目了,所以我们先要写出状态转移方程:

1、确定 base case,这个很简单,从题目中就能够得到。

2、确定「状态」,也就是原问题和子问题中会变化的变量

3、确定「选择」,也就是导致「状态」产生变化的行为。

4、明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。

但这样给出一个套路就真的可以做题目了吗?其实这其中还是有很多细节,所以我们通过一个简单的例子来逐个分析,看看此套路是否可行:

《最长上升子序列》

首先判断是不是可以使用动态规:“最优子结构,重叠子问题,后一状态取决于多个状态(具有选择)”。那么我们这一道题目要求的是最长上升子序列的长度,当前子序列长度是取决于之前的多个子序列的,子序列的选择不同,后面能够达到的长度也不一样(这也就是为什么我们要了解why,what,这样我们就很好判断是不是动态规划了)。

所以当前状态取决于之前的多个状态(和选择有关系,并且互相独立),所以这里我们使用动态规划

我们要写出状态转移方程,所以开始分析:

仅仅读了题目之后定义dp[n]的含义,可能会有很多种(如果做得题目不是很多,可能没有办法一来就定义dp[n],所以我们一点点分析)(以下我将递增子序列简称子序列)。


1、题目的【初始情况】也就是子序列长度:要么nmus.size()==0即长度为0,要么nums.size()==1即长度为1。

2、确定【状态】,也就是原问题和子问题中变化量。这里我们可以看到变化的是长度。

3、确定【选择】,选择不同的子序列,则后面子序列的长度会发生变化

4、定义dp[i],所以我们可以定义dp[i]为某一个子序列的长度,为了区别不同的子序列,我们可以重新定义dp[i]为以num[j]结尾的子序列的长度。那么dp[i]的长度不正等于nums[j]子序列长度或者nums[j]+1的长度吗?(j<i),也就是说当前状态就是上一个状态转移或者上一个状态+1转移得到的,也就是说:dp[i] = max(dp[i],dp[j]+1) (j<i)


我们详细的写出状态转移方程也就是:

方程:dp[i] = max( dp[i], dp[j]+1)  (for j in [0,i)

每轮计算新的dp[i]时遍历[0,i):

*如果num[i]>nums[j],那么当前数字可以接在前一个子序列之后,长度加一即是dp[i]=dp[j]+1;

*如果num[i]<=nums[j],跳过;

返回值也就是max(dp[i]),也就是我们题目所要求的长度。

那么在我们定义了dp[i]之后在看看初始化条件也就是除去特殊情况,dp[i]都为1,因为每一个数字都可以作为以自己结尾的长度为1的子序列。

所以我们的代码就有:

class Solution {
public:int lengthOfLIS(vector<int>& nums) {int n = (int)nums.size();//特殊情况if(n==0) return 0;int ans = 0;vector<int>dp(n,1);//全部初始化为1for(int i=0;i<n;i++) {for(int j=0;j<i;j++) {if(nums[i]>nums[j]) {dp[i] = max(dp[i],dp[j]+1);}  }ans = max(dp[i],ans);}return ans;}
};

当然,仅仅一两个简单的题目不可能展示很多情况,所以我们现在的目的就是记住这个套路,然后不断的刷题。当然,我也会规划其它动态规划的套路(上面的题目还可以用贪心、二分查找,可以参考其他博客)。


四、思路总结

所以我们总结一下思路:

  •  判断可不可以用动态规划去做(了解题目之后,从动态规划的特点入手:最优子结构、重叠子问题、状态转移),上面最长上升子序列我们就是这样判断的。
  • 想办法写出状态转移方程
  1. 确定 base case,这个很简单,从题目中就能够得到。

  2. 确定「状态」,也就是原问题和子问题中会变化的变量。

  3. 确定「选择」,也就是导致「状态」产生变化的行为。

  4. 明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。

  • 定义了dp[i]之后,写出状态转移方程(讨论各种情况)

  • 编写代码(循环迭代,注意初始化)

  • 尝试进行优化(也可以借鉴别人的优化,例如状态压缩,降低空间复杂度)

后面还会继续更新套路的哦~


五、参考资料

1、最长上升子序列(动态规划+二分查找,清晰图解)

2、动态规划套路详解

五大算法之动态规划套路详解(1)相关推荐

  1. 试设计递归算法dfs traverse_BFS 算法框架套路详解

    作者:labuladong 公众号:labuladong 后台有很多人问起 BFS 和 DFS 的框架,今天就来说说吧. 首先,你要说 labuladong 没写过 BFS 框架,这话没错,今天写个框 ...

  2. (转)dp动态规划分类详解

    dp动态规划分类详解 转自:http://blog.csdn.NET/cc_again/article/details/25866971 动态规划一直是ACM竞赛中的重点,同时又是难点,因为该算法时间 ...

  3. 操作系统:基于页面置换算法的缓存原理详解(下)

    概述: 在上一篇<操作系统:基于页面置换算法的缓存原理详解(上)>中,我们主要阐述了FIFO.LRU和Clock页面置换算法.接着上一篇说到的,本文也有三个核心算法要讲解.分别是LFU(L ...

  4. 希尔排序基础java代码_java 算法之希尔排序详解及实现代码

    摘要:这篇Java开发技术栏目下的"java 算法之希尔排序详解及实现代码",介绍的技术点是"希尔排序详解.实现代码.希尔排序.Java.实现.代码",希望对大 ...

  5. kmeans python interation flag_机器学习经典算法-logistic回归代码详解

    一.算法简要 我们希望有这么一种函数:接受输入然后预测出类别,这样用于分类.这里,用到了数学中的sigmoid函数,sigmoid函数的具体表达式和函数图象如下: 可以较为清楚的看到,当输入的x小于0 ...

  6. python实验原理_Python实现蒙特卡洛算法小实验过程详解

    蒙特卡洛算法思想 蒙特卡洛(Monte Carlo)法是一类随机算法的统称,提出者是大名鼎鼎的数学家冯·诺伊曼,他在20世纪40年代中期用驰名世界的赌城-摩纳哥的蒙特卡洛来命名这种方法. 通俗的解释一 ...

  7. Matlab中的FCM算法代码及中文详解

    Matlab中的FCM算法代码及中文详解 转自:http://xiaozu.renren.com/xiaozu/106512/336681453 function [center, U, obj_fc ...

  8. 图解机器学习算法(6) | 决策树模型详解(机器学习通关指南·完结)

    作者:韩信子@ShowMeAI 教程地址:https://www.showmeai.tech/tutorials/34 本文地址:https://www.showmeai.tech/article-d ...

  9. KMP算法之next数组详解

    KMP算法之next数组详解 KMP算法实现原理 KMP算法是一种非常高效的字符串匹配算法,下面我们来讲解一下KMP算如何高效的实现字符串匹配.我们假设如下主串和模式串: int i;//i表示主串的 ...

最新文章

  1. 借助URLOS快速安装python3网站环境
  2. mysql 当前用户连接数_实战:判断mysql中当前用户的连接数-分组淘选
  3. ec6110刷linux系统,华为EC6110-T优盘强刷固件升级包及刷机教程
  4. 如何在ftp服务器下查找文件夹,查找ftp服务器下的文件夹名
  5. 《线程管理:传递参数、确定线程数量、线程标识》
  6. 这个充电宝太黑科技了,又小又不用自己带线,长见识了~
  7. 帝国cms php替换,帝国CMS内容关键字替换图片标签解决方法
  8. Direct2D 如何关闭抗锯齿
  9. ndarray python 映射_191123 使用 Pybind11 和 OpenCV 创建 Python 库
  10. NUC1214 回文素数
  11. 12个免费的 Twitter Bootstrap 后台模板
  12. centos7 mariadb mysql_【软件安装】CentOS7安装MariaDb(mysql_替代品安装)
  13. Good Bye 2018 (A~F, H)
  14. Markdown编辑器简单总结
  15. (二)UDPSocket 客户端编写(超详细)
  16. smartprinter注册版_SmartPrinter下载 4.2 共享版
  17. 等额本息计算公式推导
  18. 北斗/GNSS在ROS机器人系统的应用
  19. 施一公等团队登Science封面:AI与冷冻电镜揭示「原子级」NPC结构,生命科学突破...
  20. 2020年10月计算机语言排名,最新!2020年10月编程语言排行榜出炉

热门文章

  1. 使用JDK自带的 keytool 工具生成公私钥证书库
  2. 这些故事你尽管听,不奇葩算我输!
  3. 基于 Golang 的 K8s 二次开发细节汇总
  4. A2DP和AVRCP 播放音视频
  5. C++实现邮件群发的方法
  6. 七段S型加减速算法--多轴时间同步方案调研
  7. 在中国程序员还是青春饭吗?35岁危机?头发见光,工资不涨?亲身经历!(内容太过真实)
  8. 2010年将流行的校园雷人语录
  9. win7设置计算机临时用户,为什么Win7用域账号登录以后总显示为临时配置账户? 穿墙书店...
  10. java中poi导出Excel表格(前台流文件接收)