动态规划算法

我们先来看一个题目:有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。

动态规划算法_第1张图片

很显然可以使用暴力破解求出所有的排列组合,但是时间复杂度是指数级的。

这里很显然使用动态规划是最合适的!那到底什么是动态规划呢?

动态规划的英文名是Dynamic Programming,是一种分阶段求解决策问题的数学思想。它不仅用于编程领域,也应用于管理学、经济学、生物学。总结起来就是一句话:大事化小,小事化了

用刚才的题目来详细说明:

假设你只差最后一步就走到第10级台阶,这时候会出现几种情况?很显然是2种,题目要求一步只许走一级或2级台阶,一种情况是从第9级走到第10级,另一种是从第8级走到第10级。所以,暂且不管0到8级台阶是怎么走的,也不管0到9级台阶是怎么走的,想要走到第10级台阶,最后一步必然是从第8级或第9级开始。

接下来引申出一个新的问题,如果我们已知0到9级台阶的走法有X种,0到8级台阶的走法有Y种,那么0到10级台阶的走法就是X+Y种。(不明白的可以看括号解释:第一种情况从9级到10级的走法数量和0到9级的走法数量是相等的,所以是X;第二种情况从8级到10级的走法数量又和0到8级的走法数量是相等的,所以是Y)。思路如下图:

动态规划算法_第2张图片

推断到这就可以得出一个结论:从0到10级台阶的走法数量 = 0到9级台阶的走法数量 + 0到8级台阶的走法数量。我们将0到10级的走法数量记为F(10); 则 F(10) = F(9) + F(8)。那么,我们如何计算F(9)和F(8)呢?利用刚才的思路继续推断,F(9) = F(8) + F(7),F(8) = F(7) + F(6)。这样,把一个复杂的问题分阶段进行简化,逐步简化成简单的问题,这就是动态规划的思想。所以当只有1级台阶和2级台阶的时候有几种走法呢?显然分别是1和2,所以我们可以归纳出如下的公式:

F(1) = 1; 

F(2) = 2; 

F(n) = F(n-1) + F(n-2)  n>2

动态规划中包含三个重要的概念,最优子结构、边界、状态转移公式。刚才我们分析出F(10) = F(9) + F(8),所以F(9) 和 F(8)是F(10)的最优子结构,当只有1级台阶或2级台阶时,我们可以直接得出结果,无需进行简化。我们称F(1)和F(2)是问题的边界。如果一个问题没有边界,将永远无法得到有限的结果。F(n) = F(n-1) + F(n-2)是阶段与阶段间的状态转移方程,这是动态规划的核心,决定了问题的每一个阶段和下一个阶段的关系。

但是到目前为止,我们只完成了动态规划的前半部分:问题建模。下面才是真正麻烦的阶段:求解问题

其实已经归纳出了F(n) = F(n-1) + F(n-2),又知道了递归结束的条件,我们可以直接用递归的思路写程序。但是该方法手段时间复杂度指数级的。如下图

动态规划算法_第3张图片

这是一棵二叉树,树的节点个数就是我们的递归方程所需要计算的次数。不难看出,这棵二叉树的高度是N-1,节点个数接近2的N-1次方。所以方法的时间复杂度可以近似地看作是O(2^N)。其实回顾下刚才的递归图,有些相同的参数被重复计算了,越往下走,重复的越多。

动态规划算法_第4张图片

如图所示,相同的颜色代表了方法被传入相同的参数。当然我们也可以使用例如哈希表之类的数据结构存储这些结果,就可以避免了重复计算。但是今天我们要学的是动态规划,那么怎么用动态规划来解决呢?

刚才的思路是对F(N)自顶向下做递归运算,现在我们试着用自底向上迭代的方式来推导。

这里我们使用表格来说明求解的过程。

动态规划算法_第5张图片

表格的第一行代表了楼梯台阶的数目,第二行代表了若干级台阶对应的走法数。F(1) = 1;   F(2) = 2;是之前就已经明确过的结果。

动态规划算法_第6张图片

第一次迭代,台阶数等于3时,走法数量是3.。这个结果是F(1)、F(2)这两个结果相加得到的。所以F(3)只依赖于F(1)、F(2)。

动态规划算法_第7张图片

同理第二次迭代,F(4)只依赖于F(2)和F(3)。同理可以继续往下推.........

由此可见,每一次迭代过程中,只要保留之前的两个状态,就可以推导出新的状态。不需要保存下全部的子状态。

这就是动态归回的实现。下面给出代码:

动态规划算法_第8张图片

