动态规划算法

时间:2022-12-01 18:39:44

    最近准备工作面试,学习动态规划算法,虽然思想倒是很好理解,但当遇到问题时,自己却无从下手,直至一天,天空划过一道闪电,文思泉涌,终于理解到动态规划的思想精髓,遂记录下来,以一个新手学习的过程理解动态规划。重点在以容易理解的角度出发。

什么是动态规划?

    百科定义:动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

    通俗的定义:动态性体现在问题在动态方程作用下,从一个状态向另一个状态过渡,动态规划算法通常基于一个递推公式及一个或多个初始状态。当前子问题的解将由上一次子问题的解推出。

什么时候用动态规划?

   动态规划算法适用于具有最优子结构的问题。最优子结构即是当前问题可以分解为规模较小的子问题,当前问题达到最优解,则子问题(规模较小问题)也一定达到最优解。当前问题和子问题性质相同,只是规模不同。

  • 优化暴力迭代问题
  • 解决回溯算法
  • 经典问题

           0-1背包问题、青蛙跳台阶问题、换零钱问题(最小张数、最多方法数)

怎么用动态规划?

心中有个表!!

心中有个表!!

心中有个表!!

(我不是在骂人.........)

暴力迭代问题,之所以耗时是因为在迭代过程中,做许多无用的计算,之前的计算的结果没有存储,为下一次计算做调用,动态规划算法则是用空间换时间,每一次计算过程中,将计算得到的结果记录下来,为下一次的计算做调用。

关于动态规划你需要知道的事

     既然是一个通用的算法,那么总要有一个数学模型,下面介绍一下动态规划的模型,理解个核心概念。

v  状态

    描述子问题的解,或者说描述问题的解(不要忘了,子问题只是规模比问题小,其实质完全相同)。这样解释可能不好理解,举个例子:

l Dp[i][j]:表示用i元面值的货币凑够j元的最小张数。

l Dp[i-1][j]:表示用前(i-1)种面值的货币凑够j元的最小张数。

l Dp[i][j-1]:表示用i种面值的货币凑够j-1元的最小张数。

   上述Dp[i-1][j]Dp[i][j-1],Dp[i][j]即是描述这个问题的状态。

   此处比如定义货币币值有[1,2,5,7,10],目标钱数target=20,则最终问题即是求dp[5][20]的值是什么?下文将详细描述这个问题的求解过程,此处理解一下什么事状态即可。

状态转移方程:

     描述问题从一个状态转移到一个状态的表达式,即是一种递推公式,类似于数学里面的数列:给你a1,a2,a3,a4..让你求an=?.

     对于上面的例子,则是告诉你Dp[i-1][j-1]Dp[i-1][j]Dp[i][j-1]等一系列状态,让你求Dp[i][j]是多少?

     对于一个问题,用数学语言表示状态倒是不难(泄密:就是二维数组的一个格子,即是Dp[i][j]),关键点也是难点就是寻找状态转移矩阵,继而用数学表达式表示出来。如果这一步实现了,其问题求解过程就是简单填表的过程。  

   干巴巴数学公式看起来抽象,话不多说,举例说明动态规划的分析过程。

问题一:用最小的纸币数换取目标的钱数

   问题描述:小明(就是我们心目中的小明)去商店买酱油,一瓶酱油2元,小明身上只有10元整钱,商店老板娘小红(对,也是活在我们课本里的小红)手里只有1,2,5元的币值的纸币,老板娘小红有严重的强迫症,每次都希望用最小的纸币数找给顾客,问小红最小需要用多少的纸币数?

分析过程:

(1)这个问题很显然是问:小红可以用[1,2,5]元的纸币,最小需要多少张可以凑10-2=8元?

(2)问题是求最小纸币数得到目标的钱数

   记住之前强调的:心中要有张表!!!心中要有张表!!!中要有张表!!!

   问题状态=表中元素==>dp[i][j]:表示用i种面值的纸币凑够j元需要的最小的张数

(3)动态规划算法适用于具有最优子结构的问题。

   如果不懂最优子结构的问题是什么问题,请转到文章前面去看。

   现在把问题规模变下:

从纸币面值的角度缩小规模

        Dp[i-1][j]:i-1种面值的纸币凑够j元最小需要多少张?

从零钱数的角度缩小规模

  Dp[i][j-1]:i面值的纸币凑够j-1元最小需要多少张?

(4)问题状态转移方程的推导。

    问题是求3种面值的纸币凑够8元,那么我们从上面两个角度出发推导状态转移方程。

试想,我们不知道凑够8元需要多少纸币,但我们如果知道凑够7元最小需要多少张,即知道Dp[3][7],那么凑够8元,只需要在Dp[3][7]+11元就是凑够8元的张数,当然,也可以是Dp[3][6]+12元,Dp[3][6]+21元,Dp[3][3]+15元,Dp[3][3]+51....等等。

   但是这还不一定是最少的,从纸币面值的角度出发缩小问题规模,即假如我们使用2纸币[1,2]也可以凑够8元,最小张数为Dp[2][8],继续,我们用1种纸币面值也可以凑够8元,最小张数为Dp[1][8]

   那么最终结果是什么呢?

