什么是 “动态规划” , 用两个经典问题举例

1.什么是动态规划?

看了很多题解,一般解决者开始就说用DP来解,然后写了嵌套的for循环,不是很容易看懂,但是确实解出来了,我们这次来看下到底什么是动态规划?它有什么特点呢?容我抄一段话:

动态规划(Dynamic programming,DP),通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

我又对动态规划有了新的认识,动态规划的核心是把问题分解成相对简单的子问题,一般有递归的思想,但一般最终用for循环来解,这样效率更高,也就是说递归的也是动态规划,只是效率不高

2.经典问题1:斐波那契数列(Fibonacci polynomial)

我们直接用例子来举例:

什么是斐波那契数列呢?

就是类似这样的数列:1,1,2,3,5,8,13,21 ...

我们这里从0开始,就是除了第0个数和第1个数为1以外,后面的数等于前面两个数之和。

2.1 用普通的递归来解斐波那契数列

[cpp] view plaincopy

  1. int fib(int n){
  2. if(n == 0 || n == 1){
  3. return 1;
  4. }
  5. return fib(n - 1) + fib(n - 2);
  6. }

先不考虑负数,上面的写法是非常经典的递归。当n == 5的时候:fib(5)的计算过程如下:

  1. fib(5)
  2. fib(4) + fib(3)
  3. (fib(3) + fib(2)) + (fib(2) + fib(1))
  4. ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
  5. (((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

效率是非常低的,规模会成指数上升。这个解法有什么问题呢?就是没有保存一些已经计算过的值,老是重复计算。

2.2 改用动态规划来解斐波那契数列

我们知道上面的问题在于没有保存一些已经计算的值,修改如下,改用两个变量已经计算过的,感谢@水晶男人的建议:

[cpp] view plaincopy

  1. int fib(int n)
  2. {
  3. int prev=1,next=1,tmp=2;
  4. for(int i = 2; i <= n; i++){
  5. tmp = prev + next;
  6. prev = next;
  7. next = tmp;
  8. }
  9. return tmp;
  10. }

3.经典问题2 Triangle (Leetcode)

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

For example, given the following triangle

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

The minimum path sum from top to bottom is 11 (i.e., 2 + 3 +5 +1 = 11).

这道题目就比上面的斐波那契数难些了。当弄下一个数时,还只能找相邻的。比如当前一步是5,那下一步只能是1或者8。也就是说下一步的index只可能等于当前index,或者index+1。

3.1用递归来解 Triangle

我们的思路是从头部开始,注意到递归主要要处理好参数和终止条件。我们这里的终止条件是到达三角形的最后一层。要不断变化的就是层数和index,因为我们要比较两个数。程序变量写的比较啰嗦,主要为了写的明白些。

[cpp] view plaincopy

  1. int minPath(vector<vector<int> > triangle, int index, int level) {
  2. if( level== triangle.size()-1) {
  3. return triangle[level][index];
  4. }else{
  5. int current = triangle[level][index];
  6. int nextFirst = minPath(triangle, index, level+1);
  7. int nextSecond = minPath(triangle, index+1, level+1);
  8. int nextFirstTotal = nextFirst + current;
  9. int nextSecondTotal = nextSecond + current;
  10. return nextFirstTotal < nextSecondTotal ? nextFirstTotal : nextSecondTotal;
  11. }
  12. }
  13. int minimumTotal(vector<vector<int> > &triangle) {
  14. if(triangle.size()==0) return 0;
  15. return minPath(triangle, 0, 0);
  16. }

3.2用DP来解Triangle

我们注意到递归非常慢,为了求第1层的最短距离,要先求第2层。为了求第2层,要先求第3层。铺了很多坑,最后全填上才能到结果。这个题目也很有意思,From top to bottom. 我们可以换个思路,从底部往上求。

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

我们可以用一个数组记录已经求出的最短距离,刚开始初始化为最后一行的数,就是4,1,8,3,然后求最后一行到倒数第2行的最短距离。因为 1 + 6 < 4 + 6, 1 + 5 < 8 + 5, 3 + 7 < 8 + 7。所以我们更新这个数组为7,6,10,3。最后一个3其实没用了。我们以此类推:

最后一行到倒数第3行的最短距离更新为: 9,10,10,3。

最后一行到倒数第4行的最短距离更新为:11,10,10,3。

第一个数11就是我们要的结果。

[cpp] view plaincopy

  1. int minimumTotal(vector<vector<int> > &triangle) {
  2. int triangleSize = triangle.size();
  3. if(triangleSize == 0){
  4. return 0;
  5. }
  6. //初始化一个数组来记录最后一行到当前行的最短距离
  7. vector<int> saveMinDistance(triangle[triangleSize - 1].size(), 0);
  8. //刚开始的值为最后一行的值
  9. for(int i = 0; i < triangle[triangleSize - 1].size(); ++i){
  10. saveMinDistance[i] = triangle[triangleSize - 1][i];
  11. }
  12. int first,second,current;
  13. first = second = current = 0;
  14. //从倒数第2行开始求到第1行
  15. for(int i = triangleSize - 2; i >=0; i--){
  16. //当第N行,需要求N+1个最短距离,并且保存他们
  17. for(int j = 0; j < i + 1; j++){
  18. current = triangle[i][j];
  19. first = current + saveMinDistance[j];
  20. second = current + saveMinDistance[j + 1];
  21. //保存最短距离
  22. saveMinDistance[j] = first < second ? first : second;
  23. }
  24. }
  25. return saveMinDistance[0];
  26. }

稍微优化点,去除了初始化最后一行,也放到for循环中:

[cpp] view plaincopy

  1. int minimumTotal(vector<vector<int> > &triangle) {
  2. int triangleSize = triangle.size();
  3. if(triangleSize == 0){
  4. return 0;
  5. }
  6. //初始化一个数组来记录最后一行到当前行的最短距离
  7. vector<int> saveMinDistance(triangle[triangleSize - 1].size() + 1, 0);
  8. int first,second,current;
  9. first = second = current = 0;
  10. //从倒数第1行开始求到第1行
  11. for(int i = triangleSize - 1; i >=0; i--){
  12. //当第N行,需要求N+1个最短距离,并且保存他们
  13. for(int j = 0; j < i + 1; j++){
  14. current = triangle[i][j];
  15. first = current + saveMinDistance[j];
  16. second = current + saveMinDistance[j + 1];
  17. //保存最短距离
  18. saveMinDistance[j] = first < second ? first : second;
  19. }
  20. }
  21. return saveMinDistance[0];
  22. }

总的来说动态规划还是比较难的技巧。因为首先当前题目要可以用动态规划的思想来做,其次要完整的把整个题目都想清楚,保存什么值,如何for循环,都是要考虑的因素。

我们这里用两道题目来窥探下动态规划,动态规划还是比较难的技巧,后期再见

什么是 “动态规划” , 用两个经典问题举例

时间: 2024-10-19 07:18:37

什么是 “动态规划” , 用两个经典问题举例的相关文章

第七篇:两个经典的文件IO程序示例

前言 本文分析两个经典的C++文件IO程序,提炼出其中文件IO的基本套路,留待日后查阅. 程序功能 程序一打印用户指定的所有文本文件,程序二向用户指定的所有文本文件中写入数据. 程序一代码及其注释 1 #include <iostream> 2 #include <fstream> // 使用文件处理对象记着要包含这个头文件 3 #include <string> 4 #include <vector> 5 6 using namespace std; 7

赠书:HTML5 Canvas 2d 编程必读的两本经典

赠书:HTML5 Canvas 2d 编程必读的两本经典 这两年多一直在和HTML5 Canvas 打交道,也带领团队开发了世界首款基于HTML5 Canvas 的演示文档工具---AxeSlide(斧子演示,www.axeslide.com).在这个领域也积累了一些 经验,希望有机会和大家分享.今天是要给大家推荐两本这方面的书,同时会送一本书给大家. 要介绍的第一本书是我学习Canvas开发的入门书——<HTML5 Canvas核心技术:图形.动画与游戏开发>. 此书作者David Gear

缺陷跟踪的两个经典分析模型

缺陷跟踪过程是软件工程中的一个极其重要的过程.本文介绍了如何使用两个经典的分析模型,来控制缺陷跟踪的过程.这两个模型叫做<活动bug走势图>.<bug打开关闭图>. 另外,在文章中还会提到两个概念:“bug收敛”.“零bug反弹”,具体含义会在介绍中说明. 先看张图片,这就是两个模型的分析图片,集成在一个坐标里面了.活动bug走势是一条线,bug打开关闭是柱图,X轴是时间.下面我们详细说说这两个模型的含义. 先要说几个名词解释: 1.活动bug数.状态不是closed的所有bug的

信号量基础和两个经典例子

信号量基础和两个经典例子 信号量(semaphore) 用于进程中传递信号的一个整数值. 三个操作: 1.一个信号量可以初始化为非负值 2.semWait操作可以使信号量减1,若信号量的值为负,则执行semWait的进程被阻塞.否则进程继续执行. 3.semSignal操作使信号量加1.若信号量的值小于等于0,则被semWait操作阻塞的进程讲被接触阻塞. ps: semWait对应P原语,semSignal对应V原语. 信号量以及PV原语的C语言定义如下 struct semaphore {

Android中退出多个Activity的两个经典方法

这里介绍两种方法:一种把每个activity记住,然后逐一干掉:另一种思路是使用广播. 方法一.用list保存activity实例,然后逐一干掉 上代码: import java.util.LinkedList; import java.util.List; import android.app.Activity; import android.app.AlertDialog; import android.app.Application; import android.content.Dial

动态规划 与两道例题

现在要把这几种常见的算法给理清弄明白了,要不然只能做个低级程序员了. 动态规划DP是求解决策过程的最优化的数学方式.动态规划一般分为线性动规,区域动规,树形动规,背包动规. 动态规划是一种方法,但不是一种算法,一般用于多决策中的最优化问题,具有递推的思想.动态规划与分治法类似,基本思想都是把待解问题分解成若干个子问题,先求解子问题,然后由这些子问题的解得到原问题的解.但分治法中分解得到的子问题是相互独立的,但动态规划中不是.动态规划的基本思路与分治法相似,也是用一个表记录所有已解子问题的答案,不

java interface的两个经典用法

1.Java多态接口动态加载实例 编写一个通用程序,用来计算没一种交通工具运行1000公里所需的时间,已知每种交通工具的参数都为3个整数A.B.C的表达式.现有两种工具:Car和Plane,其中Car的速度运算公式为:A+B+C.需要编写三个类:ComputeTime.java,Palne.java,Car.java和接口Common.java.要求在未来如果增加第3中交通工具的时候,不必修改 以前的任何程序,只需要写新的交通工具的程序.其运行过程如下: 从命令行输入ComputeTime的四个

Linux中的两个经典宏定义:获取结构体成员地址,根据成员地址获得结构体地址;Linux中双向链表的经典实现。

倘若你查看过Linux Kernel的源码,那么你对 offsetof 和 container_of 这两个宏应该不陌生.这两个宏最初是极客写出的,后来在Linux内核中被推广使用. 1. offsetof 1.1 offsetof介绍 定义:offsetof在linux内核的include/linux/stddef.h中定义.#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 说明:获得结构体(TYPE)的变量成员(

第十九篇:处理僵尸进程的两种经典方法

前言 如果父进程没有结束,而子进程终止了.那么在父进程调用 wait 函数回收这个子进程或者父进程终止以前,这个子进程将一直是僵尸进程. 本文将提供两种方法处理这个问题. 方法一:父进程回收法 wait函数将使其调用者阻塞,直到其某个子进程终止.故父进程可调用wait函数回收其僵尸子进程.除此之外,waitpid函数提供更为详尽的功能( 增加了非阻塞功能以及指定等待功能 ),请读者自行查阅相关资料. 代码实现 1 #include <unistd.h> 2 #include <sys/w