一、算法思想
动态规划算法的基础是最优原理,它是用来解决贪婪算法和分治算法无法解决或者无法简洁高效解决的算法,一般用于求解下列问题:(1)背包问题 (2)最短路径 等。
动态规划和贪婪算法一样,都一个问题的求解都是分为对很多问题的求解的综合,问题最终的解是多次选择的综合结果,但是贪婪算法中,每次选择最优解,之后不可撤回,但是动态规划中需要考察一系列的抉择,然后才能确定一个最优抉择序列是否包含最优抉择子序列,具体就通过下面的例子来说明:
1.1 最短路径
有下图所示的单向图,求从源点1到目的点5的最短路径?
如果是利用贪婪算法来进行求解,那么第一步可以到达的点为2,4,3。按照算法,到点3的距离是最短的,所以选择走到点3,然后点3可以到达2,4,接下来走到点4,因为到点4的距离最短,然后此时可以通过点4到达目的点5,最后选得的路径是1->3->4->5,路径长度为7.虽然确实是最短路径,但是可以看出,从1到3,从3到4,从4到5,整个过程是割裂开的,但是一个最短路径是求的整个过程加起来的路径长度最短,很多时候贪婪算法不一定适用,因为它不能回退。
如果是动态规划,那么要考虑的就是,首先选择了从1到3这条路径,之后要确定如何从3到5,如果3到5选择的路径不对,那么就算1到3是最短的,整个结果依然会偏大,所以假设选择了某个点x作为最短路径中的某个点,那么接下来说选择的从x到目的点的路径必须是最短的,这就是包含最优抉择序列的意思,因为考虑这种情况:一个点都没选的时候,此时就相当于要必须选择出从源点到目的点的最短路径,所以可以求出最短路径。
1.2 0/1背包问题
以1.2作为例子举例,如果要求f(3,116),那么可得f(3,116)=max(f(2,116),f(2,16)+20),由公式可知f(2,116)=max(f(1,116),f(1,102)+18)、f(2,16)=max(f(1,16),f(1,2)+18),同理求得f(1,116)=15,f(1,102)=15,f(1,16)=15,f(1,2)=0. 例如有类似于例1.2的题目,但是数据是n=4,w=[20,20,14,10],c=100,p=[20,20,18,15],在计算f(4,100)=max(f(3,100),f(3,80)+20)中,f(3,100)后续会计算f(2,80),而f(3,80)后续也会计算f(2,80),如果不能很好的避免这些重复计算,动态规划的优越性就荡然无存,下面就用几个具体的例子的dp解法来进行实战。 可以从上面两张图看出,随着不断加入新的液体,最大值也在不断的更新,并且是在满足这个二维数组的纬度下的更新,这个二维数组的纬度其实就是题目的约束条件。
有n个物品和一个容量为c的背包,从n个物品中选择装包的物品,每个物品有自己的重量w和价值p,要求算出如何在不超过背包容量的情况下,要装入物品的价值p最大。
假设n=3,w=[100,14,10],c=116,p=[20,18,15],如果选择装入第一个物品,那么问题转变为求解c=16,w[14,10],p=[18,15]的最优解,此时有两种解法:装入第一个或者装入第二个,明显看出因为p1>p2,所以装入第一个是此时的最优解,所以得到此条件下的最优结果为[1,1,0],1代表装入物品,从上可以看出,如果某一步所做的解不是此状态下的最优解,那么它肯定不是初始问题的最优解。也可以不装入第一个物品,此时得到另外一个解[0,1,1],[0,1,1]是在不装入物品1下的最优解。此时通过计算可以知道肯定是[1,1,0]是最好的结果,可是数据如果很大的时候,该怎么判断某个状态之后,做出的解就是最优解呢,或者进入这个状态之前所做的导致进入这个状态的一个解也是最优解呢,因为如果在某状态下做出的某一个解不是最优解,那这个解也一定不是最初始答案的最优解。
所以上述这种问题的核心就是一个,如何确定做的这一步是最优解的一个组成部分,动态规划的方法是建立一个动态规划递归方程,dp和贪婪算法不一样,是可逆的,可以通过递归方程不断进行迭代,不断修正做出的选择。
例如1.2中提出的背包问题,最优选择序列是由最优子选择序列构成的,我们假设f(i,y)表示背包剩下的容量为y,剩下物品是i、i+1....n的背包问题的最优解。
pn代表第n个物品的价值,wn代表第n个物品的重量,y代表剩余容量。同一个等式写两遍,代表两种不同的情况,视y的大小决定。那么可以得到如下的等式:f(n,y)=pn (y>=wn))f(n,y)=0(0<=y
那么可以算出f(3,116)=max(max(15,15+18),max(15,0+18)+20)=max(33,38)=38。得出最优解是38.然后可以由3个f(n,y)(n等于1,2,3)之间的关系判断出最终结果为[1,1,0],1代表装入背包,0代表不装入背包。
由上面的计算过程可以看出,无论之前的选择是什么,接下来的选择一定是当前状态下的最大值,也就是最优选择,这就是最优原则,它代表了一个最优选择序列一定由最优选择子序列所构成的,所以应用动态规划就一定要证明该问题适用最优原则,然后就建立动态规划的递归方程组,然后通过不断迭代递归求解该递归方程组,然后由最优解以及各个不同的f(n,y)之间的关系可以求出最优解的组成,上述那个简单的例子中并没有一些重复的计算,但是在复杂的动态规划问题中是存在很多重复的计算的,如果不能避免这些重复的计算,dp的复杂度也会很高.
二、实际应用
2.1 0/1背包问题
(1)递归求解
在第一大节中所说的函数f求解方程如下所示:int f(int i,int thecapacity)
{
if(i==numberofobjects)//遍历到了最后一个物品,这很明显也是递归的出口。
return (thecapacity
其中的numberofobjects代表的是物品的个数,i代表目前遍历到了第几个物品,thecapacity代表的是背包剩下的容量,weight数组代表的是每个物品的重量,profit数组代表的是每个物品的价值,其中profit和weight还有thecapacity是全局变量,上述代码的时间复杂度是O(2^n)。
上述这种问题在一个实际的例子当中时,比如n=5,p=[6,3,5,4,6],w=[2,2,6,5,4],c=10的时候,就会很明显的出现第一大节中提到的重复计算,具体可看下一图:
上图中的波浪线所标注的就是出现重复计算的部分,其实只要在上图树中只要两个节点高度相同,并且剩余容量相同,那么它们所有的后续计算都是重复计算,一般为了避免这种重复计算,采用的方式都是建立一个dp数组,该数组用来存储计算过的f(n,y)的值,具体可以看下面这种无重复计算的f函数:
int f(int i,int thecapacity)
{
if(dp[i][thecapacity]>=0)
return dp[i][thecapacity];//若已经计算过就不用再次计算了
if(i==numberofobjects)//遍历到了最后一个物品,这很明显也是递归的出口。
{
dp[i][thecapacity]=(thecapacity
时间复杂度已经降低到了O(cn)(c表示背包容量,n表示物品个数),其中依然有numberofobjects代表的是物品的个数,i代表目前遍历到了第几个物品,thecapacity代表的是背包剩下的容量,weight数组代表的是每个物品的重量,profit数组代表的是每个物品的价值,其中profit和weight还有thecapacity是全局变量,多增加了一个全局变量dp,它的定义应当是vector
2.2最长子序列
下面的代码是求最长严格上升子序列(元素之间不需要连续),dp在这种求最长某种限制的子序列,或者说求最大之类的情况下,都是比较适用的,比如下面这个题,具体的就在代码里面看吧。
#code
#include
运行的结果如下所示:
输入数组为[4,2,1,2,4,1,6,8,9],可以知道它的最长严格上升子序列为[1,2,4,6,8,9],符合输出答案的长度为6.
2.3求最大值
爱玩游戏的小J,小J给每个游戏标上一个成就值,同时估算了完成这些游戏所需要的时间,现在他只有X天时间,而游戏一旦开始玩,至少需要玩一天才能够停下来,那么他玩完的游戏的成就值之和最大能达到多少呢?
虽然是游戏,但其实就是一个0/1背包问题,完成这个游戏等同于放入背包,x天等同于背包容量,所以依然是一样的办法。
#code
#include
输出结果如下所示:
输入:
第一行输入case数T,对于每个case,第一行输入游戏的数目N,总时间X。从第二行到第N+1行输入游戏的成就值Ai,所需要的时间Bi。
输出:
对每个case输出一行,成就值之和的最大值。
第一个例子输入的是2天的情况下,一个游戏的价值10,耗时1天,一个游戏耗时2天,成就20,明显结果是20.
第二个例子输入的是4天的情况下,一个游戏的价值10,耗时2天,一个游戏的价值18,耗时3天,一个游戏的价值10,耗时2天,结果为20,是最大值。
2.4求最大值(依然是一个求最大值的问题,但是多了一个约束条件,相当于约束条件多一点的背包问题)
维克多博士创造了一个裂变反应堆,可取用处于液体状态的放射性物质。反应堆的容量是V加仑。他有N瓶的放射性液体,每个都有一定的质量和一定的体积。当液体倒入反应堆时,也产生一些单位的能量。现在,维克多想要将能量输出最大化。但是,有一个限制条件。他研究了原子元素的物理知识和历史,认识到反应堆内放射性液体的总量不能超过特定的临界质量M,否则反应就会失控,并引发剧烈的爆炸。写一个算法,帮助他从反应堆获得最大的能量,而不会让他丢掉性命。
输入:
该函数/方法的输入包括六个参数------
reactorCap,一个整数,表示反应堆的容量(V);
numberOfRadLiquid,一个整数,表示现有小瓶的数量(N);
criticalMass,一个整数,表示反应堆的最大临界质量(M);
volumes,一个整数列表,按顺序表示N份放射性液体的体积;
masses,一个整数列表,按顺序表示N份放射性液体的质量;
energies,一个整数列表,按顺序表示N份放射性液体产生的能量。
输出:
返回一个整数,表示可给定的约束条件下从反应堆中产生的最大能量。
示例:
输入:
reactorCap=100,numberOfRadLiquid=5,criticalMass=15,volumes=[50,40,30,20,10],masses=[1,2,3,9,5],energies=[300,480,270,200,180]
输出:
960
解释:
选择1、2、5号瓶中的液体,产生的能量=300+480+180=960.
#思路
利用动态规划解决这道题时,使用一个函数f来表示最大能量,方程为f(v,m,n), 这个方程的解表示n个小瓶在约束条件最大质量m,最大容量v下可以获得的最大能量,那么很显然f(v,m,n)=max(f(v,m,n),f(v,m,n-1),f(v-v(k),m-m(k),n)),利用一个二维数组表示在某个体积和质量的约束下可获得的最大能量。
#code
#include