就是上面所有情况中,张数最小的那种情况。对于上面问题最优结果是15元、12元、11元。

现在对于这个问题进行抽象:

为了方便理解,此处仍然用具有的面值代替抽象的,只是除去1元的硬币。

现在假定有面值values=[2,3,5],目标钱数target=9元:

Dp[i][j]={min(dp[i-1][j],dp[i][9-count*values[i] ]),i=3,2;j-count*i>=0}

dp[i-1][j]:这部分理解为分别用123中面值的纸币凑够8元;

dp[i][9-count*i]:这部分理解为分别用i种货币,且优先选择面值最大的values[i]硬币,且张数count123...满足条件是j-count*i>=0

有了上面的递推公式,下面就是根据递推公式填表的过程:

(5)定义一张表,m*(n+1)的二维数组Dp[m][n+1],其中,m是硬币币值的种数,ntarget钱数,如下表:行代表币值,列代表0~target钱数:

 

0

1

2

3

4

5

6

7

8

9

2

 

 

 

 

 

 

 

 

 

 

3

 

 

 

 

 

 

 

 

 

 

5

 

 

 

 

 

 

 

 

 

 

下面模拟程序运行步骤进行填表:

1)首先填充第一列:即用2元凑够0元、用23元凑够0元、用235凑够0元;

显示所有的情况都是0张,即经过这一轮填表为

 

0

1

2

3

4

5

6

7

8

9

2

0

 

 

 

 

 

 

 

 

 

3

0

 

 

 

 

 

 

 

 

 

5

0

 

 

 

 

 

 

 

 

 

2)填充第一行:即是用2元凑够0123...9元所需最小张数.

不难理解满足下列公式:

 jj%values[0]==0 Dp[0][jj]=jj/values[0]  

 jj%values[0]!=0 Dp[0][jj]=65536(因为是求最小值,所以不满足情况下,设为最大值)

 

0

1

2

3

4

5

6

7

8

9

2

0

65536

1

65536

2

65536

3

65536

4

65536

3

0

 

 

 

 

 

 

 

 

 

5

0

 

 

 

 

 

 

 

 

 

3)下面按行填充空白的单元

Dp[1][1]:23元凑够1元,显然为0(填充为65536)

Dp[1][2]:23元凑够2元,显然为1

Dp[1][3]:23元凑够3元,显然为1

Dp[1][4]:23元凑够4元,显然为2

依次类推....

Dp[2][9]=min{Dp[1][9],Dp[2][9-1*5],Dp[2][9-2*5]},显然删除线部分不满足target-count*values[i]>=0的条件。

4)最终填充的表格如下:

 

0

1

2

3

4

5

6

7

8

9

2

0

65536

1

65536

2

65536

3

65536

4

65536

3

0

65535

1

1

2

2

2

3

3

3

5

0

65535

1

1

2

1

2

2

2

3

5)我们问题求解的结果是Dp[3][9]=3

6)至此问题解决。

 所以归纳下来,动态规划求解过程有以下几步:

1)定义状态,即如何定义一张表

2)归纳推导递推方程

3)填充表的第一列

4)填充表的第一行

5)填充表的余下单元

6)求出最终结果。

此题具体代码详见文章http://blog.csdn.net/u013291818/article/details/57082263

 

问题二:一定面值的纸币,换取目标钱数,最多有多少种换法?

   仍然以问题一种数据举例:

   硬币面值 values[3]=[2,3,5],target = 9;

(1)定义状态。

  Dp[i][j]:i种面值的硬币,凑够j元最多方法数

(2)归纳推导递推方程。

   Dp[i][j]={Dp[i-1][j]+Dp[i][j-1*values[i]]+Dp[i][j-2*values[i]]+Dp[i][j-count*values[i]]}  j-count*values[i]>0

上述公式很好理解:

我们固定一个币值的硬币:

   Dp[i-1][j]:用0values[i]面值的硬币的方法数

   Dp[i][j-1*values[i]]:用1values[i]面值的硬币的方法数

   Dp[i][j-2*values[i]]:用2values[i]面值的硬币的方法数

   .......

   Dp[i][j-count*values[i]]:用countvalues[i]面值的硬币的方法数,其中j-count*values[i]>0

则最终Dp[i][j]即是上述方法的和。

(3)填表

具体填表方法与问题一相同,只是条件改变

 jj%values[0]==0 Dp[0][jj]=jj/values[0]  

 jj%values[0]!=0 Dp[0][jj]=0(因为是求最值,所以不满足情况下,设为最)

(4)求出最终结果。

此题具体代码见

http://blog.csdn.net/u013291818/article/details/56851079

 

问题三:青蛙跳台阶

    问题描述:一只青蛙一次可以跳上 1 级台阶,也可以跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法。

可以将问题转换硬币问题:

青蛙只有两种选择:跳一级台阶和跳2级台阶,即是面值只有[1,2]两种面值

青蛙跳上N级台阶有多少种跳法?:即是目标钱数为N,有多少种方法。

现在这个问题转换为问题二。此处不再赘述。

  

    对于上述三个问题的描述,应该能够理解动态规划的解题方法,博主处于学习阶段,理解也许有偏颇之处,文中如有描述错误之处,还请指出,共同进步。【不要吝啬你的赞】