这个方法的时间复杂度显然是O(N),由于只引入了两三个变量,所以空间复杂度只有O(1)。

所以,利用动态规划来解决,可以实现时间和空间上的最优化。当然这里只是举个最简单的例子,来理解动态规划的含义。

我们来看下一个题目

有一个国家发现了5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是10人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?

动态规划算法_第9张图片

很显然,这道题使用排列组合也可以解决,但是时间复杂度依然是指数级的O(2^N)。

动态规划有三个核心元素:最优子结构、边界、状态转移方程式。本题的最优子结构是针对4个金矿(减掉一个),考虑两种情况,一种是第5个金矿选择不挖,那么此时人数依旧是10;另一种是选择挖,那剩余人数就是10-3=7人。

动态规划算法_第10张图片

找到了最优子结构,那么我们来分析一下最优子结构和最终问题的关系。换句话说,4个金矿的最优选择和5个金矿的最优选择直接,是什么样的关系?显然5个金矿的最优选择,就是(前4座金矿10工人的挖金数量)和(前4座金矿7工人的挖金数量+第5座金矿的挖金数量)的最大值。这里我们为了便于描述,把金矿数量设为N,工人数设为W,金矿的黄金量设为数组G[ ],金矿的用工量设为数组P[ ]。那么5座金矿和4座金矿的最优选择之间存在这样的关系,F(5,10)=MAX( F(4,10),F(4,10-p[4])+G[4] )。

现在我们确定下该问题的边界是什么?显然边界也有两种情况,用公式来表达:

当N=1,W>=P[0]时,F(N,W) = G[0];

当N=1,W

整理一下就能能到问题的状态转移方程式:

F(n,w) = 0 (n<=1, w

F(n,w) = g[0] (n==1, w>=p[0]);

F(n,w) = F(n-1,w) (n>1, w

F(n,w) = max(F(n-1,w), F(n-1,w-p[n-1])+g[n-1]) (n>1, w>=p[n-1])。

至此,动态规划建模工作已经完成,现在我们看一下怎么实现。

动态规划算法_第11张图片

首先考虑第一个金矿的情况,是400金,5个工人。所以前4个格子都是0,因为人数不够。后面的格子都是400,因为只有这一座金矿可挖。

动态规划算法_第12张图片

第2座金矿有500黄金,需要5工人。因为W<5,所以F(N,W) = F(N-1,W) = 0,所以前4个格子都是0。

考虑第二行后6个格子,因为F(N,W) =MAX( F(N-1,W), F(N-1,W-5)+500),所以第5-9个格子的值是500。需要注意的是第 2行的第10个格子,也就是N=2,W=10的时候,F(N-1,W)=400,F(N-1,W-5)=400,Max(400,400+500)=900。

第3座金矿有200黄金,需要三个工人,第三行的计算方法如出一辙。

动态规划算法_第13张图片

第4座金矿有300黄金,需要4工人,第5座金矿有350黄金,需要3工人。计算方法同上。

动态规划算法_第14张图片

从表中可以看出一些规律,除了第一行以外,每个格子都是前一行的一个或两个格子推导而来。比如3金矿8工人的结果,就来自2金矿5工人和2金矿8工人,MAX(500,500+200)=700.

动态规划算法_第15张图片

动态规划算法_第16张图片

我们在使用程序实现的时候,也可以像这样从左至右,从上到下一格一格推导出最终结果。不需要存储整个表格,只需要存储前一行的结果,就可以推导出新的一行。下面看一下代码实现:

动态规划算法_第17张图片

方法利用两层迭代,来逐步推导出最终结果。在外层的每一次迭代,也就是对表格每一行的迭代过程中,都会保留上一行的结果数组 preResults,并循环计算当前行的结果数组results。

方法的时间复杂度是 O(n * w),空间复杂度是(w)。需要注意的是,当金矿只有5座的时候,动态规划的性能优势还没有体现出来。当金矿有10座,甚至更多的时候,动态规划就明显具备了优势。

至此,这道题目已经解决了。但是,如果我们将题目改一下,总工人数变成1000人,每个金矿的用工数也相应增加,这时候如果继续使用动态规划来解决就会发现所需的时间复杂度O(n*w),空间复杂度是O(w).显然在n=5,w=1000的时候要计算5000次,开辟1000单位的空间。而如果使用递归来解决,时间复杂度是O(2^n),需要计算32次,开辟5单位(递归深度)的空间。这里我们可以看出,动态规划方法的时间和空间都和W成正比,而简单递归却和W无关,所以当工人数量很多的时候,动态规划反而不如递归。

你可能感兴趣的:(算